Skip to content

Commit 1dc9183

Browse files
authored
Merge pull request #300 from mattrubin/menu-component-refactor
Refactor the Menu component
2 parents c7d69c7 + 4425e34 commit 1dc9183

File tree

4 files changed

+126
-91
lines changed

4 files changed

+126
-91
lines changed

Authenticator/Source/Menu.swift

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@
2323
// SOFTWARE.
2424
//
2525

26-
struct Menu {
27-
let infoList: InfoList
28-
private(set) var child: Child
26+
import Foundation
2927

30-
enum Child {
28+
struct Menu: Component {
29+
private let infoList: InfoList
30+
private var child: Child
31+
32+
private enum Child {
3133
case none
3234
case info(Info)
3335
case displayOptions(DisplayOptions)
@@ -54,6 +56,8 @@ struct Menu {
5456
child = .info(info)
5557
}
5658

59+
// MARK: View
60+
5761
func viewModel(digitGroupSize: Int) -> ViewModel {
5862
return ViewModel(infoList: infoList.viewModel, child: child.viewModel(digitGroupSize: digitGroupSize))
5963
}
@@ -69,34 +73,123 @@ struct Menu {
6973
}
7074
}
7175

76+
// MARK: Update
77+
78+
enum Action {
79+
case dismissInfo
80+
case dismissDisplayOptions
81+
82+
case infoListEffect(InfoList.Effect)
83+
case infoEffect(Info.Effect)
84+
case displayOptionsEffect(DisplayOptions.Effect)
85+
}
86+
87+
enum Effect {
88+
case dismissMenu
89+
case showErrorMessage(String)
90+
case showSuccessMessage(String)
91+
case openURL(URL)
92+
case setDigitGroupSize(Int)
93+
}
94+
95+
mutating func update(with action: Action) throws -> Effect? {
96+
switch action {
97+
case .dismissInfo:
98+
try dismissInfo()
99+
return nil
100+
101+
case .dismissDisplayOptions:
102+
try dismissDisplayOptions()
103+
return nil
104+
105+
case .infoListEffect(let effect):
106+
return try handleInfoListEffect(effect)
107+
108+
case .infoEffect(let effect):
109+
return handleInfoEffect(effect)
110+
111+
case .displayOptionsEffect(let effect):
112+
return handleDisplayOptionsEffect(effect)
113+
}
114+
}
115+
116+
private mutating func handleInfoListEffect(_ effect: InfoList.Effect) throws -> Effect? {
117+
switch effect {
118+
case .showDisplayOptions:
119+
try showDisplayOptions()
120+
return nil
121+
122+
case .showBackupInfo:
123+
let backupInfo: Info
124+
do {
125+
backupInfo = try Info.backupInfo()
126+
} catch {
127+
return .showErrorMessage("Failed to load backup info.")
128+
}
129+
try showInfo(backupInfo)
130+
return nil
131+
132+
case .showLicenseInfo:
133+
let licenseInfo: Info
134+
do {
135+
licenseInfo = try Info.licenseInfo()
136+
} catch {
137+
return .showErrorMessage("Failed to load acknowledgements.")
138+
}
139+
try showInfo(licenseInfo)
140+
return nil
141+
142+
case .done:
143+
return .dismissMenu
144+
}
145+
}
146+
147+
private mutating func handleInfoEffect(_ effect: Info.Effect) -> Effect? {
148+
switch effect {
149+
case .done:
150+
return .dismissMenu
151+
case let .openURL(url):
152+
return .openURL(url)
153+
}
154+
}
155+
156+
private mutating func handleDisplayOptionsEffect(_ effect: DisplayOptions.Effect) -> Effect? {
157+
switch effect {
158+
case .done:
159+
return .dismissMenu
160+
case let .setDigitGroupSize(digitGroupSize):
161+
return .setDigitGroupSize(digitGroupSize)
162+
}
163+
}
164+
72165
// MARK: -
73166

74-
enum Error: Swift.Error {
167+
private enum Error: Swift.Error {
75168
case badChildState
76169
}
77170

78-
mutating func showInfo(_ info: Info) throws {
171+
private mutating func showInfo(_ info: Info) throws {
79172
guard case .none = child else {
80173
throw Error.badChildState
81174
}
82175
child = .info(info)
83176
}
84177

85-
mutating func dismissInfo() throws {
178+
private mutating func dismissInfo() throws {
86179
guard case .info = child else {
87180
throw Error.badChildState
88181
}
89182
child = .none
90183
}
91184

92-
mutating func showDisplayOptions() throws {
185+
private mutating func showDisplayOptions() throws {
93186
guard case .none = child else {
94187
throw Error.badChildState
95188
}
96189
child = .displayOptions(DisplayOptions())
97190
}
98191

99-
mutating func dismissDisplayOptions() throws {
192+
private mutating func dismissDisplayOptions() throws {
100193
guard case .displayOptions = child else {
101194
throw Error.badChildState
102195
}

Authenticator/Source/Root.swift

Lines changed: 12 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,7 @@ extension Root {
8888
case tokenEntryFormAction(TokenEntryForm.Action)
8989
case tokenEditFormAction(TokenEditForm.Action)
9090
case tokenScannerAction(TokenScanner.Action)
91-
92-
case infoListEffect(InfoList.Effect)
93-
case infoEffect(Info.Effect)
94-
case displayOptionsEffect(DisplayOptions.Effect)
95-
case dismissInfo
96-
case dismissDisplayOptions
91+
case menuAction(Menu.Action)
9792

9893
case addTokenFromURL(Token)
9994
}
@@ -161,26 +156,11 @@ extension Root {
161156
handleTokenScannerEffect(effect)
162157
}
163158

164-
case .infoListEffect(let effect):
165-
return try handleInfoListEffect(effect)
166-
167-
case .infoEffect(let effect):
168-
return handleInfoEffect(effect)
169-
170-
case .displayOptionsEffect(let effect):
171-
return handleDisplayOptionsEffect(effect)
172-
173-
case .dismissInfo:
174-
try modal.withMenu { menu in
175-
try menu.dismissInfo()
176-
}
177-
return nil
178-
179-
case .dismissDisplayOptions:
180-
try modal.withMenu { menu in
181-
try menu.dismissDisplayOptions()
159+
case .menuAction(let action):
160+
let effect = try modal.withMenu({ menu in try menu.update(with: action) })
161+
return effect.flatMap { effect in
162+
handleMenuEffect(effect)
182163
}
183-
return nil
184164

185165
case .addTokenFromURL(let token):
186166
return .addToken(token,
@@ -323,60 +303,21 @@ extension Root {
323303
}
324304
}
325305

326-
private mutating func handleInfoListEffect(_ effect: InfoList.Effect) throws -> Effect? {
306+
private mutating func handleMenuEffect(_ effect: Menu.Effect) -> Effect? {
327307
switch effect {
328-
case .showDisplayOptions:
329-
try modal.withMenu { menu in
330-
try menu.showDisplayOptions()
331-
}
332-
return nil
333-
334-
case .showBackupInfo:
335-
let backupInfo: Info
336-
do {
337-
backupInfo = try Info.backupInfo()
338-
} catch {
339-
return .showErrorMessage("Failed to load backup info.")
340-
}
341-
try modal.withMenu { menu in
342-
try menu.showInfo(backupInfo)
343-
}
344-
return nil
345-
346-
case .showLicenseInfo:
347-
let licenseInfo: Info
348-
do {
349-
licenseInfo = try Info.licenseInfo()
350-
} catch {
351-
return .showErrorMessage("Failed to load acknowledgements.")
352-
}
353-
try modal.withMenu { menu in
354-
try menu.showInfo(licenseInfo)
355-
}
356-
return nil
357-
358-
case .done:
308+
case .dismissMenu:
359309
modal = .none
360310
return nil
361311

362-
}
363-
}
312+
case let .showErrorMessage(message):
313+
return .showErrorMessage(message)
314+
315+
case let .showSuccessMessage(message):
316+
return .showSuccessMessage(message)
364317

365-
private mutating func handleInfoEffect(_ effect: Info.Effect) -> Effect? {
366-
switch effect {
367-
case .done:
368-
modal = .none
369-
return nil
370318
case let .openURL(url):
371319
return .openURL(url)
372-
}
373-
}
374320

375-
private mutating func handleDisplayOptionsEffect(_ effect: DisplayOptions.Effect) -> Effect? {
376-
switch effect {
377-
case .done:
378-
modal = .none
379-
return nil
380321
case let .setDigitGroupSize(digitGroupSize):
381322
return .setDigitGroupSize(digitGroupSize)
382323
}

Authenticator/Source/RootViewController.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,23 +155,23 @@ extension RootViewController {
155155
case .info(let infoViewModel):
156156
presentViewModels(menuViewModel.infoList,
157157
using: InfoListViewController.self,
158-
actionTransform: Root.Action.infoListEffect,
158+
actionTransform: compose(Menu.Action.infoListEffect, Root.Action.menuAction),
159159
and: infoViewModel,
160160
using: InfoViewController.self,
161-
actionTransform: Root.Action.infoEffect)
161+
actionTransform: compose(Menu.Action.infoEffect, Root.Action.menuAction))
162162

163163
case .displayOptions(let displayOptionsViewModel):
164164
presentViewModels(menuViewModel.infoList,
165165
using: InfoListViewController.self,
166-
actionTransform: Root.Action.infoListEffect,
166+
actionTransform: compose(Menu.Action.infoListEffect, Root.Action.menuAction),
167167
and: displayOptionsViewModel,
168168
using: DisplayOptionsViewController.self,
169-
actionTransform: Root.Action.displayOptionsEffect)
169+
actionTransform: compose(Menu.Action.displayOptionsEffect, Root.Action.menuAction))
170170

171171
case .none:
172172
presentViewModel(menuViewModel.infoList,
173173
using: InfoListViewController.self,
174-
actionTransform: Root.Action.infoListEffect)
174+
actionTransform: compose(Menu.Action.infoListEffect, Root.Action.menuAction))
175175
}
176176
}
177177
currentViewModel = viewModel
@@ -216,11 +216,11 @@ extension RootViewController: UINavigationControllerDelegate {
216216
case .info:
217217
// If the current modal state is the menu with an Info child, and the just-shown view controller is
218218
// an InfoList, then the user has popped the Info view controller.
219-
dispatchAction(.dismissInfo)
219+
dispatchAction(.menuAction(.dismissInfo))
220220
case .displayOptions:
221221
// If the current modal state is the menu with a DisplayOptions child, and the just-shown view
222222
// controller is an InfoList, then the user has popped the DisplayOptions view controller.
223-
dispatchAction(.dismissDisplayOptions)
223+
dispatchAction(.menuAction(.dismissDisplayOptions))
224224
default:
225225
break
226226
}

AuthenticatorTests/RootTests.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class RootTests: XCTestCase {
6666
}
6767

6868
// Hide the backup info.
69-
let hideAction: Root.Action = .infoEffect(.done)
69+
let hideAction: Root.Action = .menuAction(.infoEffect(.done))
7070
let hideEffect: Root.Effect?
7171
do {
7272
hideEffect = try root.update(with: hideAction)
@@ -113,7 +113,7 @@ class RootTests: XCTestCase {
113113
}
114114

115115
// Show the license info.
116-
let showAction: Root.Action = .infoListEffect(.showLicenseInfo)
116+
let showAction: Root.Action = .menuAction(.infoListEffect(.showLicenseInfo))
117117
let showEffect: Root.Effect?
118118
do {
119119
showEffect = try root.update(with: showAction)
@@ -138,7 +138,7 @@ class RootTests: XCTestCase {
138138
}
139139

140140
// Hide the license info.
141-
let hideAction: Root.Action = .infoEffect(.done)
141+
let hideAction: Root.Action = .menuAction(.infoEffect(.done))
142142
let hideEffect: Root.Effect?
143143
do {
144144
hideEffect = try root.update(with: hideAction)
@@ -164,9 +164,10 @@ class RootTests: XCTestCase {
164164
return
165165
}
166166

167-
let action: Root.Action = .infoEffect(.openURL(url))
167+
let action: Root.Action = .menuAction(.infoEffect(.openURL(url)))
168168
let effect: Root.Effect?
169169
do {
170+
XCTAssertNil(try root.update(with: .tokenListAction(.showBackupInfo)))
170171
effect = try root.update(with: action)
171172
} catch {
172173
XCTFail("Unexpected error: \(error)")

0 commit comments

Comments
 (0)