Upgrade to Pro — share decks privately, control downloads, hide ads and more …

try! Swift 2017 - Writing your App Swiftly

Sommer Panage
February 05, 2017

try! Swift 2017 - Writing your App Swiftly

Clean code tips and tricks that are Swift-specific

Sommer Panage

February 05, 2017
Tweet

More Decks by Sommer Panage

Other Decks in Technology

Transcript

  1. 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
  2. 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() }
  3. 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? } } }
  4. 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) }
  5. 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() }
  6. 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 } } }
  7. 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.
  8. What about Storyboards and Xibs? • Working in teams becomes

    harder because... • XML diffs • Merge conflicts?! • No constants • Stringly typed identifiers • Fragile connections
  9. 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 }
  10. 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 } }
  11. 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 } }
  12. The Moral of the Story Using the Cartography framework harnesses

    Swift's operator overloads to make programatic AutoLayout a breeze!
  13. 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() } }
  14. /// 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 } } } }
  15. 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... }
  16. 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() } } }
  17. 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)") } } } }
  18. 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
  19. 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() } } }
  20. Protocols save the day!! • A shared interface of methods

    and properties • Addresses a particular task • Types adopting protocol need not be related
  21. protocol DataLoading { associatedtype Data var state: ViewState<Data> { get

    set } var loadingView: LoadingView { get } var errorView: ErrorView { get } func update() }
  22. 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 } } }
  23. 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
  24. 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() } }
  25. 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 } }
  26. 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
  27. 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