Controller, easy! We take out the code in UIView

Do you have a big UIViewController? For many, yes. On the one hand, it works with data, on the other - with the interface.

The tasks of separating logic from an interface are described in hundreds of articles about the architecture: MVP, MVVM, VIPER. They solve the problem of data flow, but do not answer the question of how to work with the interface: in one place there is the creation of elements, layout, configuration, input processing and animation.

Let's separate the view from the controller and see what loadView () will help us with.



The iOS app interface is a hierarchy UIView. Tasks of each view: to create elements, set up, put into place, animate. This can be seen from the methods that are in the class. UIView: addSubview(), drawRect(), layoutSubviews().

If you look at the methods of the class UIViewController, you can see that it manages the management view:loads, reacts to the loading of screens and user actions, shows new screens. Often the code that needs to be in UIView, we write in subclasses UIViewController, which makes it too big. Separate it.

loadView ()


The life cycle UIViewControllerbegins with loadView(). A simplified implementation looks like this:

// CustomViewController.swift
func loadView() {
   self.view = UIView()
}

We can override the method and specify our class.

super.loadView()no need to call!

// CustomViewController.swift
override func loadView() {
   self.view = CustomView()
}

Implementing CustomView.swift
// CustomView.swift
final class CustomView {
   let square: UIView = UIView()
   init() {
      super.init()
      square.backgroundColor = .red
      addSubview(square)
   }
}


The controller CustomView,will add it to the hierarchy, set it up .frame. The property .viewwill be the class we need:

// CustomViewController.swift
print(view) // CustomView

But while the compiler does not know about the class and believes that there is normal UIView. Let's fix this with a type conversion:

// CustomViewController.swift
func view() -> CustomView {
   returnself.view as! CustomView
}

Now you can see the variables CustomView:

// CustomViewController.swift
func viewDidLoad() {
   super.viewDidLoad()
   view().square // Работает
}

Simplify using associatedtype
Руслан Кавецкий предложил убрать дублирование кода с помощью расширения протокола:

protocol ViewSpecificController {
    associatedtype RootView: UIView
}
extension ViewSpecificController where Self: UIViewController {
    func view() -> RootView {
        returnself.view as! RootView
    }
}

Для каждого нового контроллера нужно только указать протокол и подкласс для его UIView через typealias:

// CustomViewController.swift
final class CustomViewController: UIViewController, ViewSpecificController {
   typealias RootView = CustomView
   func viewDidLoad() {
      super.viewDidLoad()
      view().square // Работает
   }
}

Code in a subclass of UIView


Creating and configuring controls


Fonts, colors, constraints and hierarchy can be customized directly in the CustomView designer:

// CustomView.swift
init() {
   super.init()
   backgroundColor = .lightGray
   addSubview(square)
}

layoutSubviews ()


The best place for a manual layout is the method layoutSubviews(). It is called every time the size changes view, so you can rely on the size boundsfor correct calculations:

// CustomView.swift
override func layoutSubviews() {
   super.layoutSubviews()
   square.frame = CGRect(x: 0, y: 0: width: 200, height: 200)
   square.center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
}

Private controls, public properties


If there is time, then I make the propertycontrols private, but I manage them through public variables or “in the knowledge” functions. Easier by example:

// CustomView.swift
private let square = UIView()
var squarePositionIsValid: Bool {
   didSet {
     square.backgroundColor =  squarePositionIsValid? .green : .red
   }
}
func moveSquare(to newCenter: CGPoint) {
    square.center = newCenter
}

Encapsulation advantage: internal logic is hidden behind the interface. For example, the validity of an object may be indicated by the color of a region, not a square, but the controller will not know anything about it.

What remains in viewDidLoad ()?


If you use Interface Builder, it is often viewDidLoad()empty. If viewyou create in code, you need to bind their actions through the target-action pattern, add UIGestureRecognizeror link delegates.

Customizable via Interface Builder


Subclass for viewcan be configured through Interface Builder (hereinafter IB).

You need to select an object view(not a controller) and set its class. Writing your own is loadView()not necessary, the controller will do it himself. But I UIViewstill have to bring the type .



IBOutlet in UIView


If you select the control inside view, the Assistant Editor will recognize the class UIViewand offer it as the second file in Automatic mode. So you can transfer IBOutletto view.



If not working
Открыть класс CustomView вручную, написать IBOutlet. Теперь можно потащить за маркер и навести на элемент в IB.



If you create an interface in the code, then all objects are available after init(), but when working with IB, access to IBOutletappears only after the interface is loaded from UIStoryboardin the method awakeFromNib():

// CustomView.swift
func awakeFromNib() {
   super.awakeFromNib() 
   square.layer.cornerRadius = 8
}

IBAction in UIViewController


For my taste, the controller should leave all user actions. From standard:

  • target-action from controls
  • delegate implementation in UIViewController
  • block implementation
  • reaction to Notification

At the same time UIViewControllercontrols only the interface. Everything related to business logic should be taken out of the controller, but this is a choice: MVP, VIPER, etc.

Objective-c


In Objective-C, you can fully replace the type UIView. To do this, you need to declare a property with the required class, override setterit getter, specifying the class:

// CustomViewController.m@interfaceCustomViewController@property (nonatomic) CustomView *customView;
@end@implementation
- (void)setView:(CustomView *)view{
   [super setView:view];
}
- (CustomView *)view {
   return (CustomView *)super.view;
}
@end

the end


In the GitHub example, you can look at the division of classes for a simple task: the color of the square depends on its position (in the green area it is green, outside it is red).

The more complex the screen, the better the effect: the controller decreases, the code is transferred to its places. The code is simply transferred to view, but encapsulation makes it easy to interact and read the code. Sometimes viewyou can reuse with another controller. For example, different controllers for the iPhone and iPad react differently to the appearance of the keyboard, but this does not change the code view.

I used this code in different projects and with different people, each time the team welcomed the simplification and picked up the practice. I hope you enjoy it too. All lungs UIViewController!

Also popular now: