Hi, Habr! My name is Bogdan, in Badoo I work in a mobile team of an iOS developer. We rarely say anything about our mobile development, although articles are one of the best ways to document good practices. This article will tell you about several useful approaches that we use in our work.
For several years now, the iOS community has been fighting UIKit. Someone comes up with complex ways of “burying” the insides of UIKit under layers of abstractions in their fictional architectures, other teams rewrite it, amusing their ego, but leaving behind a wild amount of code that needs to be maintained.
I'm lazy, so I try to write only the code that is needed. I want to write code that meets the requirements of the product and the quality standards adopted by the team, but I minimize the amount of code to support the infrastructure and standard pieces of architectural patterns. Therefore, I believe that instead of fighting UIKit, we should take it and use it as widely as possible.
Any problem can be solved by adding another level of abstraction. Therefore, many people choose VIPER - there are many levels / entities in it that can be used in work. Writing an application in VIPER is not difficult - it is much more difficult to write an MVC application with the same advantages with the support of a smaller amount of generic code.
If you start a project from scratch, you can choose an architectural template and do everything “right” from the very beginning. But in most cases, this luxury is not available to us - we have to work with the existing code base.
Let's conduct a mental experiment.
You are joining a team that has developed a large code base. What approach do you hope to see in it? Pure MVC? Any MVVM / MVP with flow controllers? Maybe a VIPER approach or a Redux-based approach in some FRP framework? Personally, I expect to see the simplest and working approach. Moreover, I want to leave behind such a code that anyone can read and correct.
In short, let's see how you can do something on the basis of view controllers, rather than trying to replace or hide them.
Suppose you have a set of screens, each of which is represented by one controller. These view controllers extract some data from the Internet and display it on the screen. From the point of view of the product, everything works perfectly, but you have no idea how to test the code for the controllers, and attempts to reuse end with copy-and-paste, which is why the view controllers increase in size.
Obviously, you need to start sharing code. But how to do it without unnecessary trouble? If you pull the code that retrieves the data into a separate object, the controller will only display information on the screen. So do:
Now everything looks very similar to MVVM, so we will use its terminology. So, we have a view and a view model. We can easily test this model. Let's move the repetitive tasks to the services like working with the network and storing data.
As a result:
What does all this have to do with UIKit? Let me explain.
The view model is stored by the view controller, and is not at all interested in whether the controller exists. So if we remove the controller from the memory, then the corresponding model will also be deleted.
On the other hand, if the controller is stored by another object (for example, a presenter) in the MVP, then if for some reason the controller is unloaded, the connection between it and the presenter is broken. And if you think that it is difficult to accidentally unload the wrong controller, then carefully read the description of UIViewController.dismiss(animated:completion:)
.
So I think that the safest thing would be to recognize the view controller as the king, and, therefore, non-UI objects are divided into two categories:
Why is it so tempting to shove all the code into the view controller? Yes, because in the controller we have access to all the data and the current state of the view. If you need to have access to the presentation life cycle in a model or presenter, you will have to pass it manually, and this is normal, but you will have to write more code.
But there is another solution. Since view controllers are capable of working with each other, Sorush Hanlow suggested using this to distribute work among small view controllers .
You can go even further and apply a universal way of connecting to the lifecycle of the view controller - ViewControllerLifecycleBehaviour
.
public protocol ViewControllerLifecycleBehaviour { func afterLoading(_ viewController: UIViewController) func beforeAppearing(_ viewController: UIViewController) func afterAppearing(_ viewController: UIViewController) func beforeDisappearing(_ viewController: UIViewController) func afterDisappearing(_ viewController: UIViewController) func beforeLayingOutSubviews(_ viewController: UIViewController) func afterLayingOutSubviews(_ viewController: UIViewController) }
I will explain with an example. Suppose we need to define screenshots in the chat view controller, but only when it is displayed on the screen. If to carry out this task in VCLBehaviour, then everything becomes simpler simple:
open override func viewDidLoad() { let screenshotDetector = ScreenshotDetector(notificationCenter: NotificationCenter.default) { // Screenshot was detected } self.add(behaviours: [screenshotDetector])}
In the implementation of the behavior is also nothing complicated:
public final class ScreenshotDetector: NSObject, ViewControllerLifecycleBehaviour { public init(notificationCenter: NotificationCenter, didDetectScreenshot: @escaping () -> Void) { self.didDetectScreenshot = didDetectScreenshot self.notificationCenter = notificationCenter } deinit { self.notificationCenter.removeObserver(self) } public func afterAppearing(_ viewController: UIViewController) { self.notificationCenter.addObserver(self, selector: #selector(userDidTakeScreenshot), name: .UIApplicationUserDidTakeScreenshot, object: nil) } public func afterDisappearing(_ viewController: UIViewController) { self.notificationCenter.removeObserver(self) } @objc private func userDidTakeScreenshot() { self.didDetectScreenshot() } private let didDetectScreenshot: () -> Void private let notificationCenter: NotificationCenter }
Behavior can also be tested in isolation, since it is closed by our ViewControllerLifecycleBehaviour
protocol.
Implementation details: here .
Behavior can be used in tasks that depend on the VCL, for example, in analytics.
Suppose you have a button deep in the hierarchy of views, and you only need to make a presentation of the new controller. Usually, the view controller from which the presentation is made is implemented for this. This is the right approach. But sometimes because of this, there is a transitional relationship used by those who are not in the middle, but in the depths of the hierarchy.
As you have probably guessed, there is another solution. You can use a chain of responders to find a controller capable of presenting another view controller.
For example:
public extension UIView { public func viewControllerForPresentation() -> UIViewController? { var next = self.next while let nextResponder = next { if let viewController = next as? UIViewController, viewController.presentedViewController == nil, !viewController.isDetached { return viewController } next = nextResponder.next } return nil } } public extension UIViewController { public var isDetached: Bool { if self.viewIfLoaded?.window?.rootViewController == self return false } return self.parent == nil && self.presentingViewController == nil } }
The Entity – component – ​​system template (entity – component – ​​system) is a great way to integrate analytics into an application. My colleague implemented such a system and it turned out to be very convenient.
Here, the “entity” is UIView, the “component” is part of the tracking data, the “system” is the analytics tracking service.
The idea is to supplement UI views with appropriate tracking data. The analytics tracking service then scans the visible portion of the view hierarchy N times / seconds and records the tracking data that has not yet been recorded.
When using such a system, the developer only needs to add tracking data like screen names and elements:
class EditProfileViewController: UIViewController { override func viewDidLoad() { ... self.trackingScreen = TrackingScreen(screenName:.screenNameMyProfile) } } class SparkUIButton: UIButton { public override func awakeFromNib() { ... self.trackingElement = TrackingElement(elementType: .elementSparkButton) } }
View hierarchy traversal is BFS, which ignores views that are not visible:
let visibleElements = Class.visibleElements(inView: window) for view in visibleElements { guard let trackingElement = view.trackingElement else { continue } self.trackViewElement(view) }
Obviously, this system has performance limitations that cannot be ignored. There are several ways to avoid overloading the main execution thread:
NSNotificationQueue
using NSPostWhenIdle
.I hope I managed to show how you can get along with UIKit, and you have found something useful for your daily work. Or at least got food for thought.
Source: https://habr.com/ru/post/341542/
All Articles