in

Протокольно-ориентированное программирование и модификация компонентов UIKit, имитирующих SwiftUI

Протокольно-ориентированное программирование — один из самых мощных и гибких инструментов для грамотного распределения ответственности в Swift.

В одной статей протокольно-ориентированное программирование использовалось для управления состоянием и построения безопасной последовательности переходов состояний без дополнительных проверок.

В этой статье мы рассмотрим другой способ использования протокольно-ориентированного программирования. В качестве бонуса — мы напишем наше расширение для программирования компонентов пользовательского интерфейса в UIKit, которое имитирует работу с SwiftUI.

Какая задача стоит перед нами? Как мы все знаем, все графические компоненты в UIKit являются прямыми потомками UIView, каждый со своими уникальными свойствами. Протокольно-ориентированный подход поможет нам наделить каждого из наследников их собственными уникальными свойствами, делая возможным комбинирование этих свойств на случай, если мы захотим использовать свойства родительского и свойства дочернего компонентов. В дополнение к протокольно-ориентированному подходу мы также будем использовать Decorator шаблон проектирования, чтобы перенести декларативный синтаксис SwiftUI в UIKit.

Давайте начнем с самого простого. Выберите несколько базовых классов пользовательского интерфейса, с которых мы начнем:

  1. UIView все графические компоненты унаследованы от него
  2. UIControl все UIButtonUISegmentedControl и так далее унаследованы от него
  3. Конечные компоненты пользовательского интерфейса, такие как UILabelUITextField и так далее

Схему наследования можно увидеть ниже:

 

Упрощенная схема наследования

Давайте начнем нашу задачу с создания интерфейса:

protocol Stylable {}

Этот протокол является основой для всех последующих расширений в нашем случае. Поскольку все графические компоненты так или иначе наследуются от UIView, чтобы охватить все компоненты — достаточно расширить UIView этим протоколом:

extension UIView: Stylable {}

Некоторые из наиболее часто используемых свойств для настройки UIView являются cornerRadiusbackgroundColorclipsToBoundscontentMode isHiddenUIView. Более того, эти свойства часто используются не только для настройки самого, но и для его потомков.

Давайте расширим возможности Stylable для всех UIView классов и их потомков:

extension Stylable where Self: UIView {
    @discardableResult
    func cornerRadius(_ value: CGFloat) -> Self {
        self.layer.cornerRadius = value

        return self
    }

    @discardableResult
    func backgroundColor(_ value: UIColor) -> Self {
        self.backgroundColor = value

        return self
    }

    @discardableResult
    func clipsToBounds(_ value: Bool) -> Self {
        self.clipsToBounds = value

        return self
    }

    @discardableResult
    func contentMode(_ value: UIView.ContentMode) -> Self {
        self.contentMode = value

        return self
    }

    @discardableResult
    func isHidden(_ value: Bool) -> Self {
        self.isHidden = value

        return self
    }
}

Давайте проверим, что дало нам это расширение:

let customView = UIView()
    .backgroundColor(.red)
    .clipsToBounds(true)
    .cornerRadius(20)

let customButton = UIButton()
    .backgroundColor(.red)
    .clipsToBounds(true)
    .cornerRadius(20)

let segmentedControl = UISegmentedControl(items: ["One", "Two"])
    .backgroundColor(.red)
    .clipsToBounds(true)
    .cornerRadius(20)

let scrollView = UIScrollView()
    .backgroundColor(.red)
    .clipsToBounds(true)
    .cornerRadius(20)

let textField = UITextField()
    .backgroundColor(.red)
    .clipsToBounds(true)
    .cornerRadius(20)

Как мы можем видеть, благодаря расширению мы можем декларативно изменять свойства не только для UIView, но и для его потомков.

Давайте перейдем к настройке UIControl. Для всех его потомков одной из наиболее часто используемых настраиваемых функций является tap, properties — isEnabledtintColorisUserInteractionEnabled.

Давайте расширим возможности Stylable для всех UIControl классов и их потомков:

extension Stylable where Self: UIControl {
    @discardableResult
    func action(_ value: (() -> Void)?, event: UIControl.Event = .touchUpInside) -> Self {
        let identifier = UIAction.Identifier(String(describing: event.rawValue))
        let action = UIAction(identifier: identifier) { _ in
            value?()
        }
        
        self.removeAction(identifiedBy: identifier, for: event)
        self.addAction(action, for: event)
        
        return self
    }
    
    @discardableResult
    func secondAction(_ value: ((Bool) -> Void)?, controlEvent: UIControl.Event = .valueChanged) -> Self {
        let identifier = UIAction.Identifier(String(describing: controlEvent.rawValue))
        let action = UIAction(identifier: identifier) { item in
            guard let control = item.sender as? UIControl else {
                return
            }
            value?(!control.isTracking)
        }
        
        self.removeAction(identifiedBy: identifier, for: controlEvent)
        self.addAction(action, for: controlEvent)
        
        return self
    }
    
