Slide 1

Slide 1 text

Writing your App Swiftly Sommer Panage Chorus Fitness @sommer

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Patterns!

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

The Demo App

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

Schrödinger's Result

Slide 9

Slide 9 text

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() }

Slide 10

Slide 10 text

What we think is happening…

Slide 11

Slide 11 text

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? } } }

Slide 12

Slide 12 text

Result open source framework by Rob Rix Model our server interaction as it actually is - success / failure! public enum Result: ResultProtocol { case success(T) case failure(Error) }

Slide 13

Slide 13 text

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(attempt: { try JSONSerialization.jsonObject(with: data, options: []) }) .mapError { _ in APIError.decoding } } .flatMap { Result(SWAPI.decodeFilms(jsonObject: $0), failWith: .decoding) } completion(result) } task.resume() }

Slide 14

Slide 14 text

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 } } }

Slide 15

Slide 15 text

The Moral of the Story Using the Result enum allowed us to • Model the sucess/failure of our server interaction more correctly • Thus simplify our view controller code.

Slide 16

Slide 16 text

The Little Layout Engine that Could

Slide 17

Slide 17 text

Old-school override func layoutSubviews() { super.layoutSubviews() // WHY AM I DOING THIS?!?! }

Slide 18

Slide 18 text

What about Storyboards and Xibs? • Working in teams becomes harder because... • XML diffs • Merge conflicts?! • No constants • Stringly typed identifiers • Fragile connections

Slide 19

Slide 19 text

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 }

Slide 20

Slide 20 text

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 } }

Slide 21

Slide 21 text

More Cartography private let margin: CGFloat = 16 private 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 } }

Slide 22

Slide 22 text

The Moral of the Story Using the Cartography framework harnesses Swift's operator overloads to make programatic AutoLayout a breeze!

Slide 23

Slide 23 text

Swiftilocks and the Three View States

Slide 24

Slide 24 text

Swiftilocks and the Three View States LOADING

Slide 25

Slide 25 text

Swiftilocks and the Three View States SUCCESS

Slide 26

Slide 26 text

Swiftilocks and the Three View States ERROR

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

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() } }

Slide 29

Slide 29 text

/// 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 } } } }

Slide 30

Slide 30 text

Too many states!!

Slide 31

Slide 31 text

Data presence + state?!

Slide 32

Slide 32 text

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... }

Slide 33

Slide 33 text

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() } } }

Slide 34

Slide 34 text

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)") } } } }

Slide 35

Slide 35 text

The Moral of the Story Modelling our view state with an enum with associated values allows us to: 1. Simplify our VC 2. Avoid ambiguous state 3. Centralize our logic

Slide 36

Slide 36 text

It's better...but...

Slide 37

Slide 37 text

Pete and the Repeated Code.

Slide 38

Slide 38 text

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() } } }

Slide 39

Slide 39 text

Protocols save the day!! • A shared interface of methods and properties • Addresses a particular task • Types adopting protocol need not be related

Slide 40

Slide 40 text

protocol DataLoading { associatedtype Data var state: ViewState { get set } var loadingView: LoadingView { get } var errorView: ErrorView { get } func update() }

Slide 41

Slide 41 text

enum ViewState { case loading case loaded(data: Content) case error(message: String) }

Slide 42

Slide 42 text

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 } } }

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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() } }

Slide 45

Slide 45 text

DataLoading in our Crawl View class CrawlView: UIView, DataLoading { let loadingView = LoadingView() let errorView = ErrorView() var state: ViewState { didSet { update() crawlLabel.text = state.data } }

Slide 46

Slide 46 text

The Moral of the Story Decomposing functionality that is shared by non- related objects into a protocol helps us • Avoid duplicated code • Consolidate our logic into one place

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

THANK YOU Contact Me: @sommer on Twitter [email protected]