Slide 1

Slide 1 text

how to control the world @stephencelis www.pointfree.co

Slide 2

Slide 2 text

why?

Slide 3

Slide 3 text

why not?

Slide 4

Slide 4 text

how to control the world

Slide 5

Slide 5 text

step one: describe the world

Slide 6

Slide 6 text

struct World { }

Slide 7

Slide 7 text

struct World { // ??? }

Slide 8

Slide 8 text

start small: control time

Slide 9

Slide 9 text

start small: control time Date() // 2018-09-13 16:50:01

Slide 10

Slide 10 text

start small: control time Date() // 2018-09-13 16:50:01 Date() // 2018-09-13 16:50:03

Slide 11

Slide 11 text

start small: control time Date() // 2018-09-13 16:50:01 Date() // 2018-09-13 16:50:03 Date() // 2018-09-13 16:50:06

Slide 12

Slide 12 text

struct World { }

Slide 13

Slide 13 text

struct World { var date: () -> Date }

Slide 14

Slide 14 text

struct World { var date: () -> Date = { Date() } }

Slide 15

Slide 15 text

struct World { var date = { Date() } }

Slide 16

Slide 16 text

☑ 1. describe the world struct World { var date = { Date() } }

Slide 17

Slide 17 text

step two: create the world

Slide 18

Slide 18 text

struct World { var date = { Date() } }

Slide 19

Slide 19 text

struct World { var date = { Date() } } var Current = World()

Slide 20

Slide 20 text

☑ 1. describe the world struct World { var date = { Date() } } ☑ 2. create the world var Current = World()

Slide 21

Slide 21 text

how to control the world

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

Current.date() // 2018-09-13 16:55:42

Slide 24

Slide 24 text

Current.date() // 2018-09-13 16:55:42 // Send the world back in time! Current.date = { .distantPast }

Slide 25

Slide 25 text

Current.date() // 2018-09-13 16:55:42 // Send the world back in time! Current.date = { .distantPast } Current.date() // 0001-01-01 00:00:00

Slide 26

Slide 26 text

Current.date() // 2018-09-13 16:55:42 // Send the world back in time! Current.date = { .distantPast } Current.date() // 0001-01-01 00:00:00 // Or into the future! Current.date = { .distantFuture }

Slide 27

Slide 27 text

Current.date() // 2018-09-13 16:55:42 // Send the world back in time! Current.date = { .distantPast } Current.date() // 0001-01-01 00:00:00 // Or into the future! Current.date = { .distantFuture } Current.date() // 4001-01-01 00:00:00

Slide 28

Slide 28 text

Current.date() // 2018-09-13 16:55:42 // Send the world back in time! Current.date = { .distantPast } Current.date() // 0001-01-01 00:00:00 // Or into the future! Current.date = { .distantFuture } Current.date() // 4001-01-01 00:00:00 Current.date() // 4001-01-01 00:00:00

Slide 29

Slide 29 text

Current.date() // 2018-09-13 16:55:42 // Send the world back in time! Current.date = { .distantPast } Current.date() // 0001-01-01 00:00:00 // Or into the future! Current.date = { .distantFuture } Current.date() // 4001-01-01 00:00:00 Current.date() // 4001-01-01 00:00:00 Current.date() // 4001-01-01 00:00:00

Slide 30

Slide 30 text

Current.date() // 2018-09-13 16:55:42 // Send the world back in time! Current.date = { .distantPast } Current.date() // 0001-01-01 00:00:00 // Or into the future! Current.date = { .distantFuture } Current.date() // 4001-01-01 00:00:00 Current.date() // 4001-01-01 00:00:00 Current.date() // 4001-01-01 00:00:00 // Restore the balance. Current.date = Date.init Current.date() // 2018-09-13 16:56:28

Slide 31

Slide 31 text

how to control the world

Slide 32

Slide 32 text

the old switcheroo

Slide 33

Slide 33 text

the old switcheroo Wherever we see: Date()

Slide 34

Slide 34 text

the old switcheroo Wherever we see: Date() Replace with: Current.date()

Slide 35

Slide 35 text

func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? ) -> Bool { Current.date = { Date(timeIntervalSinceReferenceDate: 0) } return true }

