48
Writing your App Swiftly Sommer Panage Chorus Fitness @sommer

Writing Your App Swiftly

Embed Size (px)

Citation preview

Page 1: Writing Your App Swiftly

Writing your App Swiftly

Sommer PanageChorus Fitness

@sommer

Page 2: Writing Your App Swiftly
Page 3: Writing Your App Swiftly
Page 4: Writing Your App Swiftly

Patterns!

Page 5: Writing Your App Swiftly

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

Page 6: Writing Your App Swiftly

The Demo App

Page 7: Writing Your App Swiftly
Page 8: Writing Your App Swiftly

Schrödinger's Result

Page 9: Writing Your App Swiftly

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

Page 10: Writing Your App Swiftly

What we think is happening…

Page 11: Writing Your App Swiftly

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

Page 12: Writing Your App Swiftly

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

Page 13: Writing Your App Swiftly

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

Page 14: Writing Your App Swiftly

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

Page 15: Writing Your App Swiftly

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.

Page 16: Writing Your App Swiftly

The Little Layout Engine that Could

Page 17: Writing Your App Swiftly

Old-school

override func layoutSubviews() { super.layoutSubviews()

// WHY AM I DOING THIS?!?!}

Page 18: Writing Your App Swiftly

What about Storyboards and Xibs?

• Working in teams becomes harder because...

• XML diffs

• Merge conflicts?!

• No constants

• Stringly typed identifiers

• Fragile connections

Page 19: Writing Your App Swiftly

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}

Page 20: Writing Your App Swiftly

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

Page 21: Writing Your App Swiftly

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

Page 22: Writing Your App Swiftly

The Moral of the StoryUsing the Cartography framework harnesses Swift's

operator overloads to make programatic AutoLayout a breeze!

Page 23: Writing Your App Swiftly

Swiftilocks and the Three View States

Page 24: Writing Your App Swiftly

Swiftilocks and the Three View States

LOADING

Page 25: Writing Your App Swiftly

Swiftilocks and the Three View States

SUCCESS

Page 26: Writing Your App Swiftly

Swiftilocks and the Three View States

ERROR

Page 27: Writing Your App Swiftly
Page 28: Writing Your App Swiftly

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

Page 29: Writing Your App Swiftly

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

Page 30: Writing Your App Swiftly

Too many states!!

Page 31: Writing Your App Swiftly

Data presence + state?!

Page 32: Writing Your App Swiftly

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

Page 33: Writing Your App Swiftly

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

Page 34: Writing Your App Swiftly

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

Page 35: Writing Your App Swiftly

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

Page 36: Writing Your App Swiftly

It's better...but...

Page 37: Writing Your App Swiftly

Pete and the Repeated Code.

Page 38: Writing Your App Swiftly

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

Page 39: Writing Your App Swiftly

Protocols save the day!!

• A shared interface of methods and properties

• Addresses a particular task

• Types adopting protocol need not be related

Page 40: Writing Your App Swiftly

protocol DataLoading { associatedtype Data

var state: ViewState<Data> { get set } var loadingView: LoadingView { get } var errorView: ErrorView { get }

func update()}

Page 41: Writing Your App Swiftly

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

Page 42: Writing Your App Swiftly

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

Page 43: Writing Your App Swiftly

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

Page 44: Writing Your App Swiftly

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

Page 45: Writing Your App Swiftly

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

Page 46: Writing Your App Swiftly

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

Page 47: Writing Your App Swiftly

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

Page 48: Writing Your App Swiftly

THANK YOUContact Me:

@sommer on [email protected]