Writing your App Swiftly Sommer Panage Chorus Fitness @sommer
Writing your App Swiftly
Sommer PanageChorus Fitness
@sommer
Patterns!
Today, in 4 short tales• Schrödinger's Result
• The Little Layout Engine that Could
• Swiftilocks and the Three View States
• Pete and the Repeated Code
The Demo App
Schrödinger's Result
Code in a box
func getFilms(completion: @escaping ([Film]?, APIError?) -> Void) { let url = SWAPI.baseURL.appendingPathComponent(Endpoint.films.rawValue) let task = self.session.dataTask(with: url) { (data, response, error) in if let data = data { do { let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) if let films = SWAPI.decodeFilms(jsonObject: jsonObject) { completion(films, nil) } else { completion(nil, .decoding) } } catch { completion(nil, .server(originalError: error)) } } else { completion(nil, .server(originalError: error!)) } } task.resume()}
What we think is happening…
What's actually happening…
override func viewDidLoad() { super.viewDidLoad()
apiClient.getFilms() { films, error in if let films = films { // Show film UI if let error = error { // Log warning...this is weird } } else if let error = error { // Show error UI } else { // No results at all? Show error UI I guess? } }}
Result open source framework by Rob Rix
Model our server interaction as it actually is - success / failure!public enum Result<T, Error: Swift.Error>: ResultProtocol { case success(T) case failure(Error)}
New, improved code
func getFilms(completion: @escaping (Result<[Film], APIError>) -> Void) { let task = self.session .dataTask(with: SWAPI.baseURL.appendingPathComponent(Endpoint.films.rawValue)) { (data, response, error) in let result = Result(data, failWith: APIError.server(originalError: error!)) .flatMap { data in Result<Any, AnyError>(attempt: { try JSONSerialization.jsonObject(with: data, options: []) }) .mapError { _ in APIError.decoding } } .flatMap { Result(SWAPI.decodeFilms(jsonObject: $0), failWith: .decoding) }
completion(result) } task.resume()}
New, improved code
override func viewDidLoad() { super.viewDidLoad()
apiClient.getFilms() { result in switch result { case .success(let films): print(films) // Show my UI! case .failure(let error): print(error) // Show some error UI } }}
The Moral of the StoryUsing the Result enum allowed us to
• Model the sucess/failure of our server interaction more correctly
• Thus simplify our view controller code.
The Little Layout Engine that Could
Old-school
override func layoutSubviews() { super.layoutSubviews()
// WHY AM I DOING THIS?!?!}
What about Storyboards and Xibs?
• Working in teams becomes harder because...
• XML diffs
• Merge conflicts?!
• No constants
• Stringly typed identifiers
• Fragile connections
Autolayout: iOS 9+ APIs
init() { super.init(frame: .zero)
addSubview(tableView)
// Autolayout: Table same size as parent tableView.translatesAutoresizingMaskIntoConstraints = false tableView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true tableView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true tableView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true tableView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true}
Autolayout: Cartography by Robb Böhnke
init() { super.init(frame: .zero)
addSubview(tableView)
// Autolayout: Table same size as parent constrain(tableView, self) { table, parent in table.edges == parent.edges }}
More Cartography
private let margin: CGFloat = 16private let episodeLeftPadding: CGFloat = 8
override init(style: UITableViewCellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(episodeLabel) contentView.addSubview(titleLabel)
constrain(episodeLabel, titleLabel, contentView) { episode, title, parent in episode.leading == parent.leading + margin episode.top == parent.top + margin episode.bottom == parent.bottom - margin
title.leading == episode.trailing + episodeLeftPadding title.trailing <= parent.trailing - margin title.centerY == episode.centerY }}
The Moral of the StoryUsing the Cartography framework harnesses Swift's
operator overloads to make programatic AutoLayout a breeze!
Swiftilocks and the Three View States
Swiftilocks and the Three View States
LOADING
Swiftilocks and the Three View States
SUCCESS
Swiftilocks and the Three View States
ERROR
State management with bools
/// MainView.swift
var isLoading: Bool = false { didSet { errorView.isHidden = true loadingView.isHidden = !isLoading }}
var isError: Bool = false { didSet { errorView.isHidden = !isError loadingView.isHidden = true }}
var items: [MovieItem]? { didSet { tableView.reloadData() }}
/// MainViewController.swift
override func viewDidLoad() { super.viewDidLoad()
title = "Star Wars Films" mainView.isLoading = true
apiClient.getFilms() { result in DispatchQueue.main.async { switch result { case .success(let films): self.mainView.items = films .map { MovieItem(episodeID: $0.episodeID, title: $0.title) } .sorted { $0.0.episodeID < $0.1.episodeID } self.mainView.isLoading = false self.mainView.isError = false case .failure(let error): self.mainView.isLoading = false self.mainView.isError = true } } }}
Too many states!!
Data presence + state?!
Enums to the rescue!
final class MainView: UIView {
enum State { case loading case loaded(items: [MovieItem]) case error(message: String) }
init(state: State) { ... }
// the rest of my class...}
var state: State { didSet { switch state { case .loading: items = nil loadingView.isHidden = false errorView.isHidden = true case .error(let message): print(message) items = nil loadingView.isHidden = true errorView.isHidden = false case .loaded(let movieItems): loadingView.isHidden = true errorView.isHidden = true items = movieItems tableView.reloadData() } }}
override func viewDidLoad() { super.viewDidLoad()
title = "Star Wars Films"
mainView.state = .loading
apiClient.getFilms() { result in DispatchQueue.main.async { switch result { case .success(let films): let items = films .map { MovieItem(episodeID: $0.episodeID, title: $0.title) } .sorted { $0.0.episodeID < $0.1.episodeID } self.mainView.state = .loaded(items: items) case .failure(let error): self.mainView.state = .error(message: "Error: \(error.localizedDescription)") } } }}
The Moral of the StoryModelling our view state with an enum with associated values allows us to:
1. Simplify our VC
2. Avoid ambiguous state
3. Centralize our logic
It's better...but...
Pete and the Repeated Code.
Repeated code
var state: State { didSet { switch state { case .loading: text = nil loadingView.isHidden = false errorView.isHidden = true case .error(let message): print(message) text = nil loadingView.isHidden = true errorView.isHidden = false case .loaded(let text): loadingView.isHidden = true errorView.isHidden = true text = text tableView.reloadData() } }}
Protocols save the day!!
• A shared interface of methods and properties
• Addresses a particular task
• Types adopting protocol need not be related
protocol DataLoading { associatedtype Data
var state: ViewState<Data> { get set } var loadingView: LoadingView { get } var errorView: ErrorView { get }
func update()}
enum ViewState<Content> { case loading case loaded(data: Content) case error(message: String)}
Default protocol implementation
extension DataLoading where Self: UIView { func update() { switch state { case .loading: loadingView.isHidden = false errorView.isHidden = true case .error(let error): loadingView.isHidden = true errorView.isHidden = false Log.error(error) case .loaded: loadingView.isHidden = true errorView.isHidden = true } }}
Conforming to DataLoading
1. Provide an errorView variable
2. Provide an loadingView variable
3. Provide a state variable that take some sort of Data
4. Call update() whenever needed
DataLoading in our Main View
final class MainView: UIView, DataLoading {
let loadingView = LoadingView() let errorView = ErrorView()
var state: ViewState<[MovieItem]> { didSet { update() // call update whenever we set our list of Movies tableView.reloadData() } }
DataLoading in our Crawl View
class CrawlView: UIView, DataLoading {
let loadingView = LoadingView() let errorView = ErrorView()
var state: ViewState<String> { didSet { update() crawlLabel.text = state.data } }
The Moral of the StoryDecomposing functionality that is shared by non-related objects into a protocol helps us
• Avoid duplicated code
• Consolidate our logic into one place
Conclusion• Result: easily differentiate our success/error pathways
• Cartography: use operator overloading to make code more readable
• ViewState enum: never have an ambigous view state!
• Protocols: define/decompose shared behaviors in unrelated types
THANK YOUContact Me:
@sommer on [email protected]