📜 ⬆️ ⬇️

Binding in Swift. We take the first steps to MVVM

Good day. This article will be useful for those who are tired of struggling with the variability of data in the interface, for those who are not aware of the existence of MVVM, and for those who doubt that this pattern can be successfully applied in practice when developing iOS applications. Interested please under the cat.

Prehistory with a bunch of lyrics
I can not call myself an experienced iOS developer. Although acquaintance with the world of iOS took place a few years ago, the desire to pump in the development of applications for iOS came to me quite recently. My path was thorny. Obj-C was not immediately impressed, I wanted to develop applications on something familiar. Therefore, in the course went PhoneGap, Apcelerator Titanium and that's it. But, naturally, nothing came of these undertakings. After a long break, the company in which I work seriously thought about developing a mobile application. I didn’t invent anything and simplify my life - I did work exclusively on ObjC without using third-party frameworks. And it was a pain for me. Simple things turned out to be difficult, I could not cope with autolayout, it was impossible to look at the code. Therefore, in the next project, Xamarin Forms was launched. Having worked for about two months on the project, it became clear that this technology was still far from perfect (as a result, it turned out that the project was in beta status, but this was not mentioned much of where). But while working with Xamarin Forms, I was filled with many patterns with which this project was infused, moreover I had to make a bunch of custom components, which led to a clearer understanding of the work of UIKit. At that moment, when it became clear that our project should be rewritten to the native, Swift was rapidly approaching the release. I read a book on this language and I felt strong enough to start over. But the first experience still reminded of itself, so I began to dig in the direction of MVVM in iOS. I really liked this concept.

At that time, all the articles that came across my eyes offered to solve the problem with the help of ReactiveCocoa. Looking at the code samples of this library, I realized that I still had to study and learn, because I did not understand what I saw. For Swift also offered to use ReactiveCocoa. Actually the article from Colin Eberhardt was for me the starting point. But I soon had to face the fact that the approach described by the aforementioned author led to memory leaks. Apparently I did something wrong and then did not understand what it was. Plus ReactiveCocoa remained a black box for me. It was decided to get rid of this library, given that it was used only for linking view models with view. I stumbled upon the Observable Swift project, which solved the binding problem. Soon our project was completed, and on the horizon a new one, and I wanted to thoroughly prepare for it.

Formulation of the problem


At the moment I can not imagine how you can smoothly introduce MVVM into UIKit. It means that MVVM that I saw in Xamarin Forms and which impressed me so much. Most likely, you will need to write a framework on top of UIKit and bind the developer to this framework. We will take the path of least resistance: we will use what Apple gives us. But at the same time we will strive for a more declarative description of the UI.
')
The first and most important thing that attracted me to MVVM is the dynamic binding of ViewModel and View. This allows us to describe business logic in isolation from the presentation. We have already got used to describe logic in ViewController. And this is real hell. Let's aim to minimize the code in the ViewController. First you need to learn to understand that the state of our ViewModel has changed and this change needs to be reflected in the UI. Apple offers us to use, for example, KVO. ReactiveCocoa would simplify this task. But we have Swift. And we want to make our decision as simple and clean as possible. Here is how our colleagues offer to solve this problem:

By the way, do not forget about the upcoming release of Reactive Cocoa 3.0 . But so far the Bond library is the most suitable for our task. While I was working on what I show below, Bond was just beginning its existence and did not fit my requirements. Even now, it doesn’t fit in with them, plus it seemed to me that the developer somehow complicated everything. I also wanted to simplify everything as much as possible. But, in truth, coming to a standstill while working on my vision of how data should be associated with representations, I now and then found the answers in the Bond source code.

Dynamic


Let's start small and, at the same time, the most important thing. We need to be able to learn about changes in the state of a variable and somehow respond to these changes. Let me remind you that we strive for simplicity and brevity. And in this case, Swift appears in all its glory. He gives us generics, lambdas with amazing syntax, observable properties. So let's pile something out of it.
class Dynamic<T> { init(_ v: T) { value = v } var value: T { didSet { println(value) } } } 

Now we have the opportunity to follow the change in value . In practice, it will look something like this:
 let dynamicInt: Dynamic<Int> = Dynamic(0) println(dynamicInt.value) dynamicInt.value = 1 dynamicInt.value = 17 

Add listener support for our mutable entity. The listener will be an anonymous function, in whose argument we will pass a new value .
 class Dynamic<T> { typealias Listener = T -> () private var listeners: [Listener] = [] init(_ v: T) { value = v } var value: T { didSet { for l in listeners { l(value) } } } func bind(l: Listener) { listeners.append(l) l(value) } func addListener(l: Listener) { listeners.append(l) } } 

The addListener method simply adds the handler to its listener list, and the bind method does the same, but it immediately calls the added listener and passes it the current value .
 let dynText: Dynamic<String> = Dynamic("") dynText.bind { someLabel.text = $0 } dynText.addListener { otherLabel.text = $0 } dynText.value = "New text" 

Through the use of generics, we do not need to check or make data types coercions. The compiler will do it for us. For example, in the following case, the code will not be compiled:
 let dynInt: Dynamic<Int> = Dynamic(0) dynInt.bind { someLabel.text = $0 } 