Slide 36

Slide 36 text

struct World { var date = { Date() } }

Slide 37

Slide 37 text

let formatter = DateFormatter() formatter.string(from: Current.date())

Slide 38

Slide 38 text

let formatter = DateFormatter() formatter.calendar // Calendar formatter.locale // Locale formatter.timeZone // TimeZone formatter.string(from: Current.date())

Slide 39

Slide 39 text

let’s take control! struct World { var date = { Date() } }

Slide 40

Slide 40 text

let’s take control! struct World { var calendar = Calendar.autoupdatingCurrent var date = { Date() } var locale = Locale.autoupdatingCurrent var timeZone = TimeZone.autoupdatingCurrent }

Slide 41

Slide 41 text

the old switcheroo Wherever we see or don’t see: Calendar.autoupdatingCurrent Locale.autoupdatingCurrent TimeZone.autoupdatingCurrent Replace with: Current.calendar Current.locale Current.timeZone

Slide 42

Slide 42 text

let’s take control! let formatter = DateFormatter() formatter.calendar = Current.calendar formatter.locale = Current.locale formatter.timeZone = Current.timeZone formatter.string(from: Current.date())

Slide 43

Slide 43 text

let’s take control! extension World { func dateFormatter() -> DateFormatter { let formatter = DateFormatter() formatter.calendar = self.calendar formatter.locale = self.locale formatter.timeZone = self.timeZone return formatter } } Current.dateFormatter()

Slide 44

Slide 44 text

let’s take control! Current.dateFormatter().string(from: Current.date()) // "September 13, 2018 at 5:00 PM" Current.calendar = Calendar(identifier: .buddhist) Current.locale = Locale(identifier: "es_ES") Current.timeZone = TimeZone(identifier: "Pacific/Honolulu")! Current.dateFormatter().string(from: Current.date()) // "13 de septiembre de 2561 BE, 17:00"

Slide 45

Slide 45 text

func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? ) -> Bool { Current.calendar = Calendar(identifier: .buddhist) Current.locale = Locale(identifier: "es_ES") Current.timeZone = TimeZone(identifier: "Pacific/Honolulu")! return true }

Slide 46

Slide 46 text

struct World { var calendar = Calendar.autoupdatingCurrent var date = { Date() } var locale = Locale.autoupdatingCurrent var timeZone = TimeZone.autoupdatingCurrent }

Slide 47

Slide 47 text

