Introducing ViewKit: A SwiftUI-Inspired Approach to UIKit

Lucas Werner Kuipers
6 min readMay 15, 2023

--

ViewKit: Declarative, SwiftUI-based and functional swift package for UIKit view building

Building functional and declarative UIKit with ViewKit

As an iOS developer, I’ve often found myself navigating the balance between the enticing simplicity of SwiftUI’s syntax and the comprehensive functionality of UIKit. SwiftUI is certainly a step forward in terms of readability and ease of use, but UIKit remains critical for many applications due to its extensive capabilities and wide compatibility (and, of course, a lot of existing legacy code).

I’ve created ViewKit with the hope of bringing these two worlds closer together. ViewKit is a humble attempt to bring a SwiftUI-like, declarative syntax to UIKit (and other techniques), aiming to make UIKit development a bit more streamlined and approachable through the use of simple helpers, extensions and so on.

In this article, I’ll share the journey of creating ViewKit, how it can be used, and the problems it aims to solve. I kindly invite you to give it a try and explore ViewKit, a testament to the ongoing evolution of iOS development.

Motivation Behind ViewKit

My journey with ViewKit began from a place of necessity and curiosity. As an iOS developer, I’ve spent countless hours working with UIKit, appreciating its extensive capabilities and wide compatibility. UIKit has been the backbone of iOS development for over a decade, and despite the introduction of newer frameworks, it remains relevant and indispensable in many contexts.

However, with the advent of SwiftUI, I was captivated by its simplicity and declarative style. SwiftUI represented a shift in how we thought about and interacted with UI components. Instead of managing the UI state and dealing with life cycle events as we do in UIKit, SwiftUI allowed developers to declare what the UI should do, and the framework took care of the rest. This was a significant leap forward, making UI development more intuitive and less error-prone.

But, as much as I loved SwiftUI’s declarative syntax, I realized it wasn’t always feasible to use, especially when developing apps that needed to support older iOS versions, or when certain UIKit-specific functionalities were required.

This is where ViewKit was born. I asked myself, “What if we could take the declarative syntax of SwiftUI and apply it to UIKit?” The goal wasn’t to replace UIKit or SwiftUI, but to provide a bridge between them, to bring a touch of SwiftUI’s elegance to the UIKit world that we know and trust.

There were indeed existing solutions that attempted to provide a similar functionality, but they either lacked certain features or didn’t offer the level of simplicity and flexibility I was aiming for. Anyways, this was my humble attempt (and I'm sure there are tons of limitations with it as well 😆).

Thus, I embarked on the journey to create ViewKit, a modest endeavor to make UIKit development a bit more friendly and approachable while maintaining compatibility with existing UIKit capabilities. I tried to keep things as simple as possible and add as few custom data-type dependencies as I could.

Building UIKit programmatically usually requires a lot of repetition and boilerplate code (and it introduces way too many opportunities for different coding and naming styles, which makes switching projects a bigger pain than it should).

Getting started

Ok, let's get to some code examples, shall we?

Let's say we want a simple view that displays a label and button, with the group centralized vertically and the button having some action.

Here's a way you could implement it programmatically in UIKit (it is not the only way but a common one):

import UIKit

final class UIKitSampleView: UIView {
private let containerView = UIView()

private let titleLabel: UILabel = {
let label = UILabel()
label.text = "Hello, world!"
label.font = .preferredFont(forTextStyle: .largeTitle)
return label
}()

private let actionButton: UIButton = {
let button = UIButton()
button.setTitle("Tap me!", for: .normal)
button.titleLabel?.font = .preferredFont(forTextStyle: .title1)
button.setTitleColor(.secondaryLabel, for: .highlighted)
button.backgroundColor(.systemBlue)
button.cornerRadius(20)

return button
}()

init() {
super.init(frame: .zero)
setupViewHierarchy()
setupViewConstraints()
setupActions()
}

private func setupViewHierarchy() {
add(containerView)
add(titleLabel)
add(actionButton)
}

private func setupViewConstraints() {
containerView.translatesAutoresizingMaskIntoConstraints = false
titleLabel.translatesAutoresizingMaskIntoConstraints = false
actionButton.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
containerView.centerXAnchor.constraint(equalTo: centerXAnchor),
containerView.centerYAnchor.constraint(equalTo: centerYAnchor),

titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor),
titleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
titleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),

actionButton.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 40),
actionButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
actionButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
actionButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
])
}

