Protocol-Oriented Programming and Modifying UIKit Components Mimicking SwiftUI

Written by bugorbn | Published 2023/12/20
Tech Story Tags: programming | ios-development | ios-app-development | ios-apps | swift | swiftui | uikit | protocol-oriented-programming

TLDRvia the TL;DR App

Protocol-oriented programming is one of the most powerful and flexible tools for competent composition and distribution of responsibility in Swift. In one of the previous articles, protocol-oriented programming was used to manage the state and build a safe sequence of state transitions without additional checks. If you have not read the previous article, then it is recommended to read as material that will show one of the ways to use this wonderful approach.

In this article, we’ll explore another way to use protocol-oriented programming. As a bonus — we’ll write our extension for programming UI components in UIKit that mimics the SwiftUI experience.

What is the task before us? As we all know, all graphical components in UIKit are direct descendants of UIView, each with its own unique properties. A protocol-oriented approach will help us to endow each of the heirs with their own unique properties while making it possible to combine these properties in case we want to use the properties of the parent and the properties of the child. In addition to the protocol-oriented approach, we will also use the Decorator design pattern to bring the SwiftUI declarative syntax experience to UIKit.

Let’s start with the simplest. Select several basic UI classes with which we will start:

  1. UIView — all graphical components are inherited from it

  2. UIControl — all UIButton, UISegmentedControl and so on are inherited from it

  3. Final UI components like UILabel, UITextField, and so on

The inheritance diagram can be seen below:

Let’s start our task by creating an interface:

protocol Stylable {}

This protocol is the basis for all subsequent extensions for our case. Since all graphical components somehow inherit from UIView to cover all components — it is enough to extend UIView with this protocol:

extension UIView: Stylable {}

Some of the most used properties for customizing a UIView are cornerRadius, backgroundColor, clipsToBounds, contentMode, isHidden. Moreover, these properties are often used not only to configure the UIView, but also for its descendants.

Let’s extend the possibilities of Stylable for all UIView classes and their descendants:

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’s check what this extension gave us:

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)

As we can see, thanks to the extension, we can declaratively change properties not only for UIView but also for its descendants.

Let’s move on to configuring the UIControl. For all of its descendants, one of the most used customizable things is tap, properties — isEnabled, tintColor, isUserInteractionEnabled.

Let’s extend the possibilities of Stylable for all UIControl classes and their descendants:

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
    }
}

After the Stylable extension for UIControl, an additional customization option became available for all its descendants:

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)

It is worth noting that when calling the action method using a class method, you should initialize this component lazily to ensure that the class (self) is initialized before the component is initialized.

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)
}

Let’s also extend UITextField with some of the most popular custom properties:

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
    }
}

Thanks to this extension, customizing UITextField has become even easier. To customize the GUI, the methods of its parents are available, as well as its own methods:

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

It’s worth noting that, similar to capturing self in the UIControl’s action method, assigning a delegate, also requires textField to be lazy-initialized.

By analogy, the rest of the graphical components are expanded with properties that will be used for customization.

As a bonus for my readers, I’ve compiled some of the most requested properties in this repository. You need to copy the files to your project; they are ready to use.

Don’t hesitate to contact me on Twitter if you have any questions. Also, you can always buy me a coffee.


Also published here.


Written by bugorbn | Senior iOS Developer | Founder of Flow: Trip & Travel Tracker | #ObjC | #Swift | #UIKit | #SwiftUI
Published by HackerNoon on 2023/12/20