ios - explicado - mvvm vs mvc
¿Dónde colocar la creación de vistas en patrones MVVM y MVC? (1)
Disculpe si es un tema repetido.
Normalmente escribo mis aplicaciones sin guiones gráficos y pongo la creación de vistas en "viewDidLoad", como:
class LoginVC: UIViewController {
var view1: UIView!
var label1: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
loadStaticViews()
}
func loadStaticViews() {
view1 = UIView()
label1 = UILabel()
view.addSubview(view1)
view1.addSubview(label1)
// constraints...
}
}
Y ahora quiero probar el patrón MVVM en mi próxima aplicación, y simplemente no estoy seguro de dónde poner la creación de vistas. Ahora pienso en algo así:
class LoginVCViews {
static func loadViews<T, T1, T2>(superview: UnsafeMutablePointer<T>, view: UnsafeMutablePointer<T1>, label: UnsafeMutablePointer<T2>) {
guard let superview = superview.pointee as? UIView else { return }
let v = UIView()
let l = UILabel()
superview.addSubview(v)
v.addSubview(l)
// constraints...
view.pointee = v as! T1
label.pointee = l as! T2
}
}
class LoginVC: UIViewController {
private var view1: UIView!
private var label1: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
LoginVCViews.loadViews(superview: &view, view: &view1, label: &label1)
}
}
Qué piensas ? No estoy muy familiarizado con UnsafeMutablePointer y no estoy seguro de que haya algunos problemas. ¿Y cuánto es feo?
Tal vez deberías probar la ruta totalmente orientada a objetos. Una composición de vista se ve algo así:
// conjunto de protocolos reutilizables
protocol OOString: class {
var value: String { get }
}
protocol Executable: class {
func execute()
}
protocol Screen: class {
var ui: UIViewController { get }
}
protocol ViewRepresentation: class {
var ui: UIView { get }
}
// funcionalidad reutilizable (sin dependencia de uikit)
final class ConstString: OOString {
init(_ value: String) {
self.value = value
}
let value: String
}
final class ExDoNothing: Executable {
func execute() { /* do nothing */ }
}
final class ExObjCCompatibility: NSObject, Executable {
init(decorated: Executable) {
self.decorated = decorated
}
func execute() {
decorated.execute()
}
private let decorated: Executable
}
// UI reutilizable (dependencia de uikit)
final class VrLabel: ViewRepresentation {
init(text: OOString) {
self.text = text
}
var ui: UIView {
get {
let label = UILabel()
label.text = text.value
label.textColor = UIColor.blue
return label
}
}
private let text: OOString
}
final class VrButton: ViewRepresentation {
init(text: OOString, action: Executable) {
self.text = text
self.action = ExObjCCompatibility(decorated: action)
}
var ui: UIView {
get {
let button = UIButton()
button.setTitle(text.value, for: .normal)
button.addTarget(action, action: #selector(ExObjCCompatibility.execute), for: .touchUpInside)
return button
}
}
private let text: OOString
private let action: ExObjCCompatibility
}
final class VrComposedView: ViewRepresentation {
init(first: ViewRepresentation, second: ViewRepresentation) {
self.first = first
self.second = second
}
var ui: UIView {
get {
let view = UIView()
view.backgroundColor = UIColor.lightGray
let firstUI = first.ui
view.addSubview(firstUI)
firstUI.translatesAutoresizingMaskIntoConstraints = false
firstUI.topAnchor.constraint(equalTo: view.topAnchor, constant: 100).isActive = true
firstUI.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20).isActive = true
firstUI.widthAnchor.constraint(equalToConstant: 100).isActive = true
firstUI.heightAnchor.constraint(equalToConstant: 40).isActive = true
let secondUI = second.ui
view.addSubview(secondUI)
secondUI.translatesAutoresizingMaskIntoConstraints = false
secondUI.topAnchor.constraint(equalTo: firstUI.topAnchor).isActive = true
secondUI.leadingAnchor.constraint(equalTo: firstUI.trailingAnchor, constant: 20).isActive = true
secondUI.widthAnchor.constraint(equalToConstant: 80).isActive = true
secondUI.heightAnchor.constraint(equalToConstant: 40).isActive = true
return view
}
}
private let first: ViewRepresentation
private let second: ViewRepresentation
}
// un viewcontroller
final class ContentViewController: UIViewController {
convenience override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
self.init()
}
convenience required init(coder aDecoder: NSCoder) {
self.init()
}
convenience init() {
fatalError("Not supported!")
}
init(content: ViewRepresentation) {
self.content = content
super.init(nibName: nil, bundle: nil)
}
override func loadView() {
view = content.ui
}
private let content: ViewRepresentation
}
// y ahora la lógica comercial de una pantalla (no reutilizable)
final class ScStartScreen: Screen {
var ui: UIViewController {
get {
return ContentViewController(
content: VrComposedView(
first: VrLabel(
text: ConstString("Please tap:")
),
second: VrButton(
text: ConstString("OK"),
action: ExDoNothing()
)
)
)
}
}
}
Uso en AppDelegate:
window?.rootViewController = ScStartScreen().ui
Nota:
- sigue las reglas de codificación orientada a objetos (codificación limpia, objetos elegantes, patrón decorador, ...)
- cada clase es muy simple construida
- las clases se comunican por protocolos entre sí
- todas las dependencias están dadas por inyección de dependencia en la medida de lo posible
- todo (excepto la pantalla comercial al final) es reutilizable -> de hecho: la cartera de código reutilizable crece cada día que codifica
- la lógica comercial de su aplicación se concentra en implementaciones de objetos Screen
- la prueba unitaria es muy simple cuando se utilizan implementaciones falsas para los protocolos (incluso la burla no es necesaria en la mayoría de los casos)
- problemas menores con ciclos de retención
- evitando Null, nil y Optionals (contaminan su código)
- ...
En mi opinión, es la mejor forma de codificar, pero la mayoría de la gente no lo hace así.