I’ve become quite the functional programming advocate, but one difficulty I often face is the prevalence of Object Oriented code throughout the iOS platform, and perhaps most obviously in UIKit (I’m not going to talk about SwiftUI here – it’s still too new for production in my opinion, though of course I’m looking forward to the latest version, and UIKit will be around for a long time).
So, to help alleviate this problem, I wrote a lightweight library called fuikit that allows me to leverage the power of FP with UIKit objects and classes. It works by subclassing common UIKit classes and creating a bunch of functions as properties. So for instance, instead of implementing viewDidLoad
in every subclass of UIViewController
, you could just subclass FUIViewController
and assign a function to its property called onViewDidLoad
:
class MyViewController: FUIViewController {}
func myViewController() -> MyViewController {
let vc = <create MyViewController>
vc.onViewDidLoad = setupMyViewController(_:)
}
fuikit offers a bunch of these sorts of subclasses of common UIKit classes, including UIViewController
and UITableViewController
, as well as functional style implementations of common protocols such as UITableViewDataSource
and UITableViewDelegate
, among many others.
So why is this cool? Flexibility! Let’s look at an example.
Suppose you have an array of User
s that you’d like to display in a table view. Normally this would entail implementing UITableViewDataSource
with a couple functions. But using fuikit, you could just do this:
func usersDataSource(users: [User]) -> UITableViewDataSource {
let dataSource = FPUITableViewDataSource()
dataSource.onNumberOfRowsInSection = { _ in users.count }
dataSource.onCellForRowAt = { tableView, indexPath in
if let cell = tableView.dequeueReusableCell(withIdentifier: "userCell") as? MyTableViewCell {
configure(cell, with: users[indexPath.row])
return cell
}
return UITableViewCell()
}
return dataSource
}
func configure(_ cell: MyTableViewCell, with user: User) { ... }
In words, this creates an instance of FPUITableViewDataSource
, assigns a closure to onNumberOfRowsInSection
which just returns the number of users you’re displaying, and then assigns a closure to onCellForRowAt
which dequeues a cell, configures it with a function defined elsewhere, and returns it. This allows you to use composition to break down each step of configuring the cell into component parts, and use/reuse them freely. For instance, suppose you had another model called Contact, which has a user: User? property, and you wanted to display the list of contact users in a table view. Then all you need to do is this:
let contactUsers = contacts.compactMap { $0.user }
let contactUsersDataSource = usersDataSource(users: contactUsers)
Rad! But what if you want to display the contact itself in a different way? We can do the same thing as before:
func contactsDataSource(_ contacts: [Contact]) -> UITableViewDataSource {
let dataSource = FPUITableViewDataSource()
dataSource.onNumberOfRowsInSection = { _ in contacts.count }
dataSource.onCellForRowAt = { tableView, indexPath in
if let cell = tableView.dequeueReusableCell(withIdentifier: "contactCell") as? MyTableViewCell {
configure(cell, with: contacts[indexPath.row])
return cell
}
return UITableViewCell()
}
return dataSource
}
func configure(_ cell: MyTableViewCell, with contact: Contact) { ... }
which we can use similarly as well:
let contactsDataSource = contactsDataSource(contacts)
If you look closely, you’ll notice that only 3 things are different between the contacts function and the users function: a) the cell identifier, b) the configure function called in onCellForRowAt
, and c) the type of the array of users/contacts.
So, if we want to get ~fancy~, we can generalize this to ANY array of objects to be displayed in ANY UITableViewCell using generics:
func arrayDataSource<Model, Cell>(models: [Model],
configurer: @escaping (Model, Cell) -> Void,
identifier: String = "Cell")
-> UITableViewDataSource where Cell: UITableViewCell {
dataSource.onNumberOfRowsInSection = { _ in contacts.count }
dataSource.onCellForRowAt = { tableView, indexPath in
if let cell = tableView.dequeueReusableCell(withIdentifier: identifier) as? Cell {
configurer(cell, with: models[indexPath.row])
return cell
}
return UITableViewCell()
}
return dataSource
}
And we could use it like this:
func configure(_ cell: MyTableViewCell, with contact: Contact) { ... }
let contactsDataSource = arrayDataSource(models: contacts,
configurer: configure(_:with:),
identifier: "contactCell")
which reproduces the contactsDataSource
we defined above. As you can see, this approach is incredibly flexible, and doesn’t require a new UITableViewDataSource
subclass every time you want to display a list of things. It removes boilerplate and allows you to concentrate on the things that are *different* about your app.
So how would we actually use this on a table view? Or in a controller with a table view in it? Well, as a matter of fact, fuikit provides a view controller for that purpose. It’s a regular UIViewController subclass with an outlet for a tableView and a bunch of function properties for the view controller’s lifecycle, and it’s implementation looks something like this:
open class FPUITableViewViewController: UIViewController {
@IBOutlet public var tableView: UITableView?
public var onViewDidLoad: ((FPUITableViewViewController) -> Void)?
...
open override func viewDidLoad() {
super.viewDidLoad()
onViewDidLoad?(self)
}
...
}
This is a class I subclass all the time, since so many VCs contain table views somewhere. By having this as the superclass, I can use existing functions to set up the tableView
, while also allowing me to do the normal styling, IBActions, and other VC tasks that we’ve come to expect. So for instance, to display a list of contacts, I could create the following function:
func setupMyViewController(_ vc: MyViewController) {
let contactsDataSource = arrayDataSource(models: contacts,
configurer: configure(_:with:),
identifier: "contactCell")
vc.tableView?.dataSource = contactsDataSource
// since dataSource is a weak property on UITableView,
// store contactsDataSource in a strong property:
vc.dataSource = contactsDataSource
// style vc, etc.
}
class MyViewController: FPUITableViewViewController {
var dataSource: UITableViewDataSource?
}
Then, wherever you configure your VC (a flow coordinator, prepareForSegue, etc) you can just set the appropriate property, like so:
vc.onViewDidLoad = setupMyViewController
(This is not exactly true – onViewDidLoad
technically passes a FPUITableViewViewController, so you’d need to use an operator like ~>
to cast it appropriately, which is described on my article on optional casting.)
With this, the view controller becomes an empty, reusable, configurable class, and the functions involved are (mostly) reusable. This allows me to write more, better code faster, and build almost unimaginable libraries that speed up development and reduce bugs.
1 thought on “Making UIKit Functional-Programming Friendly”