diff --git a/Example/AsyncReactorExample.xcodeproj/project.pbxproj b/Example/AsyncReactorExample.xcodeproj/project.pbxproj index 8eff3c2..6b1aa04 100644 --- a/Example/AsyncReactorExample.xcodeproj/project.pbxproj +++ b/Example/AsyncReactorExample.xcodeproj/project.pbxproj @@ -14,6 +14,8 @@ 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 */; }; + B4335C542A5FEAAD001B16EA /* GitHubAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4335C532A5FEAAD001B16EA /* GitHubAPI.swift */; }; + B43CC1B52A6154E30051AD5B /* Macro in Frameworks */ = {isa = PBXBuildFile; productRef = B43CC1B42A6154E30051AD5B /* Macro */; }; 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 */; }; @@ -31,6 +33,8 @@ 80D30FC52A1CDBC200C653B0 /* RepositoryDescriptionSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryDescriptionSheet.swift; sourceTree = ""; }; 80E8B9D02A1BAE2800E4B3CC /* RepositorySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositorySearchView.swift; sourceTree = ""; }; 80E8B9D22A1BAEC200E4B3CC /* RepositoryDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryDetailView.swift; sourceTree = ""; }; + B4335C532A5FEAAD001B16EA /* GitHubAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubAPI.swift; sourceTree = ""; }; + B4335C552A5FF25F001B16EA /* Macro */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Macro; path = ../Macro; sourceTree = ""; }; 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 = ""; }; B4ECB0B12A1250AE00B0CAAE /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -46,6 +50,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B43CC1B52A6154E30051AD5B /* Macro in Frameworks */, B4ECB0C82A12520700B0CAAE /* Logging in Frameworks */, B4ECB0C12A1250E000B0CAAE /* AsyncReactor in Frameworks */, ); @@ -86,6 +91,7 @@ 80E8B9D02A1BAE2800E4B3CC /* RepositorySearchView.swift */, 80D30FC12A1BBF9A00C653B0 /* RepositoryList.swift */, 80D30FC32A1BC07B00C653B0 /* RepositoryItem.swift */, + B4335C532A5FEAAD001B16EA /* GitHubAPI.swift */, ); path = Search; sourceTree = ""; @@ -103,6 +109,7 @@ B4ECB0A32A1250AE00B0CAAE = { isa = PBXGroup; children = ( + B4335C552A5FF25F001B16EA /* Macro */, B4ECB0BE2A1250D700B0CAAE /* AsyncReactor */, B4ECB0AE2A1250AE00B0CAAE /* AsyncReactorExample */, B4ECB0AD2A1250AE00B0CAAE /* Products */, @@ -166,6 +173,7 @@ packageProductDependencies = ( B4ECB0C02A1250E000B0CAAE /* AsyncReactor */, B4ECB0C72A12520700B0CAAE /* Logging */, + B43CC1B42A6154E30051AD5B /* Macro */, ); productName = AsyncReactorExample; productReference = B4ECB0AC2A1250AE00B0CAAE /* AsyncReactorExample.app */; @@ -233,6 +241,7 @@ B4ECB0C42A1251B500B0CAAE /* RepositorySearchReactor.swift in Sources */, 80D30FBE2A1BBD8000C653B0 /* RepositoryDetailReactor.swift in Sources */, 80E8B9D12A1BAE2800E4B3CC /* RepositorySearchView.swift in Sources */, + B4335C542A5FEAAD001B16EA /* GitHubAPI.swift in Sources */, 80D30FC42A1BC07B00C653B0 /* RepositoryItem.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -456,6 +465,10 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + B43CC1B42A6154E30051AD5B /* Macro */ = { + isa = XCSwiftPackageProductDependency; + productName = Macro; + }; B4ECB0C02A1250E000B0CAAE /* AsyncReactor */ = { isa = XCSwiftPackageProductDependency; productName = AsyncReactor; diff --git a/Example/AsyncReactorExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/AsyncReactorExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ab8ab37..f424f22 100644 --- a/Example/AsyncReactorExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/AsyncReactorExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -8,6 +8,15 @@ "revision" : "32e8d724467f8fe623624570367e3d50c5638e46", "version" : "1.5.2" } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "67c5007099d9ffdd292f421f81f4efe5ee42963e", + "version" : "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-07-09-a" + } } ], "version" : 2 diff --git a/Example/AsyncReactorExample/ContentView.swift b/Example/AsyncReactorExample/ContentView.swift index de85640..7198cc5 100644 --- a/Example/AsyncReactorExample/ContentView.swift +++ b/Example/AsyncReactorExample/ContentView.swift @@ -3,9 +3,10 @@ import AsyncReactor struct ContentView: View { var body: some View { - ReactorView(RepositorySearchReactor()) { + RepositorySearchReactorView { RepositorySearchView() } + .environment(\.gitHubApi, GitHubAPI()) } } @@ -13,6 +14,7 @@ struct ContentView_Previews: PreviewProvider { static var previews: some View { NavigationStack { ContentView() + .environment(\.gitHubApi, GitHubAPI()) } } } diff --git a/Example/AsyncReactorExample/Features/Repository/Search/GitHubAPI.swift b/Example/AsyncReactorExample/Features/Repository/Search/GitHubAPI.swift new file mode 100644 index 0000000..954664a --- /dev/null +++ b/Example/AsyncReactorExample/Features/Repository/Search/GitHubAPI.swift @@ -0,0 +1,27 @@ +// +// GitHubAPI.swift +// AsyncReactorExample +// +// Created by Dominik Arnhof on 13.07.23. +// + +import SwiftUI + +struct GitHubAPI { + func search(query: String) async throws -> [Repository] { + let (data, _) = try await URLSession.shared.data(from: URL(string:"https://api.github.com/search/repositories?q=\(query)")!) + let decodedResponse = try JSONDecoder().decode(RepositoriesResponse.self, from: data) + return decodedResponse.repositories + } +} + +private struct GitHubAPIKey: EnvironmentKey { + static var defaultValue: GitHubAPI? = nil +} + +extension EnvironmentValues { + var gitHubApi: GitHubAPI? { + get { self[GitHubAPIKey.self] } + set { self[GitHubAPIKey.self] = newValue } + } +} diff --git a/Example/AsyncReactorExample/Features/Repository/Search/RepositorySearchReactor.swift b/Example/AsyncReactorExample/Features/Repository/Search/RepositorySearchReactor.swift index efbdefd..d18c122 100644 --- a/Example/AsyncReactorExample/Features/Repository/Search/RepositorySearchReactor.swift +++ b/Example/AsyncReactorExample/Features/Repository/Search/RepositorySearchReactor.swift @@ -9,6 +9,8 @@ import Foundation import AsyncReactor import UIKit import Logging +import Macro +import SwiftUI private let logger = Logger(label: "RepositorySearchReactor") @@ -28,7 +30,8 @@ enum SortOptions: String, CaseIterable, Identifiable { } } -class RepositorySearchReactor: AsyncReactor { +@Reactor +class RepositorySearchReactor { enum Action { case onHidePrivateToggle case enterQuery(String) @@ -44,8 +47,11 @@ class RepositorySearchReactor: AsyncReactor { var sortBy: SortOptions = .watchers } - @Published - private(set) var state: State + @Dependency(\.gitHubApi) + var gitHubApi + + @Dependency(\.managedObjectContext) + var hallo @MainActor init(state: State = State()) { @@ -63,31 +69,31 @@ class RepositorySearchReactor: AsyncReactor { } } + @MainActor // NOTE: when adding AsyncReactor conformance via macro, @MainActor is not automatically added apparently... func action(_ action: Action) async { switch action { case .onHidePrivateToggle: state.hidePrivate.toggle() case .enterQuery(let query): + print("enter query: \(query)") state.query = query try? await Task.sleep(for: .seconds(1)) guard !Task.isCancelled else { return } - + print("load") await self.action(.load) case .load: state.isLoading = true do { let currentQuery = state.query.isEmpty ? "iOS" : state.query - let (data, _) = try await URLSession.shared.data(from: URL(string:"https://api.github.com/search/repositories?q=\(currentQuery)")!) - let decodedResponse = try JSONDecoder().decode(RepositoriesResponse.self, from: data) - state.repositories = decodedResponse.repositories + state.repositories = try await gitHubApi!.search(query: currentQuery) state.isLoading = false - logger.debug("search repositories success: \(String(describing: decodedResponse.repositories.count))") + logger.debug("search repositories success: \(String(describing: state.repositories.count))") } catch { logger.error("error while searching repositories: \(error)") diff --git a/Example/AsyncReactorExample/Features/Repository/Search/RepositorySearchView.swift b/Example/AsyncReactorExample/Features/Repository/Search/RepositorySearchView.swift index a8312b6..b36b40e 100644 --- a/Example/AsyncReactorExample/Features/Repository/Search/RepositorySearchView.swift +++ b/Example/AsyncReactorExample/Features/Repository/Search/RepositorySearchView.swift @@ -25,6 +25,8 @@ struct RepositorySearchView: View { var body: some View { NavigationStack { List { + TextField("", text: $query) + Toggle("Hide Private Repos", isOn: $hidePrivate) if reactor.isLoading { @@ -57,7 +59,7 @@ struct RepositorySearchView: View { .refreshable { reactor.send(.load) } - .searchable(text: $query) +// .searchable(text: $query) .toolbar { ToolbarItem { Menu("Sort By") { diff --git a/Macro/.gitignore b/Macro/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/Macro/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Macro/Package.swift b/Macro/Package.swift new file mode 100644 index 0000000..d894796 --- /dev/null +++ b/Macro/Package.swift @@ -0,0 +1,53 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription +import CompilerPluginSupport + +let package = Package( + name: "Macro", + platforms: [.macOS(.v12), .iOS(.v15), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "Macro", + targets: ["Macro"] + ), + .executable( + name: "MacroClient", + targets: ["MacroClient"] + ), + ], + dependencies: [ + // Depend on the latest Swift 5.9 prerelease of SwiftSyntax + .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0-swift-5.9-DEVELOPMENT-SNAPSHOT-2023-04-25-b"), + .package(name: "AsyncReactor", path: "../") + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + // Macro implementation that performs the source transformation of a macro. + .macro( + name: "MacroMacros", + dependencies: [ + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + .product(name: "SwiftCompilerPlugin", package: "swift-syntax") + ] + ), + + // Library that exposes a macro as part of its API, which is used in client programs. + .target(name: "Macro", dependencies: ["MacroMacros"]), + + // A client of the library, which is able to use the macro in its own code. + .executableTarget(name: "MacroClient", dependencies: ["Macro", "AsyncReactor"]), + + // A test target used to develop the macro implementation. + .testTarget( + name: "MacroTests", + dependencies: [ + "MacroMacros", + .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), + ] + ), + ] +) diff --git a/Macro/Sources/Macro/Macro.swift b/Macro/Sources/Macro/Macro.swift new file mode 100644 index 0000000..abe9084 --- /dev/null +++ b/Macro/Sources/Macro/Macro.swift @@ -0,0 +1,20 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +import AsyncReactor + +/// A macro that produces both a value and a string containing the +/// source code that generated the value. For example, +/// +/// #stringify(x + y) +/// +/// produces a tuple `(x + y, "x + y")`. +@freestanding(expression) +public macro stringify(_ value: T) -> (T, String) = #externalMacro(module: "MacroMacros", type: "StringifyMacro") + +@attached(extension, conformances: AsyncReactor) +@attached(member, names: named(state)) +@attached(peer, names: suffixed(View)) +public macro Reactor() = #externalMacro(module: "MacroMacros", type: "ReactorMacro") + +//@attached(member, names: named(_$backingData), named(backingData), named(schemaMetadata), named(`init`), named(_$observationRegistrar)) @attached(memberAttribute) @attached(conformance) public macro Model() = #externalMacro(module: "SwiftDataMacros", type: "PersistentModelMacro") diff --git a/Macro/Sources/MacroClient/main.swift b/Macro/Sources/MacroClient/main.swift new file mode 100644 index 0000000..f02da2b --- /dev/null +++ b/Macro/Sources/MacroClient/main.swift @@ -0,0 +1,40 @@ +import Macro +import SwiftData +import AsyncReactor +import SwiftUI + +let a = 17 +let b = 25 + +let (result, code) = #stringify(a + b) + +print("The value \(result) was produced by the code \"\(code)\"") + +@Reactor +class TestReactor { + enum Action { + case search(String) + } + + struct State { + var query = "" + } + + @Dependency(\.managedObjectContext) + var context + + @Dependency(\.colorScheme) + var colorScheme + + @MainActor + init(state: State = State()) { + self.state = state + } + + func action(_ action: Action) async { + switch action { + case .search(let string): + print(string) + } + } +} diff --git a/Macro/Sources/MacroMacros/MacroMacro.swift b/Macro/Sources/MacroMacros/MacroMacro.swift new file mode 100644 index 0000000..2f230d1 --- /dev/null +++ b/Macro/Sources/MacroMacros/MacroMacro.swift @@ -0,0 +1,205 @@ +import SwiftCompilerPlugin +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +/// Implementation of the `stringify` macro, which takes an expression +/// of any type and produces a tuple containing the value of that expression +/// and the source code that produced the value. For example +/// +/// #stringify(x + y) +/// +/// will expand to +/// +/// (x + y, "x + y") +public struct StringifyMacro: ExpressionMacro { + public static func expansion( + of node: some FreestandingMacroExpansionSyntax, + in context: some MacroExpansionContext + ) -> ExprSyntax { + guard let argument = node.argumentList.first?.expression else { + fatalError("compiler bug: the macro does not have any arguments") + } + + return "(\(argument), \(literal: argument.description))" + } +} + +public struct ReactorMacro: ExtensionMacro, MemberMacro, PeerMacro { + public static func expansion(of node: AttributeSyntax, attachedTo declaration: some DeclGroupSyntax, providingExtensionsOf type: some TypeSyntaxProtocol, conformingTo protocols: [TypeSyntax], in context: some MacroExpansionContext) throws -> [ExtensionDeclSyntax] { + let reactorExtension: DeclSyntax = + """ + extension \(type.trimmed): AsyncReactor {} + """ + + guard let extensionDecl = reactorExtension.as(ExtensionDeclSyntax.self) else { + return [] + } + + return [extensionDecl] + } + + public static func expansion(of node: SwiftSyntax.AttributeSyntax, providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] { + guard let classDeclaration = declaration.as(ClassDeclSyntax.self) else { + preconditionFailure("@Reactor can only be applied to classes") + } + + let info = extractInfo(declaration: classDeclaration) + + var results = [ + """ + @Published + @MainActor + private(set) var state: State + """ + ] + + var initializer = "" + + if info.canGenerateInitializer { + initializer += "@MainActor convenience init(" + + for dependency in info.dependencies { + initializer += "\(dependency.variableName): Type," + } + } + + /* + @MainActor + convenience init(gitHubApi: GitHubAPI) { + self.init() + + $gitHubApi.value = { + gitHubApi + } + } + */ + + return results.map { DeclSyntax(stringLiteral: $0) } + } + + public static func expansion(of node: SwiftSyntax.AttributeSyntax, providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] { + guard let classDeclaration = declaration.as(ClassDeclSyntax.self) else { + preconditionFailure("@Reactor can only be applied to classes") + } + + let info = extractInfo(declaration: classDeclaration) + + var environmentDeclarations = "" + + for dependency in info.dependencies { + environmentDeclarations.append( + """ + @Environment(\\.\(dependency.envKeyPath)) + private var \(dependency.envKeyPath) + """ + ) + } + + var providers = "" + + for dependency in info.dependencies { + providers.append( + """ + reactor.$\(dependency.variableName).value = { + \(dependency.envKeyPath) + } + """ + ) + } + + var initWithDefaultReactor = "" + + if info.canGenerateInitializer { + initWithDefaultReactor = """ + @MainActor + init(definesLifecycle: Bool = true, @ViewBuilder content: () -> Content) { + self.init(\(info.reactorType)(), definesLifecycle: definesLifecycle, content: content) + } + """ + } + + return [ + """ + struct \(raw: info.reactorType)View: SwiftUI.View { + let content: Content + let definesLifecycle: Bool + + @StateObject + private var reactor: \(raw: info.reactorType) + + \(raw: environmentDeclarations) + + @MainActor + init(_ reactor: @escaping @autoclosure () -> \(raw: info.reactorType), definesLifecycle: Bool = true, @ViewBuilder content: () -> Content) { + _reactor = StateObject(wrappedValue: reactor()) + self.content = content() + self.definesLifecycle = definesLifecycle + } + + \(raw: initWithDefaultReactor) + + var body: some View { + content + .environmentObject(reactor) + .reactorLifecycle(definesLifecycle ? reactor : nil) + .onAppear { + \(raw: providers) + } + } + } + """ + ] + } + + struct Info { + struct Dependency { + let variableName: String + let type: String + let envKeyPath: String + } + + let reactorType: String + let dependencies: [Dependency] + let canGenerateInitializer: Bool + } + + static func extractInfo(declaration: ClassDeclSyntax) -> Info { + let reactorType = declaration.identifier.text + + var dependencies = [Info.Dependency]() + + var canGenerateInitializer = false + + for member in declaration.memberBlock.members { + if let variable = member.decl.as(VariableDeclSyntax.self), let attributes = variable.attributes { + for attribute in attributes { + guard let attribute = attribute.as(AttributeSyntax.self), + let name = attribute.attributeName.as(SimpleTypeIdentifierSyntax.self)?.name, name.text == "Dependency", + let argument = attribute.argument?.as(TupleExprElementListSyntax.self), + let keyPath = argument.first?.as(TupleExprElementSyntax.self)?.expression.as(KeyPathExprSyntax.self), + let keyPathName = keyPath.components.first?.component.as(KeyPathPropertyComponentSyntax.self)?.identifier.text, + let variableName = variable.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier.text else { continue } + + dependencies.append(.init(variableName: variableName, type: "", envKeyPath: keyPathName)) + } + } else if let initializer = member.decl.as(InitializerDeclSyntax.self) { + let parameterList = initializer.signature.input.parameterList + + if parameterList.isEmpty || parameterList.allSatisfy({ $0.defaultArgument != nil }) { + canGenerateInitializer = true + } + } + } + + return Info(reactorType: reactorType, dependencies: dependencies, canGenerateInitializer: canGenerateInitializer) + } +} + +@main +struct MacroPlugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + StringifyMacro.self, + ReactorMacro.self + ] +} diff --git a/Macro/Tests/MacroTests/MacroTests.swift b/Macro/Tests/MacroTests/MacroTests.swift new file mode 100644 index 0000000..55e05b7 --- /dev/null +++ b/Macro/Tests/MacroTests/MacroTests.swift @@ -0,0 +1,141 @@ +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest +import MacroMacros + +let testMacros: [String: Macro.Type] = [ + "stringify": StringifyMacro.self, +] + +let testMacrosReactor: [String: Macro.Type] = [ + "Reactor": ReactorMacro.self, +] + +final class MacroTests: XCTestCase { +// func testMacro() { +// assertMacroExpansion( +// """ +// #stringify(a + b) +// """, +// expandedSource: """ +// (a + b, "a + b") +// """, +// macros: testMacros +// ) +// } +// +// func testMacroWithStringLiteral() { +// assertMacroExpansion( +// #""" +// #stringify("Hello, \(name)") +// """#, +// expandedSource: #""" +// ("Hello, \(name)", #""Hello, \(name)""#) +// """#, +// macros: testMacros +// ) +// } + + func testReactorMacro() { + assertMacroExpansion( + #""" + @Reactor + class TestReactor { + enum Action { + case search(String) + } + + struct State { + var query = "" + } + + @Dependency(\.managedObjectContext) + var context + + @Dependency(\.modelContext) + var modelContext + + @MainActor + init(state: State = State()) { + self.state = state + } + + func action(_ action: Action) async { + switch action { + case .search(let string): + print(string) + } + } + } + """#, + expandedSource: #""" + @Reactor + class TestReactor { + enum Action { + case search(String) + } + + struct State { + var query = "" + } + + @Dependency(\.managedObjectContext) + var context + + @Dependency(\.modelContext) + var modelContext + + @MainActor + init(state: State = State()) { + self.state = state + } + + func action(_ action: Action) async { + switch action { + case .search(let string): + print(string) + } + } + @Published + private (set) var state: State + + public struct RView: SwiftUI.View { + let content: Content + let definesLifecycle: Bool + + @StateObject + private var reactor: TestReactor + + @Environment(\.managedObjectContext) + private var managedObjectContext + @Environment(\.modelContext) + private var modelContext + + public init(_ reactor: @escaping @autoclosure () -> TestReactor, definesLifecycle: Bool = true, @ViewBuilder content: () -> Content) { + _reactor = StateObject(wrappedValue: reactor()) + self.content = content() + self.definesLifecycle = definesLifecycle + } + + public var body: some SwiftUI.View { + content + .environmentObject(reactor) + .reactorLifecycle(definesLifecycle ? reactor : nil) + .onAppear { + reactor.$context.value = { + managedObjectContext + } + reactor.$modelContext.value = { + modelContext + } + } + } + } + } + extension TestReactor: AsyncReactor { + } + """#, + macros: testMacrosReactor + ) + } +} diff --git a/Sources/AsyncReactor/Dependency.swift b/Sources/AsyncReactor/Dependency.swift new file mode 100644 index 0000000..08fcd66 --- /dev/null +++ b/Sources/AsyncReactor/Dependency.swift @@ -0,0 +1,29 @@ +// +// Dependency.swift +// +// +// Created by Dominik Arnhof on 14.07.23. +// + +import SwiftUI + +@propertyWrapper +public struct Dependency { + public var wrappedValue: Value { + valueProvider.value() + } + + public var projectedValue: Provider { + valueProvider + } + + public class Provider { + public var value: (() -> Value)! + } + + public let valueProvider = Provider() + + public init(_ keyPath: KeyPath) { + + } +}