How to make friends with UIKit
Hello, Habr! My name is Bogdan, at Badoo I work in the mobile team as an iOS developer. We rarely tell 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 battling UIKit. Someone comes up with complex ways to “bury” UIKit internals under layers of abstractions in their fictitious architectures, other teams rewrite it, amusing their ego, but leaving behind a wild amount of code that needs to be supported.
Make UIKit Work For You
I am 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 templates. Therefore, I believe that instead of fighting UIKit, we should accept it and use it as widely as possible.
Choosing an architecture suitable for UIKit
Any problem can be solved by adding another level of abstraction. Therefore, many choose VIPER - there are many levels / entities that can be used in work. Writing an application in VIPER is not difficult - it’s much more difficult to write an MVC application with the same advantages that supports less template 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, such a luxury is not available to us - we have to work with the existing code base.
Let's do a thought experiment.
You join 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 a simple 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 based on 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 of the controllers, and attempts to reuse end with copy-paste, which is why view controllers increase in size.
Obviously, you need to start splitting the code. But how to do it without the hassle? If you pull the code that extracts the data into a separate object, the controller will only display information on the screen. So let's do it:
Now everything looks very similar to MVVM, so we will use its terminology. So we have a view and a presentation model. We can easily test this model. Let's now transfer repetitive tasks to services, such as working with a network and storing data.
As a result:
- You can reuse your code.
- Get a source of truth that is not tied to a user interface level.
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 delete the controller from 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 MVP, then if for some reason the controller is unloaded, the connection between it and the presenter will be broken. And if you think that it is difficult to accidentally unload the wrong controller, then carefully read the description UIViewController.dismiss(animated:completion:)
.
So I believe that the safest thing is to recognize the view controller as king, and therefore, objects that are not related to the UI, divided into two categories:
- Objects with a life cycle equal to the cycle of UI elements (for example, a presentation model).
- Objects with a life cycle equal to the application cycle (for example, a service).
Using the view controller life cycle
Why is it so tempting to put all the code in 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 life cycle of the presentation in the model or presenter, then you will have to pass it manually, and this is normal, but you will have to write more code.
But there is another solution. Because view controllers are capable of working with each other, Sorush Hanlow suggested using this to distribute work between small view controllers .
You can go even further and apply a universal way to connect the view controller to the life cycle 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. If you submit this task to VCLBehaviour, then everything becomes easier than ever:
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, as it is covered by our protocol ViewControllerLifecycleBehaviour
.
Implementation Details: here .
Behavior can be used in VCL-specific tasks, such as analytics.
Using the responder chain
Suppose you have a button deep in the hierarchy of views, and you just need to make a presentation of the new controller. Usually, for this, a view controller is implemented from which the presentation is made. This is the right approach. But sometimes because of this, a transitional dependence appears, used by those who are not in the middle, but in the depths of the hierarchy.
As you probably already guessed, there is another way to solve it. To find a controller that can present another view controller, you can use a chain of responders.
For instance:
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
}
}
Using the view hierarchy
The Entity – component – system template is an excellent way to incorporate analytics into an application. My colleague implemented such a system and it turned out to be very convenient.
Здесь «сущность» – это UIView, «компонент» – часть данных отслеживания, «система» – сервис отслеживания аналитики.
Идея в том, чтобы дополнить UI-представления соответствующими данными отслеживания. Затем сервис отслеживания аналитики сканирует N раз/ секунд видимую часть иерархии представлений и записывает данные отслеживания, которые ещё не были записаны.
При использовании такой системы от разработчика требуется только добавить данные отслеживания вроде имён экранов и элементов:
class EditProfileViewController: UIViewController {
override func viewDidLoad() {
...
self.trackingScreen =
TrackingScreen(screenName:.screenNameMyProfile)
}
}
class SparkUIButton: UIButton {
public override func awakeFromNib() {
...
self.trackingElement =
TrackingElement(elementType: .elementSparkButton)
}
}
Обход иерархии представлений – это BFS, при котором игнорируются представления, которые не видны:
let visibleElements = Class.visibleElements(inView: window)
for view in visibleElements {
guard let trackingElement = view.trackingElement else {
continue
}
self.trackViewElement(view)
}
Очевидно, что у этой системы есть ограничения производительности, которые нельзя игнорировать. Избежать перегрузки основного потока выполнения можно разными способами:
- Не слишком часто сканировать иерархию представлений.
- Do not scan the hierarchy of views when scrolling (use the more appropriate run loop mode).
- Scan the hierarchy only when the notification is published in
NSNotificationQueue
usingNSPostWhenIdle
.
PS
I hope I was able to show how you can get along with UIKit, and you find something useful for your daily work. Or at least got food for thought.