The document summarizes techniques for improving app development in Swift. It discusses using the Result enum to model success and failure states from network requests, using the Cartography framework to simplify auto layout code, representing view states with an enum to avoid ambiguity, and defining shared behaviors with protocols to reduce duplicated code. The techniques aim to make code more readable, simplify view controller logic, centralize state management, and prevent duplicated implementation across unrelated types.
5. 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
11. 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?
}
}
}
12. 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)
}
13. 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()
}
14. 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
}
}
}
15. 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.
18. What about Storyboards and Xibs?
• Working in teams becomes harder because...
• XML diffs
• Merge conflicts?!
• No constants
• Stringly typed identifiers
• Fragile connections
32. 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...
}
33. 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()
}
}
}
34. 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)")
}
}
}
}
35. 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
38. 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()
}
}
}
39. Protocols save the day!!
• A shared interface of methods and properties
• Addresses a particular task
• Types adopting protocol need not be related
40. protocol DataLoading {
associatedtype Data
var state: ViewState<Data> { get set }
var loadingView: LoadingView { get }
var errorView: ErrorView { get }
func update()
}
42. 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
}
}
}
43. 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
44. 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()
}
}
45. 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
}
}
46. 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
47. 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