    @discardableResult
    func isEnabled(_ value: Bool) -> Self {
        self.isEnabled = value
        
        return self
    }
    
    @discardableResult
    func isUserInteractionEnabled(_ value: Bool) -> Self {
        self.isUserInteractionEnabled = value
        
        return self
    }
    
    @discardableResult
    func tintColor(_ value: UIColor) -> Self {
        self.tintColor = value
        
        return self
    }
}

После Stylable расширения для UIControl стала доступна дополнительная опция настройки для всех его потомков:

let customButton = UIButton()
    .backgroundColor(.red)
    .clipsToBounds(true)
    .cornerRadius(20)
    .tintColor(.red)
    .action {
       print(#function)
    }
    .isEnabled(true)
    .isUserInteractionEnabled(true)

    
let segmentedControl = UISegmentedControl(items: ["One", "Two"])
    .backgroundColor(.red)
    .clipsToBounds(true)
    .cornerRadius(20)
    .tintColor(.red)
    .action {
       print(#function)
    }
    .isEnabled(true)
    .isUserInteractionEnabled(true)

Стоит отметить, что при вызове метода action с использованием метода класса вам следует лениво инициализировать этот компонент, чтобы убедиться, что класс (self) инициализирован до инициализации компонента.

lazy var customButton = UIButton()
    .backgroundColor(.red)
    .clipsToBounds(true)
    .cornerRadius(20)
    .tintColor(.red)
    .action { [weak self] in
         self?.actionTest()
    }
    .isEnabled(true)
    .isUserInteractionEnabled(true)
    
private func actionTest() {
    print(#function)
}

Давайте также расширим UITextField некоторые из наиболее популярных пользовательских свойств:

extension Stylable where Self: UITextField {
    @discardableResult
    func text(_ value: String?) -> Self {
        self.text = value

        return self
    }

    @discardableResult
    func font(_ value: UIFont) -> Self {
        self.font = value

        return self
    }

    @discardableResult
    func textAlignment(_ value: NSTextAlignment) -> Self {
        self.textAlignment = value

        return self
    }

    @discardableResult
    func textColor(_ value: UIColor) -> Self {
        self.textColor = value

        return self
    }

    @discardableResult
    func capitalizationType(_ value: UITextAutocapitalizationType) -> Self {
        self.autocapitalizationType = value

        return self
    }

    @discardableResult
    func keyboardType(_ value: UIKeyboardType) -> Self {
        self.keyboardType = value

        return self
    }

    @discardableResult
    func isSecureTextEntry(_ value: Bool) -> Self {
        self.isSecureTextEntry = value

        return self
    }

    @discardableResult
    func autocorrectionType(_ value: UITextAutocorrectionType) -> Self {
        self.autocorrectionType = value

        return self
    }

    @discardableResult
    func contentType(_ value: UITextContentType?) -> Self {
        self.textContentType = value

        return self
    }

    @discardableResult
    func clearButtonMode(_ value: UITextField.ViewMode) -> Self {
        self.clearButtonMode = value

        return self
    }

    @discardableResult
    func placeholder(_ value: String?) -> Self {
        self.placeholder = value

        return self
    }

    @discardableResult
    func returnKeyType(_ value: UIReturnKeyType) -> Self {
        self.returnKeyType = value

        return self
    }
    
    @discardableResult
    func delegate(_ value: UITextFieldDelegate) -> Self {
        self.delegate = value

        return self
    }

    @discardableResult
    func atributedPlaceholder(
        _ value: String,
        textColor: UIColor,
        textFont: UIFont
    ) -> Self {
        let attributedString = NSAttributedString(
            string: value,
            attributes: [
                NSAttributedString.Key.foregroundColor: textColor,
                NSAttributedString.Key.font: textFont
            ]
        )

        self.attributedPlaceholder = attributedString

        return self
    }
}

Благодаря этому расширению настройка UITextField стала еще проще. Для настройки графического интерфейса доступны методы его родителей, а также собственные методы:

lazy var textField = UITextField()
     .placeholder("Placeholder")
     .textColor(.red)
     .text("Text")
     .contentType(.URL)
     .autocorrectionType(.yes)
     .font(.boldSystemFont(ofSize: 12))
     .delegate(self) 

Стоит отметить, что, подобно захвату self в методе UIControl’s action, назначение делегата также требует textField ленивой инициализации.

По аналогии, остальные графические компоненты дополнены свойствами, которые будут использоваться для настройки.

В качестве бонуса для моих читателей я собрал некоторые из наиболее востребованных свойств в этом репозитории. Вам нужно скопировать файлы в свой проект; они готовы к использованию.

What do you think?

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

GIPHY App Key not set. Please check settings