Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Writing Your App Swiftly

1,535 views

Published on

A talk discussing how the features of the Swift language itself such as enums, protocols and operator overloading can help you write clearer better iOS app code!

Published in: Software
  • Be the first to comment

Writing Your App Swiftly

  1. 1. Writing your App Swiftly Sommer Panage Chorus Fitness @sommer
  2. 2. Patterns!
  3. 3. 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
  4. 4. The Demo App
  5. 5. Schrödinger's Result
  6. 6. 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() }
  7. 7. What we think is happening…
  8. 8. 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? } } }
  9. 9. 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) }
  10. 10. 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() }
  11. 11. 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 } } }
  12. 12. 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.
  13. 13. The Little Layout Engine that Could
  14. 14. Old-school override func layoutSubviews() { super.layoutSubviews() // WHY AM I DOING THIS?!?! }
  15. 15. What about Storyboards and Xibs? • Working in teams becomes harder because... • XML diffs • Merge conflicts?! • No constants • Stringly typed identifiers • Fragile connections
  16. 16. 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 }
  17. 17. 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 } }
  18. 18. 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 } }
  19. 19. The Moral of the Story Using the Cartography framework harnesses Swift's operator overloads to make programatic AutoLayout a breeze!
  20. 20. Swiftilocks and the Three View States
  21. 21. Swiftilocks and the Three View States LOADING
  22. 22. Swiftilocks and the Three View States SUCCESS
  23. 23. Swiftilocks and the Three View States ERROR
  24. 24. 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() } }
  25. 25. /// 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 } } } }
  26. 26. Too many states!!
  27. 27. Data presence + state?!
  28. 28. 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... }
  29. 29. 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() } } }
  30. 30. 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)") } } } }
  31. 31. 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
  32. 32. It's better...but...
  33. 33. Pete and the Repeated Code.
  34. 34. 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() } } }
  35. 35. Protocols save the day!! • A shared interface of methods and properties • Addresses a particular task • Types adopting protocol need not be related
  36. 36. protocol DataLoading { associatedtype Data var state: ViewState<Data> { get set } var loadingView: LoadingView { get } var errorView: ErrorView { get } func update() }
  37. 37. enum ViewState<Content> { case loading case loaded(data: Content) case error(message: String) }
  38. 38. 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 } } }
  39. 39. 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
  40. 40. 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() } }
  41. 41. 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 } }
  42. 42. 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
  43. 43. 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
  44. 44. THANK YOU Contact Me: @sommer on Twitter me@sommerpanage.com

×