└── Reducers.playground ├── Pages ├── Currency Conversion - Reducers Intermediate.xcplaygroundpage │ └── Contents.swift ├── Currency Conversion - Reducers.xcplaygroundpage │ ├── Contents.swift │ └── timeline.xctimeline ├── Currency Conversion.xcplaygroundpage │ ├── Contents.swift │ └── timeline.xctimeline ├── Original - Image.xcplaygroundpage │ ├── Contents.swift │ └── timeline.xctimeline └── Reducers - Image.xcplaygroundpage │ ├── Contents.swift │ └── timeline.xctimeline ├── contents.xcplayground └── playground.xcworkspace └── contents.xcworkspacedata /Reducers.playground/Pages/Currency Conversion - Reducers Intermediate.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: Playground - noun: a place where people can play 2 | 3 | import UIKit 4 | 5 | enum Currency: String { 6 | case eur = "EUR" 7 | case usd = "USD" 8 | } 9 | 10 | func ratesURL(base: Currency = .eur) -> URL { 11 | return URL(string: "http://api.fixer.io/latest?base=\(base.rawValue)")! 12 | } 13 | 14 | struct State { 15 | private var inputAmount: Double? = nil 16 | private var rate: Double? = nil 17 | var output: Double? { 18 | guard let i = inputAmount, let r = rate else { return nil } 19 | return i * r 20 | } 21 | 22 | enum Message { 23 | case inputChanged(String?) 24 | case reload 25 | case ratesAvailable(data: Data?) 26 | } 27 | 28 | enum Command { 29 | case load(URL, onComplete: (Data?) -> Message) 30 | } 31 | 32 | mutating func send(_ message: Message) { 33 | switch message { 34 | case .inputChanged(let input): 35 | inputAmount = input.flatMap { Double($0) } 36 | case .reload: 37 | // TODO: load ratesURL() and update the rates 38 | fatalError() 39 | case .ratesAvailable(data: let data): 40 | guard let data = data, 41 | let json = try? JSONSerialization.jsonObject(with: data, options: []), 42 | let dict = json as? [String:Any], 43 | let dataDict = dict["rates"] as? [String:Double], 44 | let rate = dataDict[Currency.usd.rawValue] else { return } 45 | self.rate = rate 46 | } 47 | } 48 | } 49 | 50 | extension State.Command { 51 | func interpret(_ callback: @escaping (State.Message) -> ()) { 52 | switch self { 53 | case let .load(url, onComplete: transform): 54 | URLSession.shared.dataTask(with: url, completionHandler: { (data, _, _) in 55 | DispatchQueue.main.async { 56 | callback(transform(data)) 57 | } 58 | }).resume() 59 | } 60 | } 61 | } 62 | 63 | class CurrencyApp: UIViewController { 64 | let input: UITextField = { 65 | let result = UITextField() 66 | result.text = "100" 67 | result.borderStyle = .roundedRect 68 | return result 69 | }() 70 | let button: UIButton = { 71 | let result = UIButton(type: .custom) 72 | result.setTitle("Reload", for: .normal) 73 | return result 74 | }() 75 | let output: UILabel = { 76 | let result = UILabel() 77 | result.text = "..." 78 | return result 79 | }() 80 | let stackView = UIStackView() 81 | 82 | override func viewDidLoad() { 83 | super.viewDidLoad() 84 | view.backgroundColor = .lightGray 85 | stackView.axis = .vertical 86 | stackView.addArrangedSubview(input) 87 | stackView.addArrangedSubview(button) 88 | stackView.addArrangedSubview(output) 89 | view.addSubview(stackView) 90 | 91 | button.addTarget(self, action: #selector(reload), for: .touchUpInside) 92 | input.addTarget(self, action: #selector(inputChanged), for: .editingChanged) 93 | } 94 | 95 | override func viewWillAppear(_ animated: Bool) { 96 | super.viewWillAppear(animated) 97 | stackView.frame = view.bounds 98 | } 99 | 100 | var state = State() { 101 | didSet { 102 | self.output.text = state.output.map { "\($0) USD" } ?? "" 103 | } 104 | } 105 | 106 | private func send(_ message: State.Message) { 107 | state.send(message) 108 | } 109 | 110 | @objc func inputChanged() { 111 | send(.inputChanged(input.text)) 112 | } 113 | 114 | @objc func reload() { 115 | send(State.Message.reload) 116 | } 117 | 118 | } 119 | 120 | import PlaygroundSupport 121 | PlaygroundPage.current.liveView = CurrencyApp() 122 | -------------------------------------------------------------------------------- /Reducers.playground/Pages/Currency Conversion - Reducers.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: Playground - noun: a place where people can play 2 | 3 | import UIKit 4 | 5 | enum Currency: String { 6 | case eur = "EUR" 7 | case usd = "USD" 8 | } 9 | 10 | func ratesURL(base: Currency = .eur) -> URL { 11 | return URL(string: "http://api.fixer.io/latest?base=\(base.rawValue)")! 12 | } 13 | 14 | struct State { 15 | private var inputAmount: Double? = nil 16 | private var rate: Double? = nil 17 | var output: Double? { 18 | guard let i = inputAmount, let r = rate else { return nil } 19 | return i * r 20 | } 21 | 22 | enum Message { 23 | case inputChanged(String?) 24 | case reload 25 | case ratesAvailable(data: Data?) 26 | } 27 | 28 | enum Command { 29 | case load(URL, onComplete: (Data?) -> Message) 30 | } 31 | 32 | mutating func send(_ message: Message) -> Command? { 33 | switch message { 34 | case .inputChanged(let input): 35 | inputAmount = input.flatMap { Double($0) } 36 | return nil 37 | case .reload: 38 | return .load(ratesURL(), onComplete: Message.ratesAvailable) 39 | case .ratesAvailable(data: let data): 40 | guard let data = data, 41 | let json = try? JSONSerialization.jsonObject(with: data, options: []), 42 | let dict = json as? [String:Any], 43 | let dataDict = dict["rates"] as? [String:Double], 44 | let rate = dataDict[Currency.usd.rawValue] else { return nil } 45 | self.rate = rate 46 | return nil 47 | } 48 | } 49 | } 50 | 51 | extension State.Command { 52 | func interpret(_ callback: @escaping (State.Message) -> ()) { 53 | switch self { 54 | case let .load(url, onComplete: transform): 55 | URLSession.shared.dataTask(with: url, completionHandler: { (data, _, _) in 56 | DispatchQueue.main.async { 57 | callback(transform(data)) 58 | } 59 | }).resume() 60 | } 61 | } 62 | } 63 | 64 | class CurrencyApp: UIViewController { 65 | let input: UITextField = { 66 | let result = UITextField() 67 | result.text = "100" 68 | result.borderStyle = .roundedRect 69 | return result 70 | }() 71 | let button: UIButton = { 72 | let result = UIButton(type: .custom) 73 | result.setTitle("Reload", for: .normal) 74 | return result 75 | }() 76 | let output: UILabel = { 77 | let result = UILabel() 78 | result.text = "..." 79 | return result 80 | }() 81 | let stackView = UIStackView() 82 | 83 | override func viewDidLoad() { 84 | super.viewDidLoad() 85 | view.backgroundColor = .lightGray 86 | stackView.axis = .vertical 87 | stackView.addArrangedSubview(input) 88 | stackView.addArrangedSubview(button) 89 | stackView.addArrangedSubview(output) 90 | view.addSubview(stackView) 91 | 92 | button.addTarget(self, action: #selector(reload), for: .touchUpInside) 93 | input.addTarget(self, action: #selector(inputChanged), for: .editingChanged) 94 | } 95 | 96 | override func viewWillAppear(_ animated: Bool) { 97 | super.viewWillAppear(animated) 98 | stackView.frame = view.bounds 99 | } 100 | 101 | var state = State() { 102 | didSet { 103 | self.output.text = state.output.map { "\($0) USD" } ?? "" 104 | } 105 | } 106 | 107 | private func send(_ message: State.Message) { 108 | state.send(message)?.interpret(self.send) 109 | } 110 | 111 | @objc func inputChanged() { 112 | send(.inputChanged(input.text)) 113 | } 114 | 115 | @objc func reload() { 116 | send(State.Message.reload) 117 | } 118 | 119 | } 120 | 121 | import PlaygroundSupport 122 | PlaygroundPage.current.liveView = CurrencyApp() 123 | -------------------------------------------------------------------------------- /Reducers.playground/Pages/Currency Conversion - Reducers.xcplaygroundpage/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Reducers.playground/Pages/Currency Conversion.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: Playground - noun: a place where people can play 2 | 3 | import UIKit 4 | 5 | enum Currency: String { 6 | case eur = "EUR" 7 | case usd = "USD" 8 | } 9 | 10 | func ratesURL(base: Currency = .eur) -> URL { 11 | return URL(string: "http://api.fixer.io/latest?base=\(base.rawValue)")! 12 | } 13 | 14 | class CurrencyApp: UIViewController, UITextFieldDelegate { 15 | let input: UITextField = { 16 | let result = UITextField() 17 | result.text = "100" 18 | result.borderStyle = .roundedRect 19 | return result 20 | }() 21 | let button: UIButton = { 22 | let result = UIButton(type: .custom) 23 | result.setTitle("Reload", for: .normal) 24 | return result 25 | }() 26 | let output: UILabel = { 27 | let result = UILabel() 28 | result.text = "..." 29 | return result 30 | }() 31 | let stackView = UIStackView() 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | view.backgroundColor = .lightGray 36 | stackView.axis = .vertical 37 | stackView.addArrangedSubview(input) 38 | stackView.addArrangedSubview(button) 39 | stackView.addArrangedSubview(output) 40 | view.addSubview(stackView) 41 | 42 | button.addTarget(self, action: #selector(reload), for: .touchUpInside) 43 | input.addTarget(self, action: #selector(inputChanged), for: .editingChanged) 44 | } 45 | 46 | override func viewWillAppear(_ animated: Bool) { 47 | super.viewWillAppear(animated) 48 | stackView.frame = view.bounds 49 | } 50 | 51 | var rate: Double? 52 | @objc func inputChanged() { 53 | guard let rate = rate else { return } 54 | guard let text = input.text, let number = Double(text) else { return } 55 | output.text = "\(number * rate) USD" 56 | } 57 | 58 | @objc func reload() { 59 | URLSession.shared.dataTask(with: ratesURL()) { (data, _, _) in 60 | guard let data = data, 61 | let json = try? JSONSerialization.jsonObject(with: data, options: []), 62 | let dict = json as? [String:Any], 63 | let dataDict = dict["rates"] as? [String:Double], 64 | let rate = dataDict[Currency.usd.rawValue] else { return } 65 | DispatchQueue.main.async { [weak self] in 66 | self?.rate = rate 67 | self?.inputChanged() 68 | } 69 | }.resume() 70 | } 71 | 72 | } 73 | 74 | import PlaygroundSupport 75 | PlaygroundPage.current.liveView = CurrencyApp() 76 | -------------------------------------------------------------------------------- /Reducers.playground/Pages/Currency Conversion.xcplaygroundpage/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Reducers.playground/Pages/Original - Image.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: Playground - noun: a place where people can play 2 | 3 | import UIKit 4 | 5 | func randomGifURL(tag: String? = nil) -> URL { 6 | return URL(string: "http://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC")! 7 | } 8 | 9 | class GifApp: UIViewController { 10 | let imageView = UIImageView() 11 | let button: UIButton = { 12 | let result = UIButton(type: .custom) 13 | result.setTitle("Reload", for: .normal) 14 | return result 15 | }() 16 | let stackView = UIStackView() 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | stackView.axis = .vertical 21 | stackView.addArrangedSubview(imageView) 22 | stackView.addArrangedSubview(button) 23 | view.addSubview(stackView) 24 | 25 | button.addTarget(self, action: #selector(reload), for: .touchUpInside) 26 | } 27 | 28 | override func viewWillAppear(_ animated: Bool) { 29 | super.viewWillAppear(animated) 30 | stackView.frame = view.bounds 31 | } 32 | 33 | 34 | @objc func reload() { 35 | URLSession.shared.dataTask(with: randomGifURL()) { (data, _, _) in 36 | guard let data = data, 37 | let json = try? JSONSerialization.jsonObject(with: data, options: []), 38 | let dict = json as? [String:Any], 39 | let dataDict = dict["data"] as? [String:Any], 40 | let imageURLString = dataDict["image_url"] as? String, 41 | let url = URL(string: imageURLString) 42 | else { 43 | return 44 | } 45 | 46 | URLSession.shared.dataTask(with: url) { (data, _, _) in 47 | guard let data = data, let image = UIImage(data: data) else { return } 48 | DispatchQueue.main.async { [weak self] in 49 | self?.imageView.image = image 50 | } 51 | 52 | }.resume() 53 | 54 | }.resume() 55 | } 56 | 57 | } 58 | 59 | import PlaygroundSupport 60 | PlaygroundPage.current.liveView = GifApp() 61 | -------------------------------------------------------------------------------- /Reducers.playground/Pages/Original - Image.xcplaygroundpage/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Reducers.playground/Pages/Reducers - Image.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: Playground - noun: a place where people can play 2 | 3 | import UIKit 4 | 5 | func randomGifURL(tag: String? = nil) -> URL { 6 | return URL(string: "http://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC")! 7 | } 8 | 9 | struct GifAppState { 10 | var image: UIImage? 11 | 12 | enum Message { 13 | case reload 14 | case receiveMetaData(Data?) 15 | case receiveImageData(Data?) 16 | } 17 | 18 | enum Output { 19 | case load(URL, onComplete: (Data?) -> Message) 20 | } 21 | 22 | mutating func send(_ message: Message) -> Output? { 23 | switch message { 24 | case .reload: 25 | return .load(randomGifURL(), onComplete: Message.receiveMetaData) 26 | case .receiveMetaData(let data): 27 | guard let data = data, 28 | let json = try? JSONSerialization.jsonObject(with: data, options: []), 29 | let dict = json as? [String:Any], 30 | let dataDict = dict["data"] as? [String:Any], 31 | let imageURLString = dataDict["image_url"] as? String, 32 | let url = URL(string: imageURLString) else { 33 | return nil 34 | } 35 | return .load(url, onComplete: Message.receiveImageData) 36 | case .receiveImageData(let data): 37 | guard let data = data else { return nil } 38 | image = UIImage(data: data) 39 | return nil 40 | } 41 | } 42 | } 43 | 44 | extension GifAppState.Output { 45 | func interpret(_ callback: @escaping (GifAppState.Message) -> ()) { 46 | switch self { 47 | case let .load(url, onComplete: transform): 48 | URLSession.shared.dataTask(with: url, completionHandler: { (data, _, _) in 49 | DispatchQueue.main.async { 50 | callback(transform(data)) 51 | } 52 | }).resume() 53 | } 54 | } 55 | } 56 | 57 | class GifApp: UIViewController { 58 | let imageView = UIImageView() 59 | let button: UIButton = { 60 | let result = UIButton(type: .custom) 61 | result.setTitle("Reload", for: .normal) 62 | return result 63 | }() 64 | let stackView = UIStackView() 65 | 66 | var state: GifAppState = GifAppState(image: nil) { 67 | didSet { 68 | imageView.image = state.image 69 | } 70 | } 71 | 72 | override func viewDidLoad() { 73 | super.viewDidLoad() 74 | stackView.axis = .vertical 75 | stackView.addArrangedSubview(imageView) 76 | stackView.addArrangedSubview(button) 77 | view.addSubview(stackView) 78 | 79 | button.addTarget(self, action: #selector(reload), for: .touchUpInside) 80 | } 81 | 82 | override func viewWillAppear(_ animated: Bool) { 83 | super.viewWillAppear(animated) 84 | stackView.frame = view.bounds 85 | } 86 | 87 | func send(message: GifAppState.Message) { 88 | state.send(message)?.interpret(self.send) 89 | } 90 | 91 | @objc func reload() { 92 | send(message: .reload) 93 | } 94 | } 95 | 96 | import PlaygroundSupport 97 | PlaygroundPage.current.liveView = GifApp() 98 | -------------------------------------------------------------------------------- /Reducers.playground/Pages/Reducers - Image.xcplaygroundpage/timeline.xctimeline: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Reducers.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Reducers.playground/playground.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | --------------------------------------------------------------------------------