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
123 changes: 16 additions & 107 deletions Example/AsyncReactorExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,30 @@
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objectVersion = 70;
objects = {

/* Begin PBXBuildFile section */
809CDDE62A1B959D001D17BE /* Repository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 809CDDE52A1B959D001D17BE /* Repository.swift */; };
80D30FBE2A1BBD8000C653B0 /* RepositoryDetailReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D30FBD2A1BBD8000C653B0 /* RepositoryDetailReactor.swift */; };
80D30FC22A1BBF9A00C653B0 /* RepositoryList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D30FC12A1BBF9A00C653B0 /* RepositoryList.swift */; };
80D30FC42A1BC07B00C653B0 /* RepositoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D30FC32A1BC07B00C653B0 /* RepositoryItem.swift */; };
80D30FC62A1CDBC200C653B0 /* RepositoryDescriptionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D30FC52A1CDBC200C653B0 /* RepositoryDescriptionSheet.swift */; };
80E8B9D12A1BAE2800E4B3CC /* RepositorySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E8B9D02A1BAE2800E4B3CC /* RepositorySearchView.swift */; };
80E8B9D32A1BAEC200E4B3CC /* RepositoryDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E8B9D22A1BAEC200E4B3CC /* RepositoryDetailView.swift */; };
B4ECB0B02A1250AE00B0CAAE /* AsyncReactorExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4ECB0AF2A1250AE00B0CAAE /* AsyncReactorExampleApp.swift */; };
B4ECB0B22A1250AE00B0CAAE /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4ECB0B12A1250AE00B0CAAE /* ContentView.swift */; };
B4ECB0B42A1250AF00B0CAAE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B4ECB0B32A1250AF00B0CAAE /* Assets.xcassets */; };
B4ECB0B82A1250AF00B0CAAE /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B4ECB0B72A1250AF00B0CAAE /* Preview Assets.xcassets */; };
B4A9DDD22CF23EF2008CA694 /* Reactor in Frameworks */ = {isa = PBXBuildFile; productRef = B4A9DDD12CF23EF2008CA694 /* Reactor */; };
B4ECB0C12A1250E000B0CAAE /* AsyncReactor in Frameworks */ = {isa = PBXBuildFile; productRef = B4ECB0C02A1250E000B0CAAE /* AsyncReactor */; };
B4ECB0C42A1251B500B0CAAE /* RepositorySearchReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4ECB0C22A1251B500B0CAAE /* RepositorySearchReactor.swift */; };
B4ECB0C82A12520700B0CAAE /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = B4ECB0C72A12520700B0CAAE /* Logging */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
809CDDE52A1B959D001D17BE /* Repository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Repository.swift; sourceTree = "<group>"; };
80D30FBD2A1BBD8000C653B0 /* RepositoryDetailReactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryDetailReactor.swift; sourceTree = "<group>"; };
80D30FC12A1BBF9A00C653B0 /* RepositoryList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryList.swift; sourceTree = "<group>"; };
80D30FC32A1BC07B00C653B0 /* RepositoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryItem.swift; sourceTree = "<group>"; };
80D30FC52A1CDBC200C653B0 /* RepositoryDescriptionSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryDescriptionSheet.swift; sourceTree = "<group>"; };
80E8B9D02A1BAE2800E4B3CC /* RepositorySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositorySearchView.swift; sourceTree = "<group>"; };
80E8B9D22A1BAEC200E4B3CC /* RepositoryDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryDetailView.swift; sourceTree = "<group>"; };
B4ECB0AC2A1250AE00B0CAAE /* AsyncReactorExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AsyncReactorExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
B4ECB0AF2A1250AE00B0CAAE /* AsyncReactorExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncReactorExampleApp.swift; sourceTree = "<group>"; };
B4ECB0B12A1250AE00B0CAAE /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
B4ECB0B32A1250AF00B0CAAE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
B4ECB0B52A1250AF00B0CAAE /* AsyncReactorExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AsyncReactorExample.entitlements; sourceTree = "<group>"; };
B4ECB0B72A1250AF00B0CAAE /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
B4ECB0BE2A1250D700B0CAAE /* AsyncReactor */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AsyncReactor; path = ..; sourceTree = "<group>"; };
B4ECB0C22A1251B500B0CAAE /* RepositorySearchReactor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepositorySearchReactor.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
82F565F82D74885200C72CA5 /* AsyncReactorExample */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = AsyncReactorExample; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */

/* Begin PBXFrameworksBuildPhase section */
B4ECB0A92A1250AE00B0CAAE /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
B4A9DDD22CF23EF2008CA694 /* Reactor in Frameworks */,
B4ECB0C82A12520700B0CAAE /* Logging in Frameworks */,
B4ECB0C12A1250E000B0CAAE /* AsyncReactor in Frameworks */,
);
Expand All @@ -54,57 +35,11 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
809CDDE42A1B9591001D17BE /* Models */ = {
isa = PBXGroup;
children = (
809CDDE52A1B959D001D17BE /* Repository.swift */,
);
path = Models;
sourceTree = "<group>";
};
80E8B9CA2A1BAC2200E4B3CC /* Features */ = {
isa = PBXGroup;
children = (
80E8B9CB2A1BAC4900E4B3CC /* Repository */,
);
path = Features;
sourceTree = "<group>";
};
80E8B9CB2A1BAC4900E4B3CC /* Repository */ = {
isa = PBXGroup;
children = (
80E8B9D42A1BBB1D00E4B3CC /* Detail */,
80E8B9CD2A1BAC5F00E4B3CC /* Search */,
);
path = Repository;
sourceTree = "<group>";
};
80E8B9CD2A1BAC5F00E4B3CC /* Search */ = {
isa = PBXGroup;
children = (
B4ECB0C22A1251B500B0CAAE /* RepositorySearchReactor.swift */,
80E8B9D02A1BAE2800E4B3CC /* RepositorySearchView.swift */,
80D30FC12A1BBF9A00C653B0 /* RepositoryList.swift */,
80D30FC32A1BC07B00C653B0 /* RepositoryItem.swift */,
);
path = Search;
sourceTree = "<group>";
};
80E8B9D42A1BBB1D00E4B3CC /* Detail */ = {
isa = PBXGroup;
children = (
80E8B9D22A1BAEC200E4B3CC /* RepositoryDetailView.swift */,
80D30FBD2A1BBD8000C653B0 /* RepositoryDetailReactor.swift */,
80D30FC52A1CDBC200C653B0 /* RepositoryDescriptionSheet.swift */,
);
path = Detail;
sourceTree = "<group>";
};
B4ECB0A32A1250AE00B0CAAE = {
isa = PBXGroup;
children = (
B4ECB0BE2A1250D700B0CAAE /* AsyncReactor */,
B4ECB0AE2A1250AE00B0CAAE /* AsyncReactorExample */,
82F565F82D74885200C72CA5 /* AsyncReactorExample */,
B4ECB0AD2A1250AE00B0CAAE /* Products */,
B4ECB0BF2A1250E000B0CAAE /* Frameworks */,
);
Expand All @@ -118,28 +53,6 @@
name = Products;
sourceTree = "<group>";
};
B4ECB0AE2A1250AE00B0CAAE /* AsyncReactorExample */ = {
isa = PBXGroup;
children = (
80E8B9CA2A1BAC2200E4B3CC /* Features */,
809CDDE42A1B9591001D17BE /* Models */,
B4ECB0AF2A1250AE00B0CAAE /* AsyncReactorExampleApp.swift */,
B4ECB0B12A1250AE00B0CAAE /* ContentView.swift */,
B4ECB0B32A1250AF00B0CAAE /* Assets.xcassets */,
B4ECB0B52A1250AF00B0CAAE /* AsyncReactorExample.entitlements */,
B4ECB0B62A1250AF00B0CAAE /* Preview Content */,
);
path = AsyncReactorExample;
sourceTree = "<group>";
};
B4ECB0B62A1250AF00B0CAAE /* Preview Content */ = {
isa = PBXGroup;
children = (
B4ECB0B72A1250AF00B0CAAE /* Preview Assets.xcassets */,
);
path = "Preview Content";
sourceTree = "<group>";
};
B4ECB0BF2A1250E000B0CAAE /* Frameworks */ = {
isa = PBXGroup;
children = (
Expand All @@ -162,10 +75,14 @@
);
dependencies = (
);
fileSystemSynchronizedGroups = (
82F565F82D74885200C72CA5 /* AsyncReactorExample */,
);
name = AsyncReactorExample;
packageProductDependencies = (
B4ECB0C02A1250E000B0CAAE /* AsyncReactor */,
B4ECB0C72A12520700B0CAAE /* Logging */,
B4A9DDD12CF23EF2008CA694 /* Reactor */,
);
productName = AsyncReactorExample;
productReference = B4ECB0AC2A1250AE00B0CAAE /* AsyncReactorExample.app */;
Expand Down Expand Up @@ -212,8 +129,6 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B4ECB0B82A1250AF00B0CAAE /* Preview Assets.xcassets in Resources */,
B4ECB0B42A1250AF00B0CAAE /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -224,16 +139,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B4ECB0B22A1250AE00B0CAAE /* ContentView.swift in Sources */,
80D30FC22A1BBF9A00C653B0 /* RepositoryList.swift in Sources */,
80D30FC62A1CDBC200C653B0 /* RepositoryDescriptionSheet.swift in Sources */,
809CDDE62A1B959D001D17BE /* Repository.swift in Sources */,
B4ECB0B02A1250AE00B0CAAE /* AsyncReactorExampleApp.swift in Sources */,
80E8B9D32A1BAEC200E4B3CC /* RepositoryDetailView.swift in Sources */,
B4ECB0C42A1251B500B0CAAE /* RepositorySearchReactor.swift in Sources */,
80D30FBE2A1BBD8000C653B0 /* RepositoryDetailReactor.swift in Sources */,
80E8B9D12A1BAE2800E4B3CC /* RepositorySearchView.swift in Sources */,
80D30FC42A1BC07B00C653B0 /* RepositoryItem.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -458,6 +363,10 @@
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
B4A9DDD12CF23EF2008CA694 /* Reactor */ = {
isa = XCSwiftPackageProductDependency;
productName = Reactor;
};
B4ECB0C02A1250E000B0CAAE /* AsyncReactor */ = {
isa = XCSwiftPackageProductDependency;
productName = AsyncReactor;
Expand Down
76 changes: 76 additions & 0 deletions Example/AsyncReactorExample/ObservableTestView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//
// ObservableTestView.swift
// AsyncReactorExample
//
// Created by Dominik Arnhof on 21.11.24.
//

import SwiftUI
import Reactor

@available(iOS 17.0, *)
@Observable
class ObservableTestReactor: Reactor {
enum SyncAction {
case count
case enterText(String)
}

@Observable
class State {
var count = 0
var text = "test"
}

private(set) var state = State()

init(state: State = State()) {
self.state = state
print("reactor init")
}

func action(_ action: SyncAction) {
switch action {
case .count:
state.count += 1
case .enterText(let text):
state.text = text
}
}
}

@available(iOS 17.0, *)
struct ObservableTestView: View {
@Environment(ObservableTestReactor.self)
private var reactor

@ActionBinding(ObservableTestReactor.self, keyPath: \.text, action: ObservableTestReactor.SyncAction.enterText)
private var text: String

var body: some View {
let _ = Self._printChanges()
VStack {
Text(reactor.count.formatted())

Button {
reactor.action(.count)
} label: {
Text("+")
}

Text(reactor.text)

TextField("Text", text: $text)
.textFieldStyle(.roundedBorder)
.padding()
}
}
}

#Preview {
if #available(iOS 17.0, *) {
ReactorView(ObservableTestReactor()) {
ObservableTestView()
}
}
}
18 changes: 15 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ let package = Package(
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "AsyncReactor",
targets: ["AsyncReactor"]),
targets: ["AsyncReactor"]
),
.library(
name: "Reactor",
targets: ["Reactor"]
)
],
dependencies: [
// Dependencies declare other packages that this package depends on.
Expand All @@ -24,10 +29,17 @@ let package = Package(
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "ReactorBase",
dependencies: []
),
.target(
name: "Reactor",
dependencies: ["ReactorBase"]
),
.target(
name: "AsyncReactor",
dependencies: [],
path: "Sources"
dependencies: ["ReactorBase"]
),
.testTarget(
name: "AsyncReactorTests",
Expand Down
73 changes: 1 addition & 72 deletions Sources/AsyncReactor/AsyncReactor+SwiftUI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,42 +7,7 @@

#if canImport(SwiftUI)
import SwiftUI

extension AsyncReactor {
@MainActor
public func bind<T>(_ keyPath: KeyPath<State, T>, cancelId: CancelId? = nil, action: @escaping (T) -> Action) -> Binding<T> {
Binding {
self.state[keyPath: keyPath]
} set: { newValue in
Task {
if let cancelId {
await self.action(action(newValue), id: cancelId)
} else {
await self.action(action(newValue))
}
}
}
}

@MainActor
public func bind<T>(_ keyPath: KeyPath<State, T>, cancelId: CancelId? = nil, action: @escaping @autoclosure () -> Action) -> Binding<T> {
bind(keyPath, cancelId: cancelId) { _ in action() }
}

@MainActor
public func bind<T>(_ keyPath: KeyPath<State, T>, action: @escaping (T) -> SyncAction) -> Binding<T> {
Binding {
self.state[keyPath: keyPath]
} set: { newValue in
self.action(action(newValue))
}
}

@MainActor
public func bind<T>(_ keyPath: KeyPath<State, T>, action: @escaping @autoclosure () -> SyncAction) -> Binding<T> {
bind(keyPath) { _ in action() }
}
}
import ReactorBase

/// Property wrapper to get a binding to a state keyPath and a associated Action
/// Can be used and behaves like the `@State` property wrapper
Expand Down Expand Up @@ -123,40 +88,4 @@ public struct ReactorView<Content: View, R: AsyncReactor>: View {
.reactorLifecycle(definesLifecycle ? reactor : nil)
}
}

private class LifecycleModel: ObservableObject {
let onDeinit: () -> Void

init(onDeinit: @escaping () -> Void) {
self.onDeinit = onDeinit
}

deinit {
onDeinit()
}
}

struct ReactorLifecycleCancel: ViewModifier {
@StateObject
private var model: LifecycleModel

init(reactor: (any AsyncReactor)?) {
_model = StateObject(wrappedValue: LifecycleModel(onDeinit: {
reactor?.cancelLifecycleTasks()
}))
}

func body(content: Content) -> some View {
content
.task {
// empty task because when not modifying the content at all, SwiftUI seems to optimise away the modifier
}
}
}

extension View {
public func reactorLifecycle(_ reactor: (any AsyncReactor)?) -> some View {
modifier(ReactorLifecycleCancel(reactor: reactor))
}
}
#endif
Loading