Swift minion for convenient creation of table and collection views
I made this for personal use, but feel free to use it or contribute. For more examples check out Sources and Tests.
Almost any app's interface is often going on in table or collection views. This is my take on making that task easier for myself, and hopefully for others too.
Idea behind this solution is quite abstract, meaning that it may be applied in many different ways (or styles) which are the most appropriate to your case or liking.
By proper usage of this framework, it will enforce you to write more clean and maintainable code by leveraging concepts of MVVM pattern. In order to understand it better you should be familiar with the "manual" approach in the first place.
It may not be quick and easy (for everyone) to grasp at the first look, but if you give it a proper chance you may never want to create another table or collection view without using this, just saying...
- Create custom table / collection based interface faster with less boilerplate
- Usable with static (menus, forms etc.) and dynamic (local, remote etc.) data
- Provides often used cells (TextInput, Slider, Toggle, Button etc.) out of the box
I suggest to start by getting familiar with DataSource.swift, because you're essentially gonna use that stuff for everything.
These are just very simple protocols starting with DataSource
which must have sections, then Section
must have items, where each Item
contains identifier: String
, viewModel: ViewModel
and child: DataSource?
.
/// ViewModel is whatever you choose it to be, easy like this:
struct MyCustomWhatever: ViewModel {}
There are simple structs conforming to all of these protocols in BasicDataSource.swift and most of the time it should be possible to just use those. As these structs conform to Codable
too, it is also possible to create BasicDataSource
from JSON data for example.
In case of something more specific, create custom types that conform to these protocols and use those instead.
There is a simple protocol in Cell.swift which is used both for table and collection view cells. Note that TableCell
and CollectionCell
are just a simple typealiases:
public typealias TableCell = UITableViewCell & Cell
public typealias CollectionCell = UICollectionViewCell & Cell
When creating custom cells, the easiest way is to subclass from TableCellBasic
or CollectionCellBasic
and override methods from this protocol:
/// Called in `init` and `awakeFromNib`, configure outlets and layout here.
func configure()
/// Called in `configure` and `prepareForReuse`, reset interface here.
func reset()
/// Called in `tableView(_:cellForRowAt:)`, update interface with view model here.
func update(with item: Item)
/// Called in `tableView(_:didSelectRowAt:)` and whenever specific cell calls it (ie. toggle switch).
/// By default this call will be forwarded to `delegate` (after setting some `userInfo` optionally).
/// If needed, call this where it makes sense for your cell, or override and call `super` at some moment.
func callback(_ sender: Any)
There are a few often used table view cells provided out of the box:
public enum TableCellType {
case basic
case subtitle
case leftDetail
case rightDetail
case toggle
case toggleWithSubtitle
case slider
case sliderWithLabels
case textField
case textView
case button
case spinner
case customClass(TableCell.Type)
case customNib(TableCell.Type)
}
While for the collection view cells you probably want to create something more custom... :)
public enum CollectionCellType {
case basic
case button
case spinner
case customClass(CollectionCell.Type)
case customNib(CollectionCell.Type)
}
Final part of this story is TableViewController
, which you guessed it, inherits from UITableViewController
.
Only this one is nice enough to register, dequeue and update all cells you'll ever need by just configuring its dataSource
property and overriding these methods:
/// - Note: Return proper cell type for the given item identifier.
/// Based on this it knows which cells to register for which identifier.
open func cellType(forIdentifier identifier: String) -> TableCellType {
return .basic
}
/// - Note: Update cell at the given index path.
/// `TableViewController` does this by default, so if that's enough for your case just skip this,
/// otherwise call `super.update(cell, at: indexPath)` and add custom logic after that.
open func update(_ cell: TableCell, at indexPath: IndexPath) {
let item = viewModel.item(at: indexPath)
cell.update(with: item)
cell.delegate = self
}
/// - Note: Handle action from cell for the given index path.
/// This will be called in `tableView(_:didSelectRowAt:)` or when `callback(_:)` is called
open func action(for cell: TableCell, at indexPath: IndexPath, sender: Any) {}
This is almost a duplicate of TableViewController
but it's using CollectionCell
so there's that.
You should take a look at the example project, but here's a quick preview:
import AEViewModel
struct ExampleDataSource: DataSource {
struct Id {
static let cells = "cells"
static let form = "form"
static let settings = "settings"
static let github = "github"
}
var title: String? = "Example"
var sections: [Section] = [
BasicSection(footer: "Default cells which are provided out of the box.", items: [
BasicItem(identifier: Id.cells, title: "Cells")
]),
BasicSection(header: "Demo", items: [
BasicItem(identifier: Id.form, title: "Form", detail: "Static Data Source"),
BasicItem(identifier: Id.settings, title: "Settings", detail: "JSON Data Source"),
BasicItem(identifier: Id.github, title: "Github", detail: "Remote Data Source")
])
]
}
final class ExampleTVC: TableViewController {
typealias Id = ExampleDataSource.Id
// MARK: Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
dataSource = ExampleDataSource()
}
// MARK: Override
override func cellType(forIdentifier identifier: String) -> TableCellType {
return .subtitle
}
override func update(_ cell: TableCell, at indexPath: IndexPath) {
super.update(cell, at: indexPath)
cell.accessoryType = .disclosureIndicator
}
override func action(for cell: TableCell, at indexPath: IndexPath, sender: Any) {
switch dataSource.identifier(at: indexPath) {
case Id.cells:
show(CellsTVC(), sender: self)
case Id.form:
show(FormTVC(), sender: self)
case Id.settings:
show(MainSettingsTVC(), sender: self)
case Id.github:
show(GithubTVC(), sender: self)
default:
break
}
}
}
-
.package(url: "https://github.com/tadija/AEViewModel.git", from: "0.9.2")
-
github "tadija/AEViewModel"
-
pod 'AEViewModel'
This code is released under the MIT license. See LICENSE for details.