writing your app swiftly

Post on 20-Mar-2017

759 Views

Category:

Software

2 Downloads

Preview:

Click to see full reader

TRANSCRIPT

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 Twitterme@sommerpanage.com

top related