└── 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 |
--------------------------------------------------------------------------------