Skip to content

Commit f0a31ec

Browse files
tgrapperonp4checo
authored andcommitted
Add a UI test for escaped ViewStore from WithViewStore, and a Binding animations test bench (#1819)
* Add a UITest for escaped `ViewStore` from `WithViewStore` * Add `BindingsAnimationsTestBench` * Cleanup (cherry picked from commit db39334dcc593f613b63a5a84d80d70f0063f388)
1 parent 0012028 commit f0a31ec

File tree

5 files changed

+312
-1
lines changed

5 files changed

+312
-1
lines changed

Examples/Integration/Integration.xcodeproj/project.pbxproj

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
CAA1CAFC296DEE79000665B1 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CAA1CAFB296DEE79000665B1 /* Preview Assets.xcassets */; };
1616
CAA1CB10296DEE79000665B1 /* ForEachBindingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA1CB0F296DEE79000665B1 /* ForEachBindingTests.swift */; };
1717
CAA1CB1F296DEEAC000665B1 /* ForEachBindingTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA1CB1E296DEEAC000665B1 /* ForEachBindingTestCase.swift */; };
18+
E9919D3E296E28C800C8716B /* EscapedWithViewStoreTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9919D3D296E28C800C8716B /* EscapedWithViewStoreTestCase.swift */; };
19+
E9919D40296E3EF400C8716B /* EscapedWithViewStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9919D3F296E3EF400C8716B /* EscapedWithViewStoreTests.swift */; };
20+
E9919D42296E47A400C8716B /* BindingsAnimationsTestBench.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9919D41296E47A400C8716B /* BindingsAnimationsTestBench.swift */; };
1821
/* End PBXBuildFile section */
1922

2023
/* Begin PBXContainerItemProxy section */
@@ -38,6 +41,9 @@
3841
CAA1CB0B296DEE79000665B1 /* IntegrationUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IntegrationUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3942
CAA1CB0F296DEE79000665B1 /* ForEachBindingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForEachBindingTests.swift; sourceTree = "<group>"; };
4043
CAA1CB1E296DEEAC000665B1 /* ForEachBindingTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForEachBindingTestCase.swift; sourceTree = "<group>"; };
44+
E9919D3D296E28C800C8716B /* EscapedWithViewStoreTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EscapedWithViewStoreTestCase.swift; sourceTree = "<group>"; };
45+
E9919D3F296E3EF400C8716B /* EscapedWithViewStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EscapedWithViewStoreTests.swift; sourceTree = "<group>"; };
46+
E9919D41296E47A400C8716B /* BindingsAnimationsTestBench.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindingsAnimationsTestBench.swift; sourceTree = "<group>"; };
4147
/* End PBXFileReference section */
4248

