Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions apps/TesterIntegrated/swift/App.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Brownie
import ReactBrownfield
import SwiftUI
import UIKit

let initialState = BrownfieldStore(
counter: 0,
Expand Down Expand Up @@ -83,6 +84,11 @@ struct MyApp: App {
ReactNativeView(moduleName: "ReactNative")
.navigationBarHidden(true)
}

NavigationLink("Push UIKit Screen") {
UIKitExampleViewControllerRepresentable()
.navigationBarTitleDisplayMode(.inline)
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
Expand Down Expand Up @@ -118,3 +124,11 @@ struct MyApp: App {
}
}
}

struct UIKitExampleViewControllerRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIKitExampleViewController {
UIKitExampleViewController()
}

func updateUIViewController(_ uiViewController: UIKitExampleViewController, context: Context) {}
}
22 changes: 14 additions & 8 deletions apps/TesterIntegrated/swift/SwiftExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
2869185C23129ECF00458242 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2869185B23129ECF00458242 /* App.swift */; };
2869186323129ED100458242 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2869186223129ED100458242 /* Assets.xcassets */; };
2869186623129ED100458242 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2869186423129ED100458242 /* LaunchScreen.storyboard */; };
76208ADC2F11557800737E1D /* UIKitExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76208ADB2F11557800737E1D /* UIKitExampleViewController.swift */; };
AC7E040409DFEE553311B27E /* libPods-SwiftExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CF9653D1882655DA02862F71 /* libPods-SwiftExample.a */; };
BFSTORE001000000000000001 /* BrownfieldStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFSTORE001000000000000002 /* BrownfieldStore.swift */; };
ED52AE6E1313AAE3C968412D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 33BE347ABB4EE17F7F99D32D /* PrivacyInfo.xcprivacy */; };
Expand All @@ -23,6 +24,7 @@
2869186723129ED100458242 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
33BE347ABB4EE17F7F99D32D /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
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>"; };
76208ADB2F11557800737E1D /* UIKitExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitExampleViewController.swift; sourceTree = "<group>"; };
BFSTORE001000000000000002 /* BrownfieldStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrownfieldStore.swift; sourceTree = "<group>"; };
CF9653D1882655DA02862F71 /* libPods-SwiftExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-SwiftExample.a"; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
Expand All @@ -43,6 +45,7 @@
2869184F23129ECF00458242 = {
isa = PBXGroup;
children = (
76208ADB2F11557800737E1D /* UIKitExampleViewController.swift */,
2869185B23129ECF00458242 /* App.swift */,
BFSTORE001000000000000003 /* Generated */,
2869186223129ED100458242 /* Assets.xcassets */,
Expand Down Expand Up @@ -168,10 +171,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-SwiftExample/Pods-SwiftExample-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-SwiftExample/Pods-SwiftExample-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SwiftExample/Pods-SwiftExample-frameworks.sh\"\n";
Expand Down Expand Up @@ -243,10 +250,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-SwiftExample/Pods-SwiftExample-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-SwiftExample/Pods-SwiftExample-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SwiftExample/Pods-SwiftExample-resources.sh\"\n";
Expand All @@ -259,6 +270,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
76208ADC2F11557800737E1D /* UIKitExampleViewController.swift in Sources */,
2869185C23129ECF00458242 /* App.swift in Sources */,
BFSTORE001000000000000001 /* BrownfieldStore.swift in Sources */,
);
Expand Down Expand Up @@ -340,10 +352,7 @@
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
Expand Down Expand Up @@ -407,10 +416,7 @@
MTL_FAST_MATH = YES;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
Expand Down
116 changes: 116 additions & 0 deletions apps/TesterIntegrated/swift/UIKitExampleViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import UIKit
import Brownie

class UIKitExampleViewController: UIViewController {
private var store: Store<BrownfieldStore>?
private var cancelSubscription: (() -> Void)?

private let counterLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 24, weight: .bold)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()

private let userLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 18)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()

private let nameTextField: UITextField = {
let field = UITextField()
field.borderStyle = .roundedRect
field.placeholder = "Enter name"
field.translatesAutoresizingMaskIntoConstraints = false
return field
}()

private let incrementButton: UIButton = {
var config = UIButton.Configuration.borderedProminent()
config.title = "Increment"
let button = UIButton(configuration: config)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()

override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupStore()
}

private func setupUI() {
title = "UIKit Example"
view.backgroundColor = .systemBackground

let titleLabel = UILabel()
titleLabel.text = "UIKit + Brownie Store"
titleLabel.font = .systemFont(ofSize: 20, weight: .bold)
titleLabel.textAlignment = .center
titleLabel.translatesAutoresizingMaskIntoConstraints = false

let stack = UIStackView(arrangedSubviews: [
titleLabel,
counterLabel,
userLabel,
nameTextField,
incrementButton
])
stack.axis = .vertical
stack.spacing = 16
stack.translatesAutoresizingMaskIntoConstraints = false

view.addSubview(stack)

NSLayoutConstraint.activate([
stack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
])

incrementButton.addTarget(self, action: #selector(incrementTapped), for: .touchUpInside)
nameTextField.addTarget(self, action: #selector(nameChanged), for: .editingChanged)
}

private func setupStore() {
store = StoreManager.get(key: BrownfieldStore.storeName, as: BrownfieldStore.self)

guard let store else {
counterLabel.text = "Store not found"
return
}

updateUI(with: store.state)

cancelSubscription = store.subscribe { [weak self] state in
self?.updateUI(with: state)
}
}

private func updateUI(with state: BrownfieldStore) {
counterLabel.text = "Count: \(Int(state.counter))"
userLabel.text = "User: \(state.user.name)"

if nameTextField.text != state.user.name && !nameTextField.isFirstResponder {
nameTextField.text = state.user.name
}
}

@objc private func incrementTapped() {
store?.set { $0.counter += 1 }
}

@objc private func nameChanged() {
guard let name = nameTextField.text else { return }
store?.set { $0.user.name = name }
}

deinit {
cancelSubscription?()
}
}
24 changes: 24 additions & 0 deletions packages/brownie/ios/BrownieStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,28 @@ public class Store<State: Codable>: ObservableObject {
public subscript<Value>(_ keyPath: KeyPath<State, Value>) -> Value {
state[keyPath: keyPath]
}

// MARK: - UIKit Support

/// Subscribe to state changes with a closure. Returns a cancellation function.
public func subscribe(onChange: @escaping (State) -> Void) -> () -> Void {
let cancellable = $state.sink { state in
onChange(state)
}
return { cancellable.cancel() }
}

/// Subscribe to specific property changes. Returns a cancellation function.
public func subscribe<Value: Equatable>(
_ keyPath: KeyPath<State, Value>,
onChange: @escaping (Value) -> Void
) -> () -> Void {
let cancellable = $state
.map { $0[keyPath: keyPath] }
.removeDuplicates()
.sink { value in
onChange(value)
}
return { cancellable.cancel() }
}
}