APIClient.shared.token = token APIClient.shared.fetchCurrentUser { result in // … }

Slide 48

Slide 48 text

APIClient.shared.token = token APIClient.shared.fetchCurrentUser { result in // … } struct API { var setToken = { APIClient.shared.token = $0 } var fetchCurrentUser = APIClient.shared.fetchCurrentUser }

Slide 49

Slide 49 text

APIClient.shared.token = token APIClient.shared.fetchCurrentUser { result in // … } struct API { var setToken = { APIClient.shared.token = $0 } var fetchCurrentUser = APIClient.shared.fetchCurrentUser } struct World { var api = API() // … }

Slide 50

Slide 50 text

the old switcheroo Wherever we see: APIClient.shared.token = token APIClient.shared.fetchCurrentUser { result in Replace with: Current.api.setToken(token) Current.api.fetchCurrentUser { result in

Slide 51

Slide 51 text

// Simulate being logged-in as a specific user Current.api.fetchCurrentUser = { callback in callback(.success(User(name: "Blob"))) }

Slide 52

Slide 52 text

// Simulate being logged-in as a specific user Current.api.fetchCurrentUser = { callback in callback(.success(User(name: "Blob"))) } // Simulate specific errors Current.api.fetchCurrentUser = { callback in callback(.failure(APIError.userSuspended)) }

Slide 53

Slide 53 text

this is not how we do things

Slide 54

Slide 54 text

aren’t singletons evil?

Slide 55

Slide 55 text

aren’t singletons evil? — singletons are only a problem when they’re out of our control

Slide 56

Slide 56 text

what about global mutation?

Slide 57

Slide 57 text

what about global mutation? — the option to mutate, not the requirement (avoid mutation in release mode)

Slide 58

Slide 58 text

what about global mutation? — the option to mutate, not the requirement (avoid mutation in release mode) — exercise restraint (with code review and lint checks)

Slide 59

Slide 59 text

what about global mutation? — the option to mutate, not the requirement (avoid mutation in release mode) — exercise restraint (with code review and lint checks) # .swiftlint.yml custom_rules: no_current_mutation: included: ".*\\.swift" excluded: ".*Test\\.swift" name: "Current Mutation" regex: "(Current\.\S+\s+=)" message: "Don’t mutate the current world!"

Slide 60

Slide 60 text

why structs?

Slide 61

Slide 61 text

why structs? — protocols can be a premature abstraction

Slide 62

Slide 62 text

why structs? — protocols can be a premature abstraction — protocols require a ton of boilerplate

Slide 63

Slide 63 text

protocol APIClientProtocol { var token: String? { get set } func fetchCurrentUser(_ completionHandler: (Result) -> Void) }

Slide 64

Slide 64 text

protocol APIClientProtocol { var token: String? { get set } func fetchCurrentUser(_ completionHandler: (Result) -> Void) } extension APIClient: APIClientProtocol {}

Slide 65

Slide 65 text

protocol APIClientProtocol { var token: String? { get set } func fetchCurrentUser(_ completionHandler: (Result) -> Void) } extension APIClient: APIClientProtocol {} class MockAPIClient: APIClientProtocol { var token: String? var currentUserResult: Result? func fetchCurrentUser(_ completionHandler: (Result) -> Void) { completionHandler(self.fetchCurrentUserResult!) } }

Slide 66

Slide 66 text

protocol APIClientProtocol { var token: String? { get set } func fetchCurrentUser(_ completionHandler: (Result) -> Void) } extension APIClient: APIClientProtocol {} class MockAPIClient: APIClientProtocol { var token: String? var currentUserResult: Result? func fetchCurrentUser(_ completionHandler: (Result) -> Void) { completionHandler(self.fetchCurrentUserResult!) } } struct World { var api: APIClientProtocol = APIClient.shared }

Slide 67

Slide 67 text

struct API { var setToken = { APIClient.shared.token = $0 } var fetchCurrentUser = APIClient.shared.fetchCurrentUser } struct World { var api = API() }

Slide 68

Slide 68 text

why structs? — protocols can be a premature abstraction — protocols require a ton of boilerplate

Slide 69

Slide 69 text

isn’t dependency injection be!er?

Slide 70

Slide 70 text

isn’t dependency injection be!er? — passing dependencies requires a lot more boilerplate

Slide 71

Slide 71 text

class MyViewController: UIViewController { let api: APIClientProtocol let date: () -> Date let label = UILabel() init(_ api: APIClientProtocol, _ date: () -> Date) { self.api = api self.date = date } func greet() { self.api.fetchCurrentUser { result in if let user = result.success { self.label.text = "Hi, \(user.name)! It’s \(self.date())." } } } }

Slide 72

Slide 72 text

class MyViewController: UIViewController { let api: APIClientProtocol let date: () -> Date init(_ api: APIClientProtocol, _ date: () -> Date) { self.api = api self.date = date } func presentChild() { let childViewController = ChildViewController( api: self.api, date: self.date ) } } class ChildViewController: UIViewController { let api: APIClientProtocol let date: () -> Date let label = UILabel() init(_ api: APIClientProtocol, _ date: () -> Date) { self.api = api self.date = date } func greet() { self.api.fetchCurrentUser { result in if let user = result.success { self.label.text = "Hi, \(user.name)! It’s \(self.date())." } } } }

Slide 73

Slide 73 text

protocol APIClientProvider { var api: APIClientProtocol { get } } protocol DateProvider { func date() -> Date } extension World: APIClientProvider, DateProvider {} class MyViewController: UIViewController { typealias Dependencies = APIClientProvider & DateProvider let label = UILabel() let dependencies: Dependencies init(dependencies: Dependencies) { self.dependencies = dependencies } func greet() { self.dependencies.api.fetchCurrentUser { result in if let user = result.success { self.label.text = "Hi, \(user.name)! It’s \(self.dependencies.date())." } } } }

Slide 74

Slide 74 text

class MyViewController: UIViewController { typealias Dependencies = APIClientProvider & DateProvider var dependencies: Dependencies! override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { if segue.identifier == "child" { let childViewController = segue.destinationViewController as! ChildViewController childViewController.dependencies = self.dependencies } } } class ChildViewController: UIViewController { typealias Dependencies = APIClientProvider & DateProvider var dependencies: Dependencies! @IBOutlet var label: UILabel! func greet() { self.dependencies.api.fetchCurrentUser { result in if let user = result.success { self.label.text = "Hi, \(user.name)! It’s \(self.dependencies.date())." } } } }

Slide 75

Slide 75 text

with Current: class MyViewController: UIViewController {} class ChildViewController: UIViewController { @IBOutlet var label: UILabel! func greet() { Current.api.fetchCurrentUser { result in if let user = result.success { self.label.text = "Hi, \(user.name)! It’s \(Current.date())." } } } }

Slide 76

Slide 76 text

guidelines for keeping it simple 1. singletons can be good (when there’s only one and you can control it) 2. global mutation can be good (when you’re not using it in production) 3. sometimes, you don’t need a protocol, and a struct can save you a ton of boilerplate 4. dependency injection is maybe more complicated of a solution than what we need

Slide 77

Slide 77 text

next steps?

Slide 78

Slide 78 text

class TestCase: XCTestCase { override func setUp() { super.setUp() Current = World( api: Api( setToken: { _ in }, fetchCurrentUser: { callback in callback(.success(User(name: "Blob")) } , calendar: Calendar(identifier: .gregorian), date: { Date(timeIntervalSinceReferenceDate: 0) } locale: Locale(identifier: "en_US"), timeZone: TimeZone(identifier: "UTC")! ) } }

Slide 79

Slide 79 text

extension API { static let mock = API( setToken: { _ in }, fetchCurrentUser: { callback in callback(.success(User(name: "Blob")) } ) } extension World { static let mock = World( api: .mock, calendar: Calendar(identifier: .gregorian), date: { Date(timeIntervalSinceReferenceDate: 0) } locale: Locale(identifier: "en_US"), timeZone: TimeZone(identifier: "UTC")! ) }

Slide 80

Slide 80 text

class TestCase: XCTestCase { override func setUp() { super.setUp() Current = .mock } }

Slide 81

Slide 81 text

next steps?

Slide 82

Slide 82 text

testing analytics struct World { var track = Analytics.shared.track } class TestCase: XCTestCase { var events: [Analytics.Event] = [] override func setUp() { super.setUp() Current = .mock Current.track = events.append } func testLoggingIn() { // … XCTAssertEqual([.loginStart, .loginSuccess], self.events) } }

Slide 83

Slide 83 text

testing localization struct World { var preferredLanguages = Locale.preferredLanguages } func localizedString(key: String, value: String) -> String { // … }

Slide 84

Slide 84 text

it can’t all be that simple!

Slide 85

Slide 85 text

it can’t all be that simple! — more complicated dependencies, like those following the delegate pattern, may require adopting simpler wrappers

Slide 86

Slide 86 text

it can’t all be that simple! — more complicated dependencies, like those following the delegate pattern, may require adopting simpler wrappers — ephemeral/local dependencies (like view controls and view delegates) shouldn’t be controlled on the world

Slide 87

Slide 87 text

in conclusion…

Slide 88

Slide 88 text

controlling the world is good — unlock the ability to simulate external state

Slide 89

Slide 89 text

controlling the world is simple — no need for the excessive boilerplate of protocols and dependency injection: store the minimal details of the world in a struct

Slide 90

Slide 90 text

Thanks!

Slide 91

Slide 91 text

Questions? @stephencelis www.pointfree.co