Skip to content

Commit 1f4be05

Browse files
committed
feat(brownie): add UIKit store subscription support
1 parent f8aeecb commit 1f4be05

4 files changed

Lines changed: 168 additions & 8 deletions

File tree

apps/TesterIntegrated/swift/App.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Brownie
22
import ReactBrownfield
33
import SwiftUI
4+
import UIKit
45

56
let initialState = BrownfieldStore(
67
counter: 0,
@@ -83,6 +84,11 @@ struct MyApp: App {
8384
ReactNativeView(moduleName: "ReactNative")
8485
.navigationBarHidden(true)
8586
}
87+
88+
NavigationLink("Push UIKit Screen") {
89+
UIKitExampleViewControllerRepresentable()
90+
.navigationBarTitleDisplayMode(.inline)
91+
}
8692
}
8793
}.navigationViewStyle(StackNavigationViewStyle())
8894
}
@@ -118,3 +124,11 @@ struct MyApp: App {
118124
}
119125
}
120126
}
127+
128+
struct UIKitExampleViewControllerRepresentable: UIViewControllerRepresentable {
129+
func makeUIViewController(context: Context) -> UIKitExampleViewController {
130+
UIKitExampleViewController()
131+
}
132+
133+
func updateUIViewController(_ uiViewController: UIKitExampleViewController, context: Context) {}
134+
}