The compiler knows that the argument of our listener is of type Int and we cannot assign the value of this argument to the text field to the object of the UILabel class, since the type of this field is String . Moreover, thanks to the simplified syntax of anonymous functions, we were able to add listeners without unnecessary scribbling. But there is no limit to perfection. We can define a couple of operators, or overload existing ones in order to further reduce the code.
 func >> <T>(left: Dynamic<T>, right: T -> Void) { return left.addListener(right) } infix operator >>> {} func >>> <T>(left: Dynamic<T>, right: T -> Void) { left.bind(right) } 

 let dynText: Dynamic<String> = Dynamic("") dynText >>> { someLabel.text = $0 } dynText >> { otherLabel.text = $0 } dynText.value = "New text" 

Thoughts about unowned, weak, Void and Void?
In practice, the examples described above will lead to memory leaks. Here is an example:
 class MyViewController: UIViewController { @IBOutlet weak var label: UILabel! let viewModel = MyViewModel() override func viewDidLoad() { viewModel.someText >>> { self.label.text = $0 } super.viewDidLoad() } } 

It is obvious that now the listener function and self are rigidly connected with each other and the object of the class MyViewController will never be deleted . To avoid this, it is necessary to weaken the connection:
 viewModel.someText >>> { [unowned self] in self.label.text = $0 } 

That's better. But there is one thing. There is no guarantee that the listener function will not be called after deleting the MyViewController object. To protect ourselves, we use weak :
 viewModel.someText >>> { [weak self] in self?.label.text = $0 } 

But in this case, the code will not be compiled, because our listener is of type String -> Void? , and must be of type String -> Void for successful compilation. Therefore, I initially added two types of listeners to Dynamic : with returned values ​​of Void and Void? .. Accordingly, I overloaded the bind and addListener methods to support two types of listeners. But it soon became clear that the compiler could not determine which method to call, if done, for example, like this:
 viewModel.someText >>> { [weak self] in if self != nil { self!.label.text = $0 } } 

Therefore, the idea of ​​supporting two types of listeners had to be abandoned and take advantage of such tricks:
 viewModel.someText >>> { [weak self] in self?.label.text = $0; return } viewModel.someText >>> { [weak self] in self?.label.text = $0; () } viewModel.someText >>> { [weak self] v in v; self?.label.text = v } 

Of course, it would be possible to abandon the use of weak in favor of passing a dynamic object, in addition to the handler function, to the object reference and not to call the function if the object was suddenly deleted. This approach is used in the Bond library. But it was not my way :)

Simplify working with UIKit


Agree, it is unpleasant to constantly describe the same lambda for text binding and UILabel. I want simplicity:
 viewModel.someText >>> label 

Nothing is impossible. After all, we can easily come to this syntax. The idea of ​​implementation is again kindly borrowed from Bond . The idea is simple: we will store a field with an listener for an object of some kind, and we will be able to bind this listener to a dynamic object.
 final class PropertyModifier<T> { typealias Modifier = (T) -> () let modifier: Modifier init (_ l: Modifier) { self.modifier = l } } 

The object of the class PropertyModifier will be created by the view itself, and a lambda with a code that changes the value of a specific field of the view will be passed to the constructor.
 private var UILabelPropertyKeyTextModifier: UInt8 = 0 extension UILabel { var textModifier: PropertyModifier<String?> { if let pm: AnyObject = objc_getAssociatedObject(self, &UILabelPropertyKeyTextModifier) { return pm as PropertyModifier<String?> } else { let pm = PropertyModifier<String?> { [weak self] in self?.text = v; () } objc_setAssociatedObject(self, &UILabelPropertyKeyTextModifier, pm, objc_AssociationPolicy(OBJC_ASSOCIATION_RETAIN_NONATOMIC)) return pm } } } 

I note that in the extension we cannot describe the stored fields (stored properties), so the ObjC Runtime and the functions objc_setAssociatedObject , objc_getAssociatedObject come to the rescue . Now we can do this:
 viewModel.someText >>> label.textModifier.modifier 

Let's simplify:
 func >> <T>(left: Dynamic<T>, right: PropertyModifier<T>) { left.addListener(right.modifier) } func >>> <T>(left: Dynamic<T>, right: PropertyModifier<T>) { left.bind(right.modifier) } viewModel.someText >>> label.textModifier 

Much better. But that is not all. We can select any most used property of the view and assign it a default PropertyModifier .
 protocol BindableObject { typealias DefaultPropertyModifierTargetType var defaulPropertytModifier: PropertyModifier<DefaultPropertyModifierTargetType> { get } } extension UILabel: BindableObject { typealias DefaultPropertyModifierTargetType = String? var defaulPropertytModifier: PropertyModifier<DefaultPropertyModifierTargetType> { return textModifier } } func >> <T, B: BindableObject where B.DefaultPropertyModifierTargetType == T>(left: Dynamic<T>, right: B) { left.addListener(right.defaulPropertytModifier.modifier) } func >>> <T, B: BindableObject where B.DefaultPropertyModifierTargetType == T>(left: Dynamic<T>, right: B) { left.bind(right.defaulPropertytModifier.modifier) } 