private func setupActions() {
actionButton.addTarget(self, action: #selector(didTapActionButton), for: .touchUpInside)
}

@objc private func didTapActionButton() {
print("Tapped")
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Oof. Now that is some eye strainer… a lot of boilerplate just for some very simple view. Of course you could achieve the same with less lines of code (not using methods to organize the functionality) but then you are just trading clean for compact.

In comparison, here's a way you could implement it with ViewKit:

import ViewKit

final class ViewKitSampleView: ProgrammaticView {

var body: UIView {
VerticalStack(spacing: 40) {
UILabel("Hello, world!")
.font(.preferredFont(forTextStyle: .largeTitle))

UIButton("Tap me!") { _ in print("Tapped") }
.font(.preferredFont(forTextStyle: .title1))
.titleColor(.secondaryLabel, for: .highlighted)
.backgroundColor(.systemBlue)
.cornerRadius(20)
.maxWidth()
}
}
}

Yes, it is as simple as that.

You implement ProgrammaticView and define your layout with stacks, just like we do in SwiftUI, but using the already existing UIKit types and their properties (but now the customization of these properties is chainable with the dot notation).

If you look closely, you'll see ViewKit has nothing too complex. It is a bunch of extension methods that use the Fluent Interface pattern (basically returning the same object with the applied changes, thus allowing the method chaining), some resultBuilders and a few protocols.

For instance, here’s how .text is defined in ViewKit:

@discardableResult
func text(_ text: String?) -> Self {
self.text = text
return self
}

And here's how VerticalStack is defined:

public class VerticalStack: UIStackView {
public convenience init(
spacing: CGFloat = 10,
distribution: Distribution = .fill,
alignment: Alignment = .center,
@StackViewBuilder _ builder: () -> [UIView]
) {
self.init(arrangedSubviews: builder())
self.axis = .vertical
self.spacing = spacing
self.distribution = distribution
self.alignment = alignment
}
}

Where StackViewBuilder :

@resultBuilder
public struct StackViewBuilder {
public static func buildBlock(_ children: UIView...) -> [UIView] { children }
}

ProgrammaticView is:

public protocol ProgrammaticView {
@ProgrammaticViewBuilder var body: UIView { get }
}

With the builder:

@resultBuilder
public struct ProgrammaticViewBuilder {
public static func buildBlock(_ child: UIView) -> UIView { child }
public static func buildBlock(_ children: UIView...) -> UIView {
let container = UIView()

children.forEach { child in
container.addSubview(child)

child.prepareForConstraints()

container.add(child)

container.constrain([
child.topAnchor.constraint(greaterThanOrEqualTo: container.topAnchor),
child.leftAnchor.constraint(greaterThanOrEqualTo: container.leftAnchor),
child.bottomAnchor.constraint(lessThanOrEqualTo: container.bottomAnchor),
child.rightAnchor.constraint(lessThanOrEqualTo: container.rightAnchor),
child.centerXAnchor.constraint(equalTo: container.centerXAnchor),
child.centerYAnchor.constraint(equalTo: container.centerYAnchor),
])
}

return container
}
}

You use this ProgrammaticView with a custom ViewController (might change the name to ProgrammaticViewController) that takes a ProgrammaticView in its constructor and set it as it's view with a ContainerView (that is UIKit's UIView).

Basically:

open class ViewController: UIViewController {
let contentView: UIView

public init(with view: ProgrammaticView) {
self.contentView = ContainerView(for: view)
super.init(nibName: nil, bundle: nil)
}

public override func loadView() {
super.loadView()
self.view = contentView
delegate?.loadView()
}

Where ContainerView is simply:

open class ContainerView: UIView {

public init(for view: ProgrammaticView) {
super.init(frame: .zero)
let body = view.body
body.prepareForConstraints()
add(body)
constrain([
body.topAnchor.constraint(greaterThanOrEqualTo: topAnchor),
body.leftAnchor.constraint(greaterThanOrEqualTo: leftAnchor),
body.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor),
body.rightAnchor.constraint(lessThanOrEqualTo: rightAnchor),
body.centerXAnchor.constraint(equalTo: centerXAnchor),
body.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
}

Nothing too fancy, which I think is for the best. (By the way, if you find a way to simplify ViewKit even more, please do so and open a pull request).

Oh, you can also preview your ViewKit views in the Xcode canvas (if you can import SwiftUI), by embedding your ProgrammaticView in a PreviewContainer like so:

import SwiftUI

struct ViewKitSampleView_Previews: PreviewProvider {
static var previews: some View {
PreviewContainer {
ViewKitSampleView()
}
}
}

Conclusion

If you find ViewKit interesting and wish to give it a try, please do so.

Also, feel free to to share and contribute. All support is appreciated.

If you want to discuss how you can implement ViewKit in your projects or wish to talk about iOS, technology or anything else, you can DM on linkedin

Here is the link for the source code / SPM package.

Cheers!

--

--