apps/TesterIntegrated/swift/SwiftExample.xcodeproj/project.pbxproj

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
2869185C23129ECF00458242 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2869185B23129ECF00458242 /* App.swift */; };
1111
2869186323129ED100458242 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2869186223129ED100458242 /* Assets.xcassets */; };
1212
2869186623129ED100458242 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2869186423129ED100458242 /* LaunchScreen.storyboard */; };
13+
76208ADC2F11557800737E1D /* UIKitExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76208ADB2F11557800737E1D /* UIKitExampleViewController.swift */; };
1314
AC7E040409DFEE553311B27E /* libPods-SwiftExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CF9653D1882655DA02862F71 /* libPods-SwiftExample.a */; };
1415
BFSTORE001000000000000001 /* BrownfieldStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFSTORE001000000000000002 /* BrownfieldStore.swift */; };
1516
ED52AE6E1313AAE3C968412D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 33BE347ABB4EE17F7F99D32D /* PrivacyInfo.xcprivacy */; };
@@ -23,6 +24,7 @@
2324
2869186723129ED100458242 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
2425
33BE347ABB4EE17F7F99D32D /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
2526
5BE9069F7C8AF5853F650B68 /* Pods-SwiftExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftExample.debug.xcconfig"; path = "Target Support Files/Pods-SwiftExample/Pods-SwiftExample.debug.xcconfig"; sourceTree = "<group>"; };
27+
76208ADB2F11557800737E1D /* UIKitExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitExampleViewController.swift; sourceTree = "<group>"; };
2628
BFSTORE001000000000000002 /* BrownfieldStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrownfieldStore.swift; sourceTree = "<group>"; };
2729
CF9653D1882655DA02862F71 /* libPods-SwiftExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SwiftExample.a"; sourceTree = BUILT_PRODUCTS_DIR; };
2830
E74DC3AFAC0967529ED1C063 /* Pods-SwiftExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftExample.release.xcconfig"; path = "Target Support Files/Pods-SwiftExample/Pods-SwiftExample.release.xcconfig"; sourceTree = "<group>"; };
@@ -43,6 +45,7 @@
4345
2869184F23129ECF00458242 = {
4446
isa = PBXGroup;
4547
children = (
48+
76208ADB2F11557800737E1D /* UIKitExampleViewController.swift */,
4649
2869185B23129ECF00458242 /* App.swift */,
4750
BFSTORE001000000000000003 /* Generated */,
4851
2869186223129ED100458242 /* Assets.xcassets */,
@@ -168,10 +171,14 @@
168171
inputFileListPaths = (
169172
"${PODS_ROOT}/Target Support Files/Pods-SwiftExample/Pods-SwiftExample-frameworks-${CONFIGURATION}-input-files.xcfilelist",
170173
);
174+
inputPaths = (
175+
);
171176
name = "[CP] Embed Pods Frameworks";
172177
outputFileListPaths = (
173178
"${PODS_ROOT}/Target Support Files/Pods-SwiftExample/Pods-SwiftExample-frameworks-${CONFIGURATION}-output-files.xcfilelist",
174179
);
180+
outputPaths = (
181+
);
175182
runOnlyForDeploymentPostprocessing = 0;
176183
shellPath = /bin/sh;
177184
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SwiftExample/Pods-SwiftExample-frameworks.sh\"\n";
@@ -243,10 +250,14 @@
243250
inputFileListPaths = (
244251
"${PODS_ROOT}/Target Support Files/Pods-SwiftExample/Pods-SwiftExample-resources-${CONFIGURATION}-input-files.xcfilelist",
245252
);
253+
inputPaths = (
254+
);
246255
name = "[CP] Copy Pods Resources";
247256
outputFileListPaths = (
248257
"${PODS_ROOT}/Target Support Files/Pods-SwiftExample/Pods-SwiftExample-resources-${CONFIGURATION}-output-files.xcfilelist",
249258
);
259+
outputPaths = (
260+
);
250261
runOnlyForDeploymentPostprocessing = 0;
251262
shellPath = /bin/sh;
252263
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SwiftExample/Pods-SwiftExample-resources.sh\"\n";
@@ -259,6 +270,7 @@
259270
isa = PBXSourcesBuildPhase;
260271
buildActionMask = 2147483647;
261272
files = (
273+
76208ADC2F11557800737E1D /* UIKitExampleViewController.swift in Sources */,
262274
2869185C23129ECF00458242 /* App.swift in Sources */,
263275
BFSTORE001000000000000001 /* BrownfieldStore.swift in Sources */,
264276
);
@@ -340,10 +352,7 @@
340352
ONLY_ACTIVE_ARCH = YES;
341353
OTHER_CFLAGS = "$(inherited)";
342354
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
343-
OTHER_LDFLAGS = (
344-
"$(inherited)",
345-
" ",
346-
);
355+
OTHER_LDFLAGS = "$(inherited) ";
347356
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
348357
SDKROOT = iphoneos;
349358
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
@@ -407,10 +416,7 @@
407416
MTL_FAST_MATH = YES;
408417
OTHER_CFLAGS = "$(inherited)";
409418
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
410-
OTHER_LDFLAGS = (
411-
"$(inherited)",
412-
" ",
413-
);
419+
OTHER_LDFLAGS = "$(inherited) ";
414420
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
415421
SDKROOT = iphoneos;
416422
SWIFT_COMPILATION_MODE = wholemodule;
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import UIKit
2+
import Brownie
3+
4+
class UIKitExampleViewController: UIViewController {
5+
private var store: Store<BrownfieldStore>?
6+
private var cancelSubscription: (() -> Void)?
7+
8+
private let counterLabel: UILabel = {
9+
let label = UILabel()
10+
label.font = .systemFont(ofSize: 24, weight: .bold)
11+
label.textAlignment = .center
12+
label.translatesAutoresizingMaskIntoConstraints = false
13+
return label
14+
}()
15+
16+
private let userLabel: UILabel = {
17+
let label = UILabel()
18+
label.font = .systemFont(ofSize: 18)
19+
label.textAlignment = .center
20+
label.translatesAutoresizingMaskIntoConstraints = false
21+
return label
22+
}()
23+
24+
private let nameTextField: UITextField = {
25+
let field = UITextField()
26+
field.borderStyle = .roundedRect
27+
field.placeholder = "Enter name"
28+
field.translatesAutoresizingMaskIntoConstraints = false
29+
return field
30+
}()
31+
32+
private let incrementButton: UIButton = {
33+
var config = UIButton.Configuration.borderedProminent()
34+
config.title = "Increment"
35+
let button = UIButton(configuration: config)
36+
button.translatesAutoresizingMaskIntoConstraints = false
37+
return button
38+
}()
39+
40+
override func viewDidLoad() {
41+
super.viewDidLoad()
42+
setupUI()
43+
setupStore()
44+
}
45+
46+
private func setupUI() {
47+
title = "UIKit Example"
48+
view.backgroundColor = .systemBackground
49+
50+
let titleLabel = UILabel()
51+
titleLabel.text = "UIKit + Brownie Store"
52+
titleLabel.font = .systemFont(ofSize: 20, weight: .bold)
53+
titleLabel.textAlignment = .center
54+
titleLabel.translatesAutoresizingMaskIntoConstraints = false
55+
56+
let stack = UIStackView(arrangedSubviews: [
57+
titleLabel,
58+
counterLabel,
59+
userLabel,
60+
nameTextField,
61+
incrementButton
62+
])
63+
stack.axis = .vertical
64+
stack.spacing = 16
65+
stack.translatesAutoresizingMaskIntoConstraints = false
66+
67+
view.addSubview(stack)
68+
69+
NSLayoutConstraint.activate([
70+
stack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
71+
stack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
72+
stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
73+
stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
74+
])
75+
76+
incrementButton.addTarget(self, action: #selector(incrementTapped), for: .touchUpInside)
77+
nameTextField.addTarget(self, action: #selector(nameChanged), for: .editingChanged)
78+
}
79+
80+
private func setupStore() {
81+
store = StoreManager.get(key: BrownfieldStore.storeName, as: BrownfieldStore.self)
82+
83+
guard let store else {
84+
counterLabel.text = "Store not found"
85+
return
86+
}
87+
88+
updateUI(with: store.state)
89+
90+
cancelSubscription = store.subscribe { [weak self] state in
91+
self?.updateUI(with: state)
92+
}
93+
}
94+
95+
private func updateUI(with state: BrownfieldStore) {
96+
counterLabel.text = "Count: \(Int(state.counter))"
97+
userLabel.text = "User: \(state.user.name)"
98+
99+
if nameTextField.text != state.user.name && !nameTextField.isFirstResponder {
100+
nameTextField.text = state.user.name
101+
}
102+
}
103+
104+
@objc private func incrementTapped() {
105+
store?.set { $0.counter += 1 }
106+
}
107+
108+
@objc private func nameChanged() {
109+
guard let name = nameTextField.text else { return }
110+
store?.set { $0.user.name = name }
111+
}
112+
113+
deinit {
114+
cancelSubscription?()
115+
}
116+
}

packages/brownie/ios/BrownieStore.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,28 @@ public class Store<State: Codable>: ObservableObject {
148148
public subscript<Value>(_ keyPath: KeyPath<State, Value>) -> Value {
149149
state[keyPath: keyPath]
150150
}
151+
152+
// MARK: - UIKit Support
153+
154+
/// Subscribe to state changes with a closure. Returns a cancellation function.
155+
public func subscribe(onChange: @escaping (State) -> Void) -> () -> Void {
156+
let cancellable = $state.sink { state in
157+
onChange(state)
158+
}
159+
return { cancellable.cancel() }
160+
}
161+
162+
/// Subscribe to specific property changes. Returns a cancellation function.
163+
public func subscribe<Value: Equatable>(
164+
_ keyPath: KeyPath<State, Value>,
165+
onChange: @escaping (Value) -> Void
166+
) -> () -> Void {
167+
let cancellable = $state
168+
.map { $0[keyPath: keyPath] }
169+
.removeDuplicates()
170+
.sink { value in
171+
onChange(value)
172+
}
173+
return { cancellable.cancel() }
174+
}
151175
}

0 commit comments

Comments
 (0)