That's all. UILabel got a standard PropertyModifier that changes the value of the text field. And we came to the appointed goal, namely, we can create a relationship as follows:
 viewModel.someText >>> label 

Teams


One of the remarkable concepts in Xamarin Forms that I like is the commands. In fact, we can describe a command using two functions: one returns true or false , indicating that the command can be executed, and the second an action that the command performs. Suppose we have a button ( UIButton ). The button has an enabled field, the button can be pressed by the user, after which some action should take place. Remember that we are committed to the declarative description of the behavior of the interface? So let's extend this idea to our controls.
 final class Command<T> { typealias CommandType = (value: T, sender: AnyObject?) -> () weak var enabled: Dynamic<Bool>? private let command: CommandType init (enabled: Dynamic<Bool>, command: CommandType) { self.enabled = enabled self.command = command } init (command: CommandType) { self.command = command } func execute(value: T) { execute(value, sender: nil) } func execute(value: T, sender: AnyObject?) { var enabled = true if let en = self.enabled?.value { enabled = en } if enabled { command(value: value, sender: sender) } } } protocol Commander { typealias CommandType func setCommand(command: Command<CommandType>) } func >> <T, B: Commander where B.CommandType == T>(left: B, right: Command<T>) { left.setCommand(right) } private var UIButtonPropertyKeyCommand: UInt8 = 0 extension UIButton: Commander { typealias CommandType = () func setCommand(command: Command<CommandType>) { if let c: AnyObject = objc_getAssociatedObject(self, &UIButtonPropertyKeyCommand) { fatalError("Multiple assigment to command") return } objc_setAssociatedObject(self, &UIButtonPropertyKeyCommand, command, objc_AssociationPolicy(OBJC_ASSOCIATION_ASSIGN)) command.enabled?.bind { [weak self] in self?.enabled = $0; () } addTarget(self, action: Selector("buttonTapped:"), forControlEvents: .TouchUpInside) } func buttonTapped(sender: AnyObject?) { if let c: Command<CommandType> = objc_getAssociatedObject(self, &UIButtonPropertyKeyCommand) as? Command<CommandType> { c.execute((), sender: sender) } } } 

So, we have a command that has an enabled field and a function that must be executed when calling the execute method. We need to associate our team with a button. For this, we got the Commander protocol with the setCommand method. We implement our protocol for UIButton by associating the dynamic field of the enabled command with the corresponding UIButton property. We also overloaded the operator >> for convenience. What we get in the end:
 class PageModel { let nextPageEnabled: Dynamic<Bool> = Dynamic(true) lazy var openNextPage: Command<()> = Command ( enabled: self.nextPageEnabled, command: { [weak self] value, sender in //Open next page }) } class MyViewController: UIViewController { @IBOutlet weak var nextButton: UIButton! let pageModel = PageModel() override func viewDidLoad() { nextButton >> pageModel.openNextPage super.viewDidLoad() } } 

Conclusion


We have dynamic objects at our disposal that we can associate with anything. We have teams that allow us to describe the action by pressing the button more expressively. And this is already enough to simplify our UIViewController . Behind the scenes were map and filter for Dynamic , bidirectional binding and simplified work with UITableView . But you can look at it yourself. A project demonstrating the capabilities of the approach described is available on GitHub . I recommend to take a look at it.
A couple of examples for the seed
 class TwoWayBindingPage: Page { typealias PMT = TwoWayBindingPageModel @IBOutlet weak var switchLabel: UILabel! @IBOutlet weak var switchControl: UISwitch! @IBOutlet weak var switchButton: UIButton! @IBOutlet weak var textFieldLabel: UILabel! @IBOutlet weak var textField: UITextField! @IBOutlet weak var textFieldButton: UIButton! @IBOutlet weak var sliderLabel: UILabel! @IBOutlet weak var slider: UISlider! @IBOutlet weak var sliderButton: UIButton! override func bindPageModel() { super.bindPageModel() let pm = pageModel as PMT switchButton >> pm.changeSomethingEnabled textFieldButton >> pm.changeUserName sliderButton >> pm.changeAccuracy pm.somethingEnabled | { "Current dynamic value: \($0)" } >>> switchLabel pm.userName | { "Current dynamic value: \($0)" } >>> textFieldLabel pm.accuracy | { "Current dynamic value: \($0)" } >>> sliderLabel pm.somethingEnabled <<>>> switchControl pm.userName <<>>> textField pm.accuracy <<>>> slider } } class BeerListPage: Page { typealias PMT = BeerListPageModel @IBOutlet weak var tableView: UITableView! private var tableViewHelper: SimpleTableViewHelper! override func bindPageModel() { super.bindPageModel() let pm = pageModel as PMT tableViewHelper = SimpleTableViewHelper(tableView: tableView, data: pm.beerList, cellType: BeerTableCell.self, command: pm.openBeerPage) tableView.pullToRefreshControl >> pm tableView.infiniteScrollControl >> pm } } 


Thanks for attention. Comments, suggestions and criticism are welcome.

Source: https://habr.com/ru/post/252535/


All Articles