- Swift Team Code Standards
- Use latest Xcode version (Xcode 11).
- Use VIP (clean-swift cycle) for complex scenes.
- Use MVP architecture for small scenes (less than 300 line of code).
Clean Swift iOS Architecture for Fixing Massive View Controller
- Use Modular architecture and Swift Package Manager.
- Having the setup already in place, you may be wondering what development flow you could use while developing in SPM. This is what works for me right now:
- Pull the changes from remote and generate project using
swift build & swift package generate-xcodeproj. - Implement features/fixes and push it to the remote. When in need of another dependency, add it to the Package.swift and regenerate the project.
- Repeat steps 1 and 2.
Extract 3 or more properties or local variables (that share the same prefix) into their own method, type or tuple.
struct Path {
var string: String
}
func loadFile(at path: Path) throws -> File {
...
}
extension Path: ExpressibleByStringLiteral {
init(stringLiteral value: String) {
self.string = value
}
}
extension Path: ExpressibleByStringInterpolation {}
try loadFile(at: "~/documents/article.md")Making types expressible by string interpolation
Using child view controllers as plugins in Swift
Reusable data sources in Swift
Preventing views from being model aware in Swift
Let's make an extension on UIViewController that makes handling child view controllers a lot simpler:
extension UIViewController {
func add(_ child: UIViewController) {
addChild(child)
view.addSubview(child.view)
child.didMove(toParent: self)
}
func remove() {
// Just to be safe, we check that this view controller
// is actually added to a parent before removing it.
guard parent != nil else {
return
}
willMove(toParent: nil)
view.removeFromSuperview()
removeFromParent()
}
}Child view controller (LoadingViewController) that showing an activity indicator while loading data:
class LoadingViewController: UIViewController {
private lazy var activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
override func viewDidLoad() {
super.viewDidLoad()
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(activityIndicator)
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// We use a 0.5 second delay to not show an activity indicator
// in case our data loads very quickly.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.activityIndicator.startAnimating()
}
}
}ErrorViewController that displays an error message and has a Reload button with reloadHandler:
class ErrorViewController: UIViewController {
var reloadHandler: () -> Void = {}
}
private extension ListViewController {
func handle(_ error: Error) {
let errorViewController = ErrorViewController()
errorViewController.reloadHandler = { [weak self] in
self?.loadItems()
}
add(errorViewController)
}
}Using a child view controller:
class ListViewController: UITableViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadItems()
}
private func loadItems() {
let loadingViewController = LoadingViewController()
add(loadingViewController)
dataLoader.loadItems { [weak self] result in
loadingViewController.remove()
self?.handle(result)
}
}
}class TableViewDataSource<Model>: NSObject, UITableViewDataSource {
typealias CellConfigurator = (Model, UITableViewCell) -> Void
var models: [Model]
private let reuseIdentifier: String
private let cellConfigurator: CellConfigurator
init(models: [Model],
reuseIdentifier: String,
cellConfigurator: @escaping CellConfigurator) {
self.models = models
self.reuseIdentifier = reuseIdentifier
self.cellConfigurator = cellConfigurator
}
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return models.count
}
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = models[indexPath.row]
let cell = tableView.dequeueReusableCell(
withIdentifier: reuseIdentifier,
for: indexPath
)
cellConfigurator(model, cell)
return cell
}
}Using TableViewDataSource with 1 section:
extension TableViewDataSource where Model == Message {
static func make(for messages: [Message],
reuseIdentifier: String = "message") -> TableViewDataSource {
return TableViewDataSource(
models: messages,
reuseIdentifier: reuseIdentifier
) { (message, cell) in
cell.textLabel?.text = message.title
cell.detailTextLabel?.text = message.preview
}
}
}
func messagesDidLoad(_ messages: [Message]) {
dataSource = .make(for: messages)
tableView.dataSource = dataSource
}Composing sections
class SectionedTableViewDataSource: NSObject {
private let dataSources: [UITableViewDataSource]
init(dataSources: [UITableViewDataSource]) {
self.dataSources = dataSources
}
}
extension SectionedTableViewDataSource: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return dataSources.count
}
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
let dataSource = dataSources[section]
return dataSource.tableView(tableView, numberOfRowsInSection: 0)
}
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let dataSource = dataSources[indexPath.section]
let indexPath = IndexPath(row: indexPath.row, section: 0)
return dataSource.tableView(tableView, cellForRowAt: indexPath)
}
}Using SectionedTableViewDataSource with 2 sections:
let dataSource = SectionedTableViewDataSource(dataSources: [
TableViewDataSource.make(for: recentContacts),
TableViewDataSource.make(for: topMessages)
])Preventing views from being model aware in Swift:
class UserTableViewCellPresenter {
private let friendManager: FriendManager
init(friendManager: FriendManager) {
self.friendManager = friendManager
}
func configure(_ cell: UITableViewCell, forDisplaying user: User) {
cell.textLabel?.text = "\(user.firstName) \(user.lastName)"
cell.imageView?.image = user.profileImage
if !user.isFriend {
// We create a local reference to the friend manager so that
// the button doesn't have to capture the configurator.
let friendManager = self.friendManager
let addFriendButton = AddFriendButton()
addFriendButton.closure = {
friendManager.addUserAsFriend(user)
}
cell.accessoryView = addFriendButton
} else {
cell.accessoryView = nil
}
}
}
class UserListViewController: UITableViewController {
override func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let user = users[indexPath.row]
presenter.configure(cell, forDisplaying: user)
return cell
}
}View factories
For "static" views it's often enough to be able to configure them once. In such situations, using the Factory pattern can be a great option
class MessageViewFactory {
func makeView(for message: Message) -> UIView {
let view = TextView()
view.titleLabel.text = message.title
view.textLabel.text = message.text
view.imageView.image = message.icon
return view
}
}A Swift command line tool for generating your Xcode project
Soulful docs for Swift
External tool: Codecov