Withable

📐 Declarative UIKit in 10 lines of code.

See corresponding article at Declarative UIKit with 10 lines of code A simple extension instead of libraries for more.

How to use

With a single extension on AnyObject you can do things like this.

class ContentViewController: UIViewController {
    
    ...
    
    lazy var titleLabel = UILabel()
        .with {
            $0.text = viewModel.title
            $0.textColor = .label
            $0.font = .preferredFont(forTextStyle: .largeTitle)
        }
    
    ...
}

With any kind of object, really.

lazy var submitButton = UIButton()
    .with {
        $0.setTitle("Submit", for: .normal)
        $0.addTarget(self, action: #selector(didTapSubmitButton), for: .touchUpInside)
    }
present(
    DetailViewController()
        .with {
            $0.modalTransitionStyle = .crossDissolve
            $0.modalPresentationStyle = .overCurrentContext
        },
    animated: true
)
present(
    UIAlertController(title: title, message: message, preferredStyle: .alert)
        .with {
            $0.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
        },
    animated: true
)
let today = DateFormatter()
    .with {
        $0.dateStyle = .medium
        $0.locale = Locale(identifier: "en_US")
    }
    .string(from: Date())
lazy var displayLink = CADisplayLink(target: self, selector: #selector(update))
    .with {
        $0.isPaused = true
        $0.preferredFramesPerSecond = 120
        $0.add(to: RunLoop.main, forMode: .common)
    }

Even value types as well (after conforming to Withable).

extension PersonNameComponents: Withable { }

let name = PersonNameComponents()
    .with {
        $0.givenName = "Geri"
        $0.familyName = "Borbás"
    }

Not to mention 3D stuff (ARKit, RealityKit, SceneKit).

view.scene.addAnchor(
    AnchorEntity(plane: .horizontal)
        .with {
            $0.addChild(
                ModelEntity(
                    mesh: MeshResource.generateBox(size: 0.3),
                    materials: [
                        SimpleMaterial(color: .green, isMetallic: true)
                    ]
                )
            )
        }
)

How it works

It is implemented in this with method. 💎

public extension Withable {
    
    func with(_ closure: (Self) -> Void) -> Self {
        closure(self)
        return self
    }
}

The method implements pretty classic patterns. You can think of it as something between an unspecialized/parametric builder, or a decorator with customizable/pluggable decorating behaviour. See Withable.swift for all details (generics, value types).

UIKit benefits

The package contains a couple of convinient extensions of UIKit classes what I use (probably will be moved to their own package as they grow). I left them here intentionally as they may exemplify how you can create your own extensions tailored for your codebases' needs.

For example, you may create a convenient text decorator for UILabel.

extension UILabel {
    
    func with(text: String?) -> Self {
        with {
            $0.text = text
        }
    }
}

Furthermore, you can condense your styles to simple extensions like this.

extension UILabel {
    
    var withTitleStyle: Self {
        with {
            $0.textColor = .label
            $0.font = .preferredFont(forTextStyle: .largeTitle)
        }
    }
    
    var withPropertyStyle: Self {
        with {
            $0.textColor = .systemBackground
            $0.font = .preferredFont(forTextStyle: .headline)
            $0.setContentCompressionResistancePriority(.required, for: .vertical)
        }
    }
    
    var withPropertyValueStyle: Self {
        with {
            $0.textColor = .systemGray
            $0.font = .preferredFont(forTextStyle: .body)
        }
    }
    
    var withParagraphStyle: Self {
        with {
            $0.textColor = .label
            $0.numberOfLines = 0
            $0.font = .preferredFont(forTextStyle: .footnote)
        }
    }
}

With extensions like that, you can clean up view controllers.

class ContentViewController: UIViewController {
    
    let viewModel = Planets().earth
    
    private lazy var body = UIStackView().vertical(spacing: 10).views(
        UILabel()
            .with(text: viewModel.title)
            .withTitleStyle,
        UIStackView().vertical(spacing: 5).views(
            UIStackView().horizontal(spacing: 5).views(
                UILabel()
                    .with(text: "size")
                    .withPropertyStyle
                    .withBox,
                UILabel()
                    .with(text: viewModel.properties.size)
                    .withPropertyValueStyle,
                UIView.spacer
            ),
            UIStackView().horizontal(spacing: 5).views(
                UILabel()
                    .with(text: "distance")
                    .withPropertyStyle
                    .withBox,
                UILabel()
                    .with(text: viewModel.properties.distance)
                    .withPropertyValueStyle,
                UIView.spacer
            ),
            UIStackView().horizontal(spacing: 5).views(
                UILabel()
                    .with(text: "mass")
                    .withPropertyStyle
                    .withBox,
                UILabel()
                    .with(text: viewModel.properties.mass)
                    .withPropertyValueStyle,
                UIView.spacer
            )
        ),
        UIImageView()
            .with(image: UIImage(named: viewModel.imageAssetName)),
        UILabel()
            .with(text: viewModel.paragraphs.first)
            .withParagraphStyle,
        UILabel()
            .with(text: viewModel.paragraphs.last)
            .withParagraphStyle,
        UIView.spacer
    )
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(body)
        view.backgroundColor = .systemBackground
        body.pin(
            to: view.safeAreaLayoutGuide,
            insets: UIEdgeInsets(top: 30, left: 30, bottom: 30, right: 30)
        )
    }
}

I recommend to read the corresponding article at Declarative UIKit with 10 lines of code A simple extension instead of libraries to read more about the background and more examples.

Used by Apple

Later on, I found out that on occasions Apple uses the very same pattern to enable decorating objects inline. These decorator functions are even uses the same with naming convention.

These examples below are in vanilla UIKit. 🍦

let arrow = UIImage(named: "Arrow").withTintColor(.blue)
let mail = UIImage(systemName: "envelope").withRenderingMode(.alwaysTemplate)
let color = UIColor.label.withAlphaComponent(0.5)

Stored properties in extensions

In addition, the package contains an NSObject extension that helps creating stored properties in extensions. I ended up including it because I found extending UIKit classes with stored properties is a pretty common usecase. See NSObject+Extensions.swift and UIButton+Extensions.swift for more.

You can do things like this.

extension UITextField {
    
    var nextTextField: UITextField? {
        get {
            associatedObject(for: "nextTextField") as? UITextField
        }
        set {
            set(associatedObject: newValue, for: "nextTextField")
        }
    }
}

Declare constraints inline

One more secret weapon is the UIView.onMoveToSuperview extension, which is simply a closure called (once) when the view gets added to a superview. With that, you can declare the constraints in advance using this closure at initialization time, then they are added/activated later on at runtime by the time when the view has a superview. See Keyboard Avoidance repository for usage examples.

License

Licensed under the MIT License.

Description

  • Swift Tools 5.5.0
View More Packages from this Author

Dependencies

  • None
Last updated: Sat Oct 19 2024 09:42:49 GMT-0900 (Hawaii-Aleutian Daylight Time)