Skip to content

Commit ba785e8

Browse files
Case study for tvOS focus. (#226)
* Case study for tvOS focus. * clean up * clean up * fix ios 13 * move random to env Co-authored-by: Stephen Celis <[email protected]>
1 parent 24d45b4 commit ba785e8

File tree

19 files changed

+609
-3
lines changed

19 files changed

+609
-3
lines changed

Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj

Lines changed: 258 additions & 3 deletions
Large diffs are not rendered by default.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import ComposableArchitecture
2+
import SwiftUI
3+
import UIKit
4+
5+
@UIApplicationMain
6+
class AppDelegate: UIResponder, UIApplicationDelegate {
7+
var window: UIWindow?
8+
9+
func application(
10+
_ application: UIApplication,
11+
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
12+
) -> Bool {
13+
let contentView = RootView(
14+
store: Store(
15+
initialState: .init(),
16+
reducer: rootReducer,
17+
environment: .init()
18+
)
19+
)
20+
21+
let window = UIWindow(frame: UIScreen.main.bounds)
22+
window.rootViewController = UIHostingController(rootView: contentView)
23+
self.window = window
24+
window.makeKeyAndVisible()
25+
return true
26+
}
27+
}
7.96 KB
Loading
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
{
2+
"images" : [
3+
{
4+
"idiom" : "iphone",
5+
"scale" : "2x",
6+
"size" : "20x20"
7+
},
8+
{
9+
"idiom" : "iphone",
10+
"scale" : "3x",
11+
"size" : "20x20"
12+
},
13+
{
14+
"idiom" : "iphone",
15+
"scale" : "2x",
16+
"size" : "29x29"
17+
},
18+
{
19+
"idiom" : "iphone",
20+
"scale" : "3x",
21+
"size" : "29x29"
22+
},
23+
{
24+
"idiom" : "iphone",
25+
"scale" : "2x",
26+
"size" : "40x40"
27+
},
28+
{
29+
"idiom" : "iphone",
30+
"scale" : "3x",
31+
"size" : "40x40"
32+
},
33+
{
34+
"idiom" : "iphone",
35+
"scale" : "2x",
36+
"size" : "60x60"
37+
},
38+
{
39+
"filename" : "AppIcon.png",
40+
"idiom" : "iphone",
41+
"scale" : "3x",
42+
"size" : "60x60"
43+
},
44+
{
45+
"idiom" : "ipad",
46+
"scale" : "1x",
47+
"size" : "20x20"
48+
},
49+
{
50+
"idiom" : "ipad",
51+
"scale" : "2x",
52+
"size" : "20x20"
53+
},
54+
{
55+
"idiom" : "ipad",
56+
"scale" : "1x",
57+
"size" : "29x29"
58+
},
59+
{
60+
"idiom" : "ipad",
61+
"scale" : "2x",
62+
"size" : "29x29"
63+
},
64+
{
65+
"idiom" : "ipad",
66+
"scale" : "1x",
67+
"size" : "40x40"
68+
},
69+
{
70+
"idiom" : "ipad",
71+
"scale" : "2x",
72+
"size" : "40x40"
73+
},
74+
{
75+
"idiom" : "ipad",
76+
"scale" : "1x",
77+
"size" : "76x76"
78+
},
79+
{
80+
"idiom" : "ipad",
81+
"scale" : "2x",
82+
"size" : "76x76"
83+
},
84+
{
85+
"idiom" : "ipad",
86+
"scale" : "2x",
87+
"size" : "83.5x83.5"
88+
},
89+
{
90+
"idiom" : "ios-marketing",
91+
"scale" : "1x",
92+
"size" : "1024x1024"
93+
}
94+
],
95+
"info" : {
96+
"author" : "xcode",
97+
"version" : 1
98+
}
99+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"info" : {
3+
"author" : "xcode",
4+
"version" : 1
5+
}
6+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import ComposableArchitecture
2+
3+
struct RootState {
4+
var focus = FocusState()
5+
}
6+
7+
enum RootAction {
8+
case focus(FocusAction)
9+
}
10+
11+
struct RootEnvironment {
12+
var focus = FocusEnvironment()
13+
}
14+
15+
let rootReducer = Reducer<RootState, RootAction, RootEnvironment>.combine(
16+
focusReducer.pullback(
17+
state: \.focus,
18+
action: /RootAction.focus,
19+
environment: { $0.focus }
20+
)
21+
)
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import ComposableArchitecture
2+
import SwiftUI
3+
4+
private let readMe = """
5+
This demonstrates how to programmatically control focus in a tvOS app using the Composable \
6+
Architecture.
7+
8+
The current focus can be held in the feature's state, and then the view must listen to changes \
9+
to that value, via the .onChange view modifier, in order to tell the view's ResetFocusAction \
10+
to reset its focus.
11+
"""
12+
13+
struct FocusState: Equatable {
14+
var currentFocus = 1
15+
}
16+
17+
enum FocusAction {
18+
case randomButtonClicked
19+
}
20+
21+
struct FocusEnvironment {
22+
var randomElement: () -> Int = { (1..<11).randomElement()! }
23+
}
24+
25+
let focusReducer = Reducer<FocusState, FocusAction, FocusEnvironment> { state, action, environment in
26+
switch action {
27+
case .randomButtonClicked:
28+
state.currentFocus = environment.randomElement()
29+
return .none
30+
}
31+
}
32+
33+
#if swift(>=5.3)
34+
@available(tvOS 14.0, *)
35+
struct FocusView: View {
36+
let store: Store<FocusState, FocusAction>
37+
38+
@Environment(\.resetFocus) var resetFocus
39+
@Namespace private var namespace
40+
41+
var body: some View {
42+
WithViewStore(self.store) { viewStore in
43+
VStack(spacing: 100) {
44+
Text(readMe)
45+
.font(.headline)
46+
.multilineTextAlignment(.leading)
47+
.padding()
48+
49+
HStack(spacing: 40) {
50+
ForEach(1..<11) { index in
51+
Button(numbers[index]) {}
52+
.prefersDefaultFocus(viewStore.currentFocus == index, in: self.namespace)
53+
}
54+
}
55+
56+
Button("Focus Random") { viewStore.send(.randomButtonClicked) }
57+
}
58+
.onChange(of: viewStore.currentFocus) { _ in
59+
// Update the view's focus when the state tells us the focus changed.
60+
self.resetFocus(in: self.namespace)
61+
}
62+
.focusScope(self.namespace)
63+
}
64+
}
65+
}
66+
67+
@available(tvOS 14.0, *)
68+
struct FocusView_Previews: PreviewProvider {
69+
static var previews: some View {
70+
FocusView(
71+
store: .init(
72+
initialState: .init(),
73+
reducer: focusReducer,
74+
environment: .init()
75+
)
76+
)
77+
}
78+
}
79+
#endif
80+
81+
private let numbers = [
82+
"Zero",
83+
"One",
84+
"Two",
85+
"Three",
86+
"Four",
87+
"Five",
88+
"Six",
89+
"Seven",
90+
"Eight",
91+
"Nine",
92+
"Ten"
93+
]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleDevelopmentRegion</key>
6+
<string>$(DEVELOPMENT_LANGUAGE)</string>
7+
<key>CFBundleExecutable</key>
8+
<string>$(EXECUTABLE_NAME)</string>
9+
<key>CFBundleIdentifier</key>
10+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11+
<key>CFBundleInfoDictionaryVersion</key>
12+
<string>6.0</string>
13+
<key>CFBundleName</key>
14+
<string>$(PRODUCT_NAME)</string>
15+
<key>CFBundlePackageType</key>
16+
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
17+
<key>CFBundleShortVersionString</key>
18+
<string>1.0</string>
19+
<key>CFBundleVersion</key>
20+
<string>1</string>
21+
<key>LSRequiresIPhoneOS</key>
22+
<true/>
23+
<key>UILaunchStoryboardName</key>
24+
<string>LaunchScreen</string>
25+
<key>UIRequiredDeviceCapabilities</key>
26+
<array>
27+
<string>arm64</string>
28+
</array>
29+
<key>UIUserInterfaceStyle</key>
30+
<string>Automatic</string>
31+
</dict>
32+
</plist>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import ComposableArchitecture
2+
import SwiftUI
3+
4+
struct RootView: View {
5+
let store: Store<RootState, RootAction>
6+
7+
var body: some View {
8+
NavigationView {
9+
Form {
10+
Section {
11+
self.focusView
12+
}
13+
}
14+
}
15+
}
16+
17+
var focusView: AnyView? {
18+
if #available(tvOS 14.0, *) {
19+
#if swift(>=5.3)
20+
return AnyView(
21+
NavigationLink(
22+
destination: FocusView(
23+
store: self.store.scope(state: { $0.focus }, action: RootAction.focus)
24+
),
25+
label: {
26+
Text("Focus")
27+
})
28+
)
29+
#else
30+
return nil
31+
#endif
32+
} else {
33+
return nil
34+
}
35+
}
36+
}
37+
38+
struct ContentView_Previews: PreviewProvider {
39+
static var previews: some View {
40+
NavigationView {
41+
RootView(
42+
store: Store(
43+
initialState: .init(),
44+
reducer: rootReducer,
45+
environment: .init()
46+
)
47+
)
48+
}
49+
}
50+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import XCTest
2+
@testable import tvOSCaseStudies
3+
4+
class tvOSCaseStudiesTests: XCTestCase {
5+
}

0 commit comments

Comments
 (0)