4349
/* Begin PBXFrameworksBuildPhase section */
@@ -82,9 +88,11 @@
8288
CAA1CAF3296DEE78000665B1 /* Integration */ = {
8389
isa = PBXGroup;
8490
children = (
91+
E9919D41296E47A400C8716B /* BindingsAnimationsTestBench.swift */,
92+
E9919D3D296E28C800C8716B /* EscapedWithViewStoreTestCase.swift */,
8593
CAA1CB1E296DEEAC000665B1 /* ForEachBindingTestCase.swift */,
86-
CAA1CAF4296DEE78000665B1 /* IntegrationApp.swift */,
8794
CA595272296DF46D00B5B695 /* NavigationStackBindingTestCase.swift */,
95+
CAA1CAF4296DEE78000665B1 /* IntegrationApp.swift */,
8896
CAA1CAF8296DEE79000665B1 /* Assets.xcassets */,
8997
CAA1CAFA296DEE79000665B1 /* Preview Content */,
9098
);
@@ -102,6 +110,7 @@
102110
CAA1CB0E296DEE79000665B1 /* IntegrationUITests */ = {
103111
isa = PBXGroup;
104112
children = (
113+
E9919D3F296E3EF400C8716B /* EscapedWithViewStoreTests.swift */,
105114
CAA1CB0F296DEE79000665B1 /* ForEachBindingTests.swift */,
106115
CA595274296DF55A00B5B695 /* NavigationStackBindingTests.swift */,
107116
);
@@ -220,7 +229,9 @@
220229
buildActionMask = 2147483647;
221230
files = (
222231
CAA1CB1F296DEEAC000665B1 /* ForEachBindingTestCase.swift in Sources */,
232+
E9919D42296E47A400C8716B /* BindingsAnimationsTestBench.swift in Sources */,
223233
CA595273296DF46D00B5B695 /* NavigationStackBindingTestCase.swift in Sources */,
234+
E9919D3E296E28C800C8716B /* EscapedWithViewStoreTestCase.swift in Sources */,
224235
CAA1CAF5296DEE78000665B1 /* IntegrationApp.swift in Sources */,
225236
);
226237
runOnlyForDeploymentPostprocessing = 0;
@@ -229,6 +240,7 @@
229240
isa = PBXSourcesBuildPhase;
230241
buildActionMask = 2147483647;
231242
files = (
243+
E9919D40296E3EF400C8716B /* EscapedWithViewStoreTests.swift in Sources */,
232244
CA595275296DF55A00B5B695 /* NavigationStackBindingTests.swift in Sources */,
233245
CAA1CB10296DEE79000665B1 /* ForEachBindingTests.swift in Sources */,
234246
);
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import ComposableArchitecture
2+
import SwiftUI
3+
4+
struct BindingsAnimations: ReducerProtocol {
5+
func reduce(into state: inout Bool, action: Void) -> EffectTask<Void> {
6+
state.toggle()
7+
return .none
8+
}
9+
}
10+
11+
final class VanillaModel: ObservableObject {
12+
@Published var flag = false
13+
}
14+
15+
let mediumAnimation = Animation.linear(duration: 0.7)
16+
let fastAnimation = Animation.linear(duration: 0.2)
17+
18+
struct BindingsAnimationsTestBench: View {
19+
let viewStore: ViewStoreOf<BindingsAnimations>
20+
let vanillaModel = VanillaModel()
21+
22+
init(store: StoreOf<BindingsAnimations>) {
23+
self.viewStore = ViewStore(store, observe: { $0 })
24+
}
25+
26+
var body: some View {
27+
List {
28+
Section {
29+
SideBySide {
30+
AnimatedWithObservation.ObservedObjectBinding()
31+
} viewStoreView: {
32+
AnimatedWithObservation.ViewStoreBinding()
33+
}
34+
} header: {
35+
Text("Animated with observation.")
36+
} footer: {
37+
Text("Should animate with the \"medium\" animation.")
38+
}
39+
40+
Section {
41+
SideBySide {
42+
AnimatedFromBinding.ObservedObjectBinding()
43+
} viewStoreView: {
44+
AnimatedFromBinding.ViewStoreBinding()
45+
}
46+
} header: {
47+
Text("Animated from binding.")
48+
} footer: {
49+
Text("Should animate with the \"fast\" animation.")
50+
}
51+
52+
Section {
53+
SideBySide {
54+
AnimatedFromBindingWithObservation.ObservedObjectBinding()
55+
} viewStoreView: {
56+
AnimatedFromBindingWithObservation.ViewStoreBinding()
57+
}
58+
} header: {
59+
Text("Animated from binding with observation.")
60+
} footer: {
61+
Text("Should animate with the \"medium\" animation.")
62+
}
63+
}
64+
.headerProminence(.increased)
65+
.environmentObject(viewStore)
66+
.environmentObject(vanillaModel)
67+
}
68+
}
69+
70+
struct SideBySide<ObservedObjectView: View, ViewStoreView: View>: View {
71+
let observedObjectView: ObservedObjectView
72+
let viewStoreView: ViewStoreView
73+
init(
74+
@ViewBuilder observedObjectView: () -> ObservedObjectView,
75+
@ViewBuilder viewStoreView: () -> ViewStoreView
76+
) {
77+
self.observedObjectView = observedObjectView()
78+
self.viewStoreView = viewStoreView()
79+
}
80+
var body: some View {
81+
Grid {
82+
GridRow {
83+
observedObjectView
84+
.frame(width: 100, height: 100)
85+
viewStoreView
86+
.frame(width: 100, height: 100)
87+
}
88+
.labelsHidden()
89+
GridRow {
90+
Text("@ObservedObject")
91+
.fixedSize()
92+
Text("ViewStore")
93+
}
94+
.font(.footnote.bold())
95+
.monospaced()
96+
}
97+
.frame(maxWidth: .infinity)
98+
}
99+
}
100+
101+
struct ContentView: View {
102+
@Binding var flag: Bool
103+
104+
var body: some View {
105+
ZStack {
106+
Circle()
107+
.fill(.red.opacity(0.25))
108+
Circle()
109+
.strokeBorder(.red.opacity(0.5), lineWidth: 2)
110+
}
111+
.frame(width: flag ? 100 : 75)
112+
}
113+
}
114+
115+
struct AnimatedWithObservation {
116+
struct ObservedObjectBinding: View {
117+
@EnvironmentObject var vanillaModel: VanillaModel
118+
var body: some View {
119+
ZStack {
120+
ContentView(flag: $vanillaModel.flag)
121+
.animation(mediumAnimation, value: vanillaModel.flag)
122+
Toggle("", isOn: $vanillaModel.flag)
123+
}
124+
}
125+
}
126+
127+
struct ViewStoreBinding: View {
128+
@EnvironmentObject var viewStore: ViewStoreOf<BindingsAnimations>
129+
var body: some View {
130+
ZStack {
131+
ContentView(flag: viewStore.binding(send: ()))
132+
.animation(mediumAnimation, value: viewStore.state)
133+
Toggle("", isOn: viewStore.binding(send: ()))
134+
}
135+
}
136+
}
137+
}
138+
139+
struct AnimatedFromBinding {
140+
struct ObservedObjectBinding: View {
141+
@EnvironmentObject var vanillaModel: VanillaModel
142+
var body: some View {
143+
ZStack {
144+
ContentView(flag: $vanillaModel.flag)
145+
Toggle("", isOn: $vanillaModel.flag.animation(fastAnimation))
146+
}
147+
}
148+
}
149+
150+
struct ViewStoreBinding: View {
151+
@EnvironmentObject var viewStore: ViewStoreOf<BindingsAnimations>
152+
var body: some View {
153+
ZStack {
154+
ContentView(flag: viewStore.binding(send: ()))
155+
Toggle("", isOn: viewStore.binding(send: ()).animation(fastAnimation))
156+
}
157+
}
158+
}
159+
}
160+
161+
struct AnimatedFromBindingWithObservation {
162+
struct ObservedObjectBinding: View {
163+
@EnvironmentObject var vanillaModel: VanillaModel
164+
var body: some View {
165+
ZStack {
166+
ContentView(flag: $vanillaModel.flag)
167+
.animation(mediumAnimation, value: vanillaModel.flag)
168+
Toggle("", isOn: $vanillaModel.flag.animation(fastAnimation))
169+
}
170+
}
171+
}
172+
173+
struct ViewStoreBinding: View {
174+
@EnvironmentObject var viewStore: ViewStoreOf<BindingsAnimations>
175+
var body: some View {
176+
ZStack {
177+
ContentView(flag: viewStore.binding(send: ()))
178+
.animation(mediumAnimation, value: viewStore.state)
179+
Toggle("", isOn: viewStore.binding(send: ()).animation(fastAnimation))
180+
}
181+
}
182+
}
183+
}
184+
185+
struct BindingsAnimationsTestBench_Previews: PreviewProvider {
186+
static var previews: some View {
187+
BindingsAnimationsTestBench(
188+
store: .init(
189+
initialState: false,
190+
reducer: BindingsAnimations()
191+
)
192+
)
193+
}
194+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import ComposableArchitecture
2+
import SwiftUI
3+
4+
struct EscapedWithViewStoreTestCase: ReducerProtocol {
5+
enum Action: Equatable, Sendable {
6+
case incr
7+
case decr
8+
}
9+
10+
func reduce(into state: inout Int, action: Action) -> EffectTask<Action> {
11+
switch action {
12+
case .incr:
13+
state += 1
14+
return .none
15+
case .decr:
16+
state -= 1
17+
return .none
18+
}
19+
}
20+
}
21+
22+
struct EscapedWithViewStoreTestCaseView: View {
23+
let store: StoreOf<EscapedWithViewStoreTestCase>
24+
25+
var body: some View {
26+
VStack {
27+
WithViewStore(store, observe: { $0 }) { viewStore in
28+
GeometryReader { proxy in
29+
Text("\(viewStore.state)")
30+
.accessibilityValue("\(viewStore.state)")
31+
.accessibilityLabel("EscapedLabel")
32+
}
33+
Button("Button", action: { viewStore.send(.incr) })
34+
Text("\(viewStore.state)")
35+
.accessibilityValue("\(viewStore.state)")
36+
.accessibilityLabel("Label")
37+
Stepper {
38+
Text("Stepper")
39+
} onIncrement: {
40+
viewStore.send(.incr)
41+
} onDecrement: {
42+
viewStore.send(.decr)
43+
}
44+
}
45+
}
46+
}
47+
}

Examples/Integration/Integration/IntegrationApp.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ struct IntegrationApp: App {
99
WindowGroup {
1010
NavigationStack {
1111
List {
12+
NavigationLink("EscapedWithViewStoreTestCase") {
13+
EscapedWithViewStoreTestCaseView(
14+
store: Store(
15+
initialState: 10,
16+
reducer: EscapedWithViewStoreTestCase()
17+
)
18+
)
19+
}
1220
NavigationLink("ForEachBindingTestCase") {
1321
ForEachBindingTestCaseView(
1422
store: Store(
@@ -29,6 +37,15 @@ struct IntegrationApp: App {
2937
)
3038
)
3139
}
40+
41+
NavigationLink("Binding Animations Test Bench") {
42+
BindingsAnimationsTestBench(
43+
store: Store(
44+
initialState: false,
45+
reducer: BindingsAnimations()
46+
)
47+
)
48+
}
3249
}
3350
}
3451
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import XCTest
2+
3+
@MainActor
4+
final class EscapedWithViewStoreTests: XCTestCase {
5+
6+
override func setUpWithError() throws {
7+
continueAfterFailure = false
8+
}
9+
10+
func testExample() async throws {
11+
let app = XCUIApplication()
12+
app.launch()
13+
14+
app.collectionViews.buttons["EscapedWithViewStoreTestCase"].tap()
15+
16+
XCTAssertEqual(app.staticTexts["Label"].value as? String, "10")
17+
XCTAssertEqual(app.staticTexts["EscapedLabel"].value as? String, "10")
18+
19+
app.buttons["Button"].tap()
20+
21+
XCTAssertEqual(app.staticTexts["Label"].value as? String, "11")
22+
XCTAssertEqual(app.staticTexts["EscapedLabel"].value as? String, "11")
23+
24+
let stepper = app.steppers["Stepper"]
25+
26+
stepper.buttons["Increment"].tap()
27+
stepper.buttons["Increment"].tap()
28+
stepper.buttons["Increment"].tap()
29+
stepper.buttons["Increment"].tap()
30+
31+
XCTAssertEqual(app.staticTexts["Label"].value as? String, "15")
32+
XCTAssertEqual(app.staticTexts["EscapedLabel"].value as? String, "15")
33+
34+
stepper.buttons["Decrement"].tap()
35+
stepper.buttons["Decrement"].tap()
36+
stepper.buttons["Decrement"].tap()
37+
38+
XCTAssertEqual(app.staticTexts["Label"].value as? String, "12")
39+
XCTAssertEqual(app.staticTexts["EscapedLabel"].value as? String, "12")
40+
}
41+
}

0 commit comments

Comments
 (0)