diff --git a/Examples/SyncUps/SyncUps/AppFeature.swift b/Examples/SyncUps/SyncUps/AppFeature.swift index e555370..58e48ba 100644 --- a/Examples/SyncUps/SyncUps/AppFeature.swift +++ b/Examples/SyncUps/SyncUps/AppFeature.swift @@ -51,7 +51,7 @@ extension Store: RecordMeetingDelegate { func debounceSave(syncUps: [SyncUp]) async throws { cancel(Self.debounceSave) - try await di.continuousClock.sleep(for: .seconds(1)) + try await di.continuousClock.sleep(for: .seconds(1)) try await di.dataManager.save(JSONEncoder().encode(syncUps), .syncUps) } } @@ -81,8 +81,8 @@ struct AppView: View { var body: some View { NavigationSteps( - selection: $state.binding.path.selected - ) { + selection: $state.binding.path.selected + ) { listView detailView @@ -93,7 +93,7 @@ struct AppView: View { meetingView } } - .stepEnvironment($state.binding.path) + .stepEnvironment($state.binding.path) } private var listView: some View { diff --git a/Examples/SyncUps/SyncUps/Dependencies/OpenSettings.swift b/Examples/SyncUps/SyncUps/Dependencies/OpenSettings.swift index 6c7c691..93077e2 100644 --- a/Examples/SyncUps/SyncUps/Dependencies/OpenSettings.swift +++ b/Examples/SyncUps/SyncUps/Dependencies/OpenSettings.swift @@ -3,8 +3,8 @@ import VDStore extension StoreDIValues { - @StoreDIValue - var openSettings: @Sendable () async -> Void = Self.openSettings + @StoreDIValue + var openSettings: @Sendable () async -> Void = Self.openSettings private static let openSettings: @Sendable () async -> Void = { await MainActor.run { diff --git a/Examples/SyncUps/SyncUps/Dependencies/SpeechRecognizer.swift b/Examples/SyncUps/SyncUps/Dependencies/SpeechRecognizer.swift index d53f8c6..8659af6 100644 --- a/Examples/SyncUps/SyncUps/Dependencies/SpeechRecognizer.swift +++ b/Examples/SyncUps/SyncUps/Dependencies/SpeechRecognizer.swift @@ -152,7 +152,7 @@ private actor Speech { request: SFSpeechAudioBufferRecognitionRequest ) -> AsyncThrowingStream { AsyncThrowingStream { continuation in - return; + return; self.recognitionContinuation = continuation let audioSession = AVAudioSession.sharedInstance() do { diff --git a/Examples/SyncUps/SyncUps/RecordMeeting.swift b/Examples/SyncUps/SyncUps/RecordMeeting.swift index 8a6aafd..8fd0759 100644 --- a/Examples/SyncUps/SyncUps/RecordMeeting.swift +++ b/Examples/SyncUps/SyncUps/RecordMeeting.swift @@ -38,22 +38,22 @@ extension StoreDIValues { extension Store { func confirmDiscard() { - di.pop() - cancel(Self.onTask) + di.pop() + cancel(Self.onTask) } func save() { state.syncUp.meetings.insert( Meeting( id: di.uuid(), - date: di.date.now, + date: di.date.now, transcript: state.transcript ), at: 0 ) di.recordMeetingDelegate?.savePath(transcript: state.transcript) - di.pop() - cancel(Self.onTask) + di.pop() + cancel(Self.onTask) } func endMeetingButtonTapped() { @@ -85,10 +85,10 @@ extension Store { } group.addTask { - while !Task.isCancelled { - await timerTick() - try? await di.continuousClock.sleep(for: .seconds(1)) - } + while !Task.isCancelled { + await timerTick() + try? await di.continuousClock.sleep(for: .seconds(1)) + } } } } @@ -231,7 +231,7 @@ struct MeetingHeaderView: View { var body: some View { VStack { - ProgressView(value: max(0, min(1, progress))) + ProgressView(value: max(0, min(1, progress))) .progressViewStyle(MeetingProgressViewStyle(theme: theme)) HStack { VStack(alignment: .leading) { diff --git a/Examples/SyncUps/SyncUps/SyncUpDetail.swift b/Examples/SyncUps/SyncUps/SyncUpDetail.swift index 03334a6..b141144 100644 --- a/Examples/SyncUps/SyncUps/SyncUpDetail.swift +++ b/Examples/SyncUps/SyncUps/SyncUpDetail.swift @@ -10,7 +10,7 @@ struct SyncUpDetail: Equatable { @Steps struct Destination: Equatable { var alert = Alert() - var edit = SyncUpForm(syncUp: SyncUp(id: StoreDIValues.current.uuid())) + var edit = SyncUpForm(syncUp: SyncUp(id: StoreDIValues.current.uuid())) @Steps struct Alert: Equatable { @@ -52,7 +52,7 @@ extension Store { withAnimation { di.syncUpDetailDelegate?.deleteSyncUp(syncUp: state.syncUp) } - di.pop() + di.pop() } func continueWithoutRecording() { @@ -210,7 +210,7 @@ extension View { isPresented: store.binding.destination.alert.isSelected(.confirmDeletion) ) { Button("Yes", role: .destructive) { - store.confirmDeletion() + store.confirmDeletion() } Button("Nevermind", role: .cancel) {} } message: { diff --git a/Examples/SyncUps/SyncUps/SyncUpForm.swift b/Examples/SyncUps/SyncUps/SyncUpForm.swift index ca92d40..275a56f 100644 --- a/Examples/SyncUps/SyncUps/SyncUpForm.swift +++ b/Examples/SyncUps/SyncUps/SyncUpForm.swift @@ -12,9 +12,9 @@ struct SyncUpForm: Equatable { ) { self.focus = focus self.syncUp = syncUp - if self.syncUp.attendees.isEmpty { - self.syncUp.attendees.append(Attendee(id: StoreDIValues.current.uuid())) - } + if self.syncUp.attendees.isEmpty { + self.syncUp.attendees.append(Attendee(id: StoreDIValues.current.uuid())) + } } enum Field: Hashable { diff --git a/Examples/TicTacToe/App/RootView.swift b/Examples/TicTacToe/App/RootView.swift index 48a8594..abaff6c 100644 --- a/Examples/TicTacToe/App/RootView.swift +++ b/Examples/TicTacToe/App/RootView.swift @@ -1,8 +1,8 @@ import AppCore import AppSwiftUI import AppUIKit -import VDStore import SwiftUI +import VDStore private let readMe = """ This application demonstrates how to build a moderately complex application in the VDStore. @@ -27,35 +27,35 @@ enum GameType: Identifiable { struct RootView: View { - @ViewStore( - Store(TicTacToe.login()).transformDI { - $0.logoutButtonDelegate = $0.store(for: TicTacToe.self) - $0.loginDelegate = $0.store(for: TicTacToe.self) - } - ) - private var state + @ViewStore( + Store(TicTacToe.login()).transformDI { + $0.logoutButtonDelegate = $0.store(for: TicTacToe.self) + $0.loginDelegate = $0.store(for: TicTacToe.self) + } + ) + private var state @State var showGame: GameType? var body: some View { - NavigationStack { - Form { - Text(readMe) - - Section { - Button("SwiftUI version") { showGame = .swiftui } - Button("UIKit version") { showGame = .uikit } - } - } - .sheet(item: $showGame) { gameType in - if gameType == .swiftui { - AppView(store: $state) - } else { - UIKitAppView(store: $state) - } - } - .navigationTitle("Tic-Tac-Toe") - } + NavigationStack { + Form { + Text(readMe) + + Section { + Button("SwiftUI version") { showGame = .swiftui } + Button("UIKit version") { showGame = .uikit } + } + } + .sheet(item: $showGame) { gameType in + if gameType == .swiftui { + AppView(store: $state) + } else { + UIKitAppView(store: $state) + } + } + .navigationTitle("Tic-Tac-Toe") + } } } diff --git a/Examples/TicTacToe/TicTacToe.xcodeproj/project.xcworkspace/xcuserdata/danil.xcuserdatad/UserInterfaceState.xcuserstate b/Examples/TicTacToe/TicTacToe.xcodeproj/project.xcworkspace/xcuserdata/danil.xcuserdatad/UserInterfaceState.xcuserstate index 4b50676..694a372 100644 Binary files a/Examples/TicTacToe/TicTacToe.xcodeproj/project.xcworkspace/xcuserdata/danil.xcuserdatad/UserInterfaceState.xcuserstate and b/Examples/TicTacToe/TicTacToe.xcodeproj/project.xcworkspace/xcuserdata/danil.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Examples/TicTacToe/tic-tac-toe/Package.swift b/Examples/TicTacToe/tic-tac-toe/Package.swift index 1566953..21c2c73 100644 --- a/Examples/TicTacToe/tic-tac-toe/Package.swift +++ b/Examples/TicTacToe/tic-tac-toe/Package.swift @@ -27,7 +27,7 @@ let package = Package( ], dependencies: [ .package(name: "VDStore", path: "../../.."), - .package(url: "https://github.com/dankinsoid/VDFlow.git", from: "4.21.0") + .package(url: "https://github.com/dankinsoid/VDFlow.git", from: "4.21.0"), ], targets: [ .target( @@ -37,7 +37,7 @@ let package = Package( "LoginCore", "NewGameCore", "VDStore", - "VDFlow" + "VDFlow", ] ), .testTarget( @@ -67,7 +67,7 @@ let package = Package( ), .target( name: "GameCore", - dependencies: ["VDStore", "VDFlow"] + dependencies: ["VDStore", "VDFlow"] ), .testTarget( name: "GameCoreTests", @@ -87,8 +87,8 @@ let package = Package( dependencies: [ "AuthenticationClient", "TwoFactorCore", - "VDStore", - "VDFlow" + "VDStore", + "VDFlow", ] ), .testTarget( @@ -114,8 +114,8 @@ let package = Package( name: "NewGameCore", dependencies: [ "GameCore", - "VDStore", - "VDFlow" + "VDStore", + "VDFlow", ] ), .testTarget( @@ -141,8 +141,8 @@ let package = Package( name: "TwoFactorCore", dependencies: [ "AuthenticationClient", - "VDStore", - "VDFlow" + "VDStore", + "VDFlow", ] ), .testTarget( diff --git a/Examples/TicTacToe/tic-tac-toe/Sources/AppCore/AppCore.swift b/Examples/TicTacToe/tic-tac-toe/Sources/AppCore/AppCore.swift index 9ffc51f..6f70d5c 100644 --- a/Examples/TicTacToe/tic-tac-toe/Sources/AppCore/AppCore.swift +++ b/Examples/TicTacToe/tic-tac-toe/Sources/AppCore/AppCore.swift @@ -1,26 +1,26 @@ -import VDStore -import VDFlow import LoginCore import NewGameCore import TwoFactorCore +import VDFlow +import VDStore @Steps public struct TicTacToe: Equatable { - public var login: Login = Login() - public var newGame: NewGame = NewGame() + public var login = Login() + public var newGame = NewGame() } extension Store: LogoutButtonDelegate { - public func logoutButtonTapped() { - state = .login() - } + public func logoutButtonTapped() { + state = .login() + } } extension Store: LoginDelegate { - public func didSucceedLogin() { - state = .newGame() - } + public func didSucceedLogin() { + state = .newGame() + } } diff --git a/Examples/TicTacToe/tic-tac-toe/Sources/AppSwiftUI/AppView.swift b/Examples/TicTacToe/tic-tac-toe/Sources/AppSwiftUI/AppView.swift index bcef31d..0b0ae3f 100644 --- a/Examples/TicTacToe/tic-tac-toe/Sources/AppSwiftUI/AppView.swift +++ b/Examples/TicTacToe/tic-tac-toe/Sources/AppSwiftUI/AppView.swift @@ -1,8 +1,8 @@ import AppCore -import VDStore import LoginSwiftUI import NewGameSwiftUI import SwiftUI +import VDStore public struct AppView: View { @ViewStore private var state: TicTacToe @@ -11,16 +11,16 @@ public struct AppView: View { _state = ViewStore(store) } - public var body: some View { - switch state.selected { - case .login: - NavigationStack { - LoginView(store: $state.login) - } - case .newGame: - NavigationStack { - NewGameView(store: $state.newGame) - } - } - } + public var body: some View { + switch state.selected { + case .login: + NavigationStack { + LoginView(store: $state.login) + } + case .newGame: + NavigationStack { + NewGameView(store: $state.newGame) + } + } + } } diff --git a/Examples/TicTacToe/tic-tac-toe/Sources/AppUIKit/AppViewController.swift b/Examples/TicTacToe/tic-tac-toe/Sources/AppUIKit/AppViewController.swift index f153c7f..62e42c7 100644 --- a/Examples/TicTacToe/tic-tac-toe/Sources/AppUIKit/AppViewController.swift +++ b/Examples/TicTacToe/tic-tac-toe/Sources/AppUIKit/AppViewController.swift @@ -1,10 +1,10 @@ import AppCore -import VDStore import Combine import LoginUIKit import NewGameUIKit import SwiftUI import UIKit +import VDStore public struct UIKitAppView: UIViewControllerRepresentable { @Store private var state: TicTacToe @@ -26,11 +26,11 @@ public struct UIKitAppView: UIViewControllerRepresentable { } class AppViewController: UINavigationController { - @Store private var state: TicTacToe - private var cancellableSet: Set = [] + @Store private var state: TicTacToe + private var cancellableSet: Set = [] init(store: Store) { - _state = store + _state = store super.init(nibName: nil, bundle: nil) } @@ -42,18 +42,18 @@ class AppViewController: UINavigationController { override func viewDidLoad() { super.viewDidLoad() - $state.publisher - .map(\.selected) - .removeDuplicates() - .sink { [weak self] selected in - guard let self else { return } - switch selected { - case .login: - setViewControllers([LoginViewController(store: $state.login)], animated: false) - case .newGame: - setViewControllers([NewGameViewController(store: $state.newGame)], animated: false) - } - } - .store(in: &cancellableSet) + $state.publisher + .map(\.selected) + .removeDuplicates() + .sink { [weak self] selected in + guard let self else { return } + switch selected { + case .login: + setViewControllers([LoginViewController(store: $state.login)], animated: false) + case .newGame: + setViewControllers([NewGameViewController(store: $state.newGame)], animated: false) + } + } + .store(in: &cancellableSet) } } diff --git a/Examples/TicTacToe/tic-tac-toe/Sources/AuthenticationClient/AuthenticationClient.swift b/Examples/TicTacToe/tic-tac-toe/Sources/AuthenticationClient/AuthenticationClient.swift index ce0a614..efa87bf 100644 --- a/Examples/TicTacToe/tic-tac-toe/Sources/AuthenticationClient/AuthenticationClient.swift +++ b/Examples/TicTacToe/tic-tac-toe/Sources/AuthenticationClient/AuthenticationClient.swift @@ -18,7 +18,7 @@ public enum AuthenticationError: Equatable, LocalizedError, Sendable { case invalidUserPassword case invalidTwoFactor case invalidIntermediateToken - case unimplemented + case unimplemented public var errorDescription: String? { switch self { @@ -28,8 +28,8 @@ public enum AuthenticationError: Equatable, LocalizedError, Sendable { return "Invalid second factor (try 1234)" case .invalidIntermediateToken: return "404!! What happened to your token there bud?!?!" - case .unimplemented: - return "This feature is not yet implemented." + case .unimplemented: + return "This feature is not yet implemented." } } } @@ -37,42 +37,43 @@ public enum AuthenticationError: Equatable, LocalizedError, Sendable { public struct AuthenticationClient: Sendable { public var login: - @Sendable (_ email: String, _ password: String) async throws -> AuthenticationResponse = { _, _ in - throw AuthenticationError.unimplemented - } + @Sendable (_ email: String, _ password: String) async throws -> AuthenticationResponse = { _, _ in + throw AuthenticationError.unimplemented + } public var twoFactor: - @Sendable (_ code: String, _ token: String) async throws -> AuthenticationResponse = { _, _ in - throw AuthenticationError.unimplemented - } + @Sendable (_ code: String, _ token: String) async throws -> AuthenticationResponse = { _, _ in + throw AuthenticationError.unimplemented + } } -extension AuthenticationClient { - public static let liveValue = Self( - login: { email, password in - guard email.contains("@"), password == "password" - else { throw AuthenticationError.invalidUserPassword } - - try await Task.sleep(for: .seconds(1)) - return AuthenticationResponse( - token: "deadbeef", twoFactorRequired: email.contains("2fa") - ) - }, - twoFactor: { code, token in - guard token == "deadbeef" - else { throw AuthenticationError.invalidIntermediateToken } - - guard code == "1234" - else { throw AuthenticationError.invalidTwoFactor } - - try await Task.sleep(for: .seconds(1)) - return AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: false) - } - ) +public extension AuthenticationClient { + + static let liveValue = Self( + login: { email, password in + guard email.contains("@"), password == "password" + else { throw AuthenticationError.invalidUserPassword } + + try await Task.sleep(for: .seconds(1)) + return AuthenticationResponse( + token: "deadbeef", twoFactorRequired: email.contains("2fa") + ) + }, + twoFactor: { code, token in + guard token == "deadbeef" + else { throw AuthenticationError.invalidIntermediateToken } + + guard code == "1234" + else { throw AuthenticationError.invalidTwoFactor } + + try await Task.sleep(for: .seconds(1)) + return AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: false) + } + ) } public extension StoreDIValues { - - @StoreDIValue - var authenticationClient = valueFor(live: AuthenticationClient.liveValue, test: AuthenticationClient()) + + @StoreDIValue + var authenticationClient = valueFor(live: AuthenticationClient.liveValue, test: AuthenticationClient()) } diff --git a/Examples/TicTacToe/tic-tac-toe/Sources/GameCore/GameCore.swift b/Examples/TicTacToe/tic-tac-toe/Sources/GameCore/GameCore.swift index a3d47ce..d79e08f 100644 --- a/Examples/TicTacToe/tic-tac-toe/Sources/GameCore/GameCore.swift +++ b/Examples/TicTacToe/tic-tac-toe/Sources/GameCore/GameCore.swift @@ -1,50 +1,50 @@ -import VDStore -import VDFlow import SwiftUI +import VDFlow +import VDStore public struct Game: Sendable, Equatable { - - public var board: Three> = .empty - public var currentPlayer: Player = .x - public let oPlayerName: String - public let xPlayerName: String - - public init(oPlayerName: String, xPlayerName: String) { - self.oPlayerName = oPlayerName - self.xPlayerName = xPlayerName - } - - public var currentPlayerName: String { - switch currentPlayer { - case .o: return oPlayerName - case .x: return xPlayerName - } - } + + public var board: Three> = .empty + public var currentPlayer: Player = .x + public let oPlayerName: String + public let xPlayerName: String + + public init(oPlayerName: String, xPlayerName: String) { + self.oPlayerName = oPlayerName + self.xPlayerName = xPlayerName + } + + public var currentPlayerName: String { + switch currentPlayer { + case .o: return oPlayerName + case .x: return xPlayerName + } + } } @Actions -extension Store { - - public func cellTapped(row: Int, column: Int) { - guard - state.board[row][column] == nil, - !state.board.hasWinner - else { return } - - state.board[row][column] = state.currentPlayer - - if !state.board.hasWinner { - state.currentPlayer.toggle() - } - } - - public func playAgainButtonTapped() { - state = Game(oPlayerName: state.oPlayerName, xPlayerName: state.xPlayerName) - } - - public func quitButtonTapped() { - di.dismiss() - } +public extension Store { + + func cellTapped(row: Int, column: Int) { + guard + state.board[row][column] == nil, + !state.board.hasWinner + else { return } + + state.board[row][column] = state.currentPlayer + + if !state.board.hasWinner { + state.currentPlayer.toggle() + } + } + + func playAgainButtonTapped() { + state = Game(oPlayerName: state.oPlayerName, xPlayerName: state.xPlayerName) + } + + func quitButtonTapped() { + di.dismiss() + } } public enum Player: Equatable, Sendable { diff --git a/Examples/TicTacToe/tic-tac-toe/Sources/GameSwiftUI/GameView.swift b/Examples/TicTacToe/tic-tac-toe/Sources/GameSwiftUI/GameView.swift index b8b8e2b..0f92be7 100644 --- a/Examples/TicTacToe/tic-tac-toe/Sources/GameSwiftUI/GameView.swift +++ b/Examples/TicTacToe/tic-tac-toe/Sources/GameSwiftUI/GameView.swift @@ -1,7 +1,7 @@ -import VDStore -import VDFlow import GameCore import SwiftUI +import VDFlow +import VDStore public struct GameView: View { diff --git a/Examples/TicTacToe/tic-tac-toe/Sources/GameUIKit/GameViewController.swift b/Examples/TicTacToe/tic-tac-toe/Sources/GameUIKit/GameViewController.swift index b5d246f..c961c3a 100644 --- a/Examples/TicTacToe/tic-tac-toe/Sources/GameUIKit/GameViewController.swift +++ b/Examples/TicTacToe/tic-tac-toe/Sources/GameUIKit/GameViewController.swift @@ -1,11 +1,11 @@ -import VDStore import Combine import GameCore import UIKit +import VDStore public final class GameViewController: UIViewController { @Store private var state: Game - private var cancellableSet: Set = [] + private var cancellableSet: Set = [] public init(store: Store) { _state = store @@ -109,21 +109,21 @@ public final class GameViewController: UIViewController { ]) } - $state.publisher - .removeDuplicates() - .sink { state in - titleLabel.text = state.title - playAgainButton.isHidden = state.isPlayAgainButtonHidden - - for (rowIdx, row) in state.rows.enumerated() { - for (colIdx, label) in row.enumerated() { - let button = cells[rowIdx][colIdx] - button.setTitle(label, for: .normal) - button.isEnabled = state.isGameEnabled - } - } - } - .store(in: &cancellableSet) + $state.publisher + .removeDuplicates() + .sink { state in + titleLabel.text = state.title + playAgainButton.isHidden = state.isPlayAgainButtonHidden + + for (rowIdx, row) in state.rows.enumerated() { + for (colIdx, label) in row.enumerated() { + let button = cells[rowIdx][colIdx] + button.setTitle(label, for: .normal) + button.isEnabled = state.isGameEnabled + } + } + } + .store(in: &cancellableSet) } @objc private func gridCell11Tapped() { $state.cellTapped(row: 0, column: 0) } @@ -137,11 +137,11 @@ public final class GameViewController: UIViewController { @objc private func gridCell33Tapped() { $state.cellTapped(row: 2, column: 2) } @objc private func quitButtonTapped() { - $state.quitButtonTapped() + $state.quitButtonTapped() } @objc private func playAgainButtonTapped() { - $state.playAgainButtonTapped() + $state.playAgainButtonTapped() } } diff --git a/Examples/TicTacToe/tic-tac-toe/Sources/LoginCore/LoginCore.swift b/Examples/TicTacToe/tic-tac-toe/Sources/LoginCore/LoginCore.swift index 552f31d..4b1bebb 100644 --- a/Examples/TicTacToe/tic-tac-toe/Sources/LoginCore/LoginCore.swift +++ b/Examples/TicTacToe/tic-tac-toe/Sources/LoginCore/LoginCore.swift @@ -1,47 +1,47 @@ import AuthenticationClient -import VDStore -import VDFlow import Dispatch import TwoFactorCore +import VDFlow +import VDStore public struct Login: Sendable, Equatable { - public var flow: Flow = .none - public var email = "" - public var isLoginRequestInFlight = false - public var password = "" + public var flow: Flow = .none + public var email = "" + public var isLoginRequestInFlight = false + public var password = "" - public init() {} + public init() {} - public var isFormValid: Bool { - !email.isEmpty && !password.isEmpty - } + public var isFormValid: Bool { + !email.isEmpty && !password.isEmpty + } - @Steps - public struct Flow: Equatable, Sendable { - public var twoFactor: TwoFactor = TwoFactor(token: "") - public var alert = "" - public var none - } + @Steps + public struct Flow: Equatable, Sendable { + public var twoFactor = TwoFactor(token: "") + public var alert = "" + public var none + } } @Actions public extension Store { - func loginButtonTapped() async { - state.isLoginRequestInFlight = true - defer { - state.isLoginRequestInFlight = false - } - do { - let response = try await di.authenticationClient.login(state.email, state.password) - if response.twoFactorRequired { - state.flow.$twoFactor.select(with: TwoFactor(token: response.token)) - } else { - di.loginDelegate?.didSucceedLogin() - } - } catch { - state.flow.$alert.select(with: error.localizedDescription) - } - } + func loginButtonTapped() async { + state.isLoginRequestInFlight = true + defer { + state.isLoginRequestInFlight = false + } + do { + let response = try await di.authenticationClient.login(state.email, state.password) + if response.twoFactorRequired { + state.flow.$twoFactor.select(with: TwoFactor(token: response.token)) + } else { + di.loginDelegate?.didSucceedLogin() + } + } catch { + state.flow.$alert.select(with: error.localizedDescription) + } + } } diff --git a/Examples/TicTacToe/tic-tac-toe/Sources/LoginSwiftUI/LoginView.swift b/Examples/TicTacToe/tic-tac-toe/Sources/LoginSwiftUI/LoginView.swift index 8f633ad..7850e4d 100644 --- a/Examples/TicTacToe/tic-tac-toe/Sources/LoginSwiftUI/LoginView.swift +++ b/Examples/TicTacToe/tic-tac-toe/Sources/LoginSwiftUI/LoginView.swift @@ -1,10 +1,10 @@ import AuthenticationClient -import VDStore -import VDFlow import LoginCore import SwiftUI import TwoFactorCore import TwoFactorSwiftUI +import VDFlow +import VDStore public struct LoginView: View { @@ -30,7 +30,7 @@ public struct LoginView: View { .keyboardType(.emailAddress) .textContentType(.emailAddress) - SecureField("••••••••", text: $state.binding.password) + SecureField("••••••••", text: $state.binding.password) } Button { @@ -41,9 +41,9 @@ public struct LoginView: View { _ = UIApplication.shared.sendAction( #selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil ) - Task { - await $state.loginButtonTapped() - } + Task { + await $state.loginButtonTapped() + } } label: { HStack { Text("Log in") @@ -56,11 +56,11 @@ public struct LoginView: View { .disabled(state.isLoginButtonDisabled) } .disabled(state.isFormDisabled) - .alert(state.flow.alert, isPresented: $state.binding.flow.isSelected(.alert)) { - Button("Ok") {} - } - .navigationDestination(isPresented: $state.binding.flow.isSelected(.twoFactor)) { - TwoFactorView(store: $state.flow.twoFactor) + .alert(state.flow.alert, isPresented: $state.binding.flow.isSelected(.alert)) { + Button("Ok") {} + } + .navigationDestination(isPresented: $state.binding.flow.isSelected(.twoFactor)) { + TwoFactorView(store: $state.flow.twoFactor) } .navigationTitle("Login") } @@ -75,7 +75,7 @@ private extension Login { #Preview { NavigationStack { LoginView( - store: Store(Login()).transformDI { + store: Store(Login()).transformDI { $0.authenticationClient.login = { @Sendable _, _ in AuthenticationResponse(token: "deadbeef", twoFactorRequired: false) } diff --git a/Examples/TicTacToe/tic-tac-toe/Sources/LoginUIKit/LoginViewController.swift b/Examples/TicTacToe/tic-tac-toe/Sources/LoginUIKit/LoginViewController.swift index 74f8d89..58b959b 100644 --- a/Examples/TicTacToe/tic-tac-toe/Sources/LoginUIKit/LoginViewController.swift +++ b/Examples/TicTacToe/tic-tac-toe/Sources/LoginUIKit/LoginViewController.swift @@ -1,13 +1,13 @@ -import VDStore -import VDFlow import Combine import LoginCore import TwoFactorUIKit import UIKit +import VDFlow +import VDStore public class LoginViewController: UIViewController { public let store: Store - private var cancellableSet: Set = [] + private var cancellableSet: Set = [] public init(store: Store) { self.store = store @@ -92,46 +92,46 @@ public class LoginViewController: UIViewController { var alertController: UIAlertController? var twoFactorController: TwoFactorViewController? - store.publisher - .removeDuplicates() - .sink { [weak self] state in - guard let self else { return } - if state.email != emailTextField.text { - emailTextField.text = state.email - } - emailTextField.isEnabled = state.isEmailTextFieldEnabled - if passwordTextField.text != state.password { - passwordTextField.text = state.password - } - passwordTextField.isEnabled = state.isPasswordTextFieldEnabled - loginButton.isEnabled = state.isLoginButtonEnabled - activityIndicator.isHidden = state.isActivityIndicatorHidden - - if store.state.flow.selected == .alert, - alertController == nil - { - alertController = UIAlertController(title: store.state.flow.alert, message: nil, preferredStyle: .alert) - alertController?.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil)) - present(alertController!, animated: true, completion: nil) - } else if store.state.flow.selected != .alert, alertController != nil { - alertController?.dismiss(animated: true) - alertController = nil - } - - if store.state.flow.selected == .twoFactor, - twoFactorController == nil - { - twoFactorController = TwoFactorViewController(store: store.flow.twoFactor) - navigationController?.pushViewController( - twoFactorController!, - animated: true - ) - } else if store.state.flow.selected != .twoFactor, twoFactorController != nil { - navigationController?.popToViewController(self, animated: true) - twoFactorController = nil - } - } - .store(in: &cancellableSet) + store.publisher + .removeDuplicates() + .sink { [weak self] state in + guard let self else { return } + if state.email != emailTextField.text { + emailTextField.text = state.email + } + emailTextField.isEnabled = state.isEmailTextFieldEnabled + if passwordTextField.text != state.password { + passwordTextField.text = state.password + } + passwordTextField.isEnabled = state.isPasswordTextFieldEnabled + loginButton.isEnabled = state.isLoginButtonEnabled + activityIndicator.isHidden = state.isActivityIndicatorHidden + + if store.state.flow.selected == .alert, + alertController == nil + { + alertController = UIAlertController(title: store.state.flow.alert, message: nil, preferredStyle: .alert) + alertController?.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil)) + present(alertController!, animated: true, completion: nil) + } else if store.state.flow.selected != .alert, alertController != nil { + alertController?.dismiss(animated: true) + alertController = nil + } + + if store.state.flow.selected == .twoFactor, + twoFactorController == nil + { + twoFactorController = TwoFactorViewController(store: store.flow.twoFactor) + navigationController?.pushViewController( + twoFactorController!, + animated: true + ) + } else if store.state.flow.selected != .twoFactor, twoFactorController != nil { + navigationController?.popToViewController(self, animated: true) + twoFactorController = nil + } + } + .store(in: &cancellableSet) } override public func viewDidAppear(_ animated: Bool) { @@ -143,17 +143,17 @@ public class LoginViewController: UIViewController { } @objc private func loginButtonTapped(sender: UIButton) { - Task { - await store.loginButtonTapped() - } + Task { + await store.loginButtonTapped() + } } @objc private func emailTextFieldChanged(sender: UITextField) { - store.state.email = sender.text ?? "" + store.state.email = sender.text ?? "" } @objc private func passwordTextFieldChanged(sender: UITextField) { - store.state.password = sender.text ?? "" + store.state.password = sender.text ?? "" } } @@ -167,6 +167,5 @@ private extension Login { @Actions private extension Store { - func twoFactorDismissed() { - } + func twoFactorDismissed() {} } diff --git a/Examples/TicTacToe/tic-tac-toe/Sources/NewGameCore/NewGameCore.swift b/Examples/TicTacToe/tic-tac-toe/Sources/NewGameCore/NewGameCore.swift index ffbb18f..66b77b9 100644 --- a/Examples/TicTacToe/tic-tac-toe/Sources/NewGameCore/NewGameCore.swift +++ b/Examples/TicTacToe/tic-tac-toe/Sources/NewGameCore/NewGameCore.swift @@ -1,42 +1,41 @@ -import VDStore -import VDFlow import GameCore +import VDFlow +import VDStore public struct NewGame: Equatable { - public var flow: Flow = .none - public var oPlayerName = "" - public var xPlayerName = "" - - @Steps - public struct Flow: Equatable { - public var game: Game = Game(oPlayerName: "", xPlayerName: "") - public var none - } - - public init() { - } + public var flow: Flow = .none + public var oPlayerName = "" + public var xPlayerName = "" + + @Steps + public struct Flow: Equatable { + public var game = Game(oPlayerName: "", xPlayerName: "") + public var none + } + + public init() {} } @MainActor public protocol LogoutButtonDelegate { - func logoutButtonTapped() + func logoutButtonTapped() } -extension StoreDIValues { - @StoreDIValue - public var logoutButtonDelegate: LogoutButtonDelegate? +public extension StoreDIValues { + @StoreDIValue + var logoutButtonDelegate: LogoutButtonDelegate? } @Actions -extension Store { - - public func letsPlayButtonTapped() { - state.flow.$game.select( - with: Game( - oPlayerName: state.oPlayerName, - xPlayerName: state.xPlayerName - ) - ) - } +public extension Store { + + func letsPlayButtonTapped() { + state.flow.$game.select( + with: Game( + oPlayerName: state.oPlayerName, + xPlayerName: state.xPlayerName + ) + ) + } } diff --git a/Examples/TicTacToe/tic-tac-toe/Sources/NewGameSwiftUI/NewGameView.swift b/Examples/TicTacToe/tic-tac-toe/Sources/NewGameSwiftUI/NewGameView.swift index ab7b0a1..1a18ad0 100644 --- a/Examples/TicTacToe/tic-tac-toe/Sources/NewGameSwiftUI/NewGameView.swift +++ b/Examples/TicTacToe/tic-tac-toe/Sources/NewGameSwiftUI/NewGameView.swift @@ -1,13 +1,13 @@ -import VDStore -import VDFlow import GameCore import GameSwiftUI import NewGameCore import SwiftUI +import VDFlow +import VDStore public struct NewGameView: View { - - @ViewStore var state: NewGame + + @ViewStore var state: NewGame public init(store: Store) { _state = ViewStore(store) @@ -16,7 +16,7 @@ public struct NewGameView: View { public var body: some View { Form { Section { - TextField("Blob Sr.", text: $state.binding.xPlayerName) + TextField("Blob Sr.", text: $state.binding.xPlayerName) .autocapitalization(.words) .disableAutocorrection(true) .textContentType(.name) @@ -34,18 +34,18 @@ public struct NewGameView: View { } Button("Let's play!") { - $state.letsPlayButtonTapped() + $state.letsPlayButtonTapped() } .disabled(state.isLetsPlayButtonDisabled) } .navigationTitle("New Game") - .navigationBarItems( - trailing: Button("Logout") { - $state.di.logoutButtonDelegate?.logoutButtonTapped() - } - ) - .navigationDestination(isPresented: $state.binding.flow.isSelected(.game)) { - GameView(store: $state.flow.game) + .navigationBarItems( + trailing: Button("Logout") { + $state.di.logoutButtonDelegate?.logoutButtonTapped() + } + ) + .navigationDestination(isPresented: $state.binding.flow.isSelected(.game)) { + GameView(store: $state.flow.game) } } } diff --git a/Examples/TicTacToe/tic-tac-toe/Sources/NewGameUIKit/NewGameViewController.swift b/Examples/TicTacToe/tic-tac-toe/Sources/NewGameUIKit/NewGameViewController.swift index d870dd1..5bb12b4 100644 --- a/Examples/TicTacToe/tic-tac-toe/Sources/NewGameUIKit/NewGameViewController.swift +++ b/Examples/TicTacToe/tic-tac-toe/Sources/NewGameUIKit/NewGameViewController.swift @@ -1,12 +1,12 @@ -import VDStore import Combine import GameUIKit import NewGameCore import UIKit +import VDStore public class NewGameViewController: UIViewController { @Store var state: NewGame - private var cancellableBag: Set = [] + private var cancellableBag: Set = [] public init(store: Store) { _state = store @@ -91,41 +91,41 @@ public class NewGameViewController: UIViewController { var gameController: GameViewController? - $state.publisher - .removeDuplicates() - .sink { [weak self] state in - guard let self else { return } - playerOTextField.text = state.oPlayerName - playerXTextField.text = state.xPlayerName - letsPlayButton.isEnabled = state.isLetsPlayButtonEnabled - - if state.flow.selected == .game, - gameController == nil - { - gameController = GameViewController(store: $state.flow.game) - navigationController?.pushViewController(gameController!, animated: true) - } else if state.flow.selected != .game, gameController != nil { - navigationController?.popToViewController(self, animated: true) - gameController = nil - } - } - .store(in: &cancellableBag) + $state.publisher + .removeDuplicates() + .sink { [weak self] state in + guard let self else { return } + playerOTextField.text = state.oPlayerName + playerXTextField.text = state.xPlayerName + letsPlayButton.isEnabled = state.isLetsPlayButtonEnabled + + if state.flow.selected == .game, + gameController == nil + { + gameController = GameViewController(store: $state.flow.game) + navigationController?.pushViewController(gameController!, animated: true) + } else if state.flow.selected != .game, gameController != nil { + navigationController?.popToViewController(self, animated: true) + gameController = nil + } + } + .store(in: &cancellableBag) } @objc private func logoutButtonTapped() { - $state.di.logoutButtonDelegate?.logoutButtonTapped() + $state.di.logoutButtonDelegate?.logoutButtonTapped() } @objc private func playerXTextChanged(sender: UITextField) { - state.xPlayerName = sender.text ?? "" + state.xPlayerName = sender.text ?? "" } @objc private func playerOTextChanged(sender: UITextField) { - state.oPlayerName = sender.text ?? "" + state.oPlayerName = sender.text ?? "" } @objc private func letsPlayTapped() { - $state.letsPlayButtonTapped() + $state.letsPlayButtonTapped() } } diff --git a/Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorCore/TwoFactorCore.swift b/Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorCore/TwoFactorCore.swift index 7befcf5..408db40 100644 --- a/Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorCore/TwoFactorCore.swift +++ b/Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorCore/TwoFactorCore.swift @@ -1,56 +1,56 @@ import AuthenticationClient import Combine -import VDStore import Dispatch import VDFlow +import VDStore public struct TwoFactor: Sendable, Equatable { - - public var flow: Flow = .none - public var code = "" - public var isTwoFactorRequestInFlight = false - public let token: String - - public init(token: String) { - self.token = token - } - - public var isFormValid: Bool { - code.count >= 4 - } - - @Steps - public struct Flow: Sendable, Equatable { - public var alert = "" - public var none - } + + public var flow: Flow = .none + public var code = "" + public var isTwoFactorRequestInFlight = false + public let token: String + + public init(token: String) { + self.token = token + } + + public var isFormValid: Bool { + code.count >= 4 + } + + @Steps + public struct Flow: Sendable, Equatable { + public var alert = "" + public var none + } } @Actions -extension Store { - - public func submitButtonTapped() async { - state.isTwoFactorRequestInFlight = true - defer { - state.isTwoFactorRequestInFlight = false - } - do { - _ = try await di.authenticationClient.twoFactor(state.code, state.token) - di.loginDelegate?.didSucceedLogin() - } catch { - state.flow.$alert.select(with: error.localizedDescription) - } - } +public extension Store { + + func submitButtonTapped() async { + state.isTwoFactorRequestInFlight = true + defer { + state.isTwoFactorRequestInFlight = false + } + do { + _ = try await di.authenticationClient.twoFactor(state.code, state.token) + di.loginDelegate?.didSucceedLogin() + } catch { + state.flow.$alert.select(with: error.localizedDescription) + } + } } @MainActor public protocol LoginDelegate { - - func didSucceedLogin() + + func didSucceedLogin() } -extension StoreDIValues { - - @StoreDIValue - public var loginDelegate: LoginDelegate? +public extension StoreDIValues { + + @StoreDIValue + var loginDelegate: LoginDelegate? } diff --git a/Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorSwiftUI/TwoFactorView.swift b/Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorSwiftUI/TwoFactorView.swift index e1590b8..5d52cd6 100644 --- a/Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorSwiftUI/TwoFactorView.swift +++ b/Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorSwiftUI/TwoFactorView.swift @@ -1,15 +1,15 @@ import AuthenticationClient -import VDStore -import VDFlow import SwiftUI import TwoFactorCore +import VDFlow +import VDStore public struct TwoFactorView: View { @ViewStore public var state: TwoFactor public init(store: Store) { - _state = ViewStore(store) + _state = ViewStore(store) } public var body: some View { @@ -17,7 +17,7 @@ public struct TwoFactorView: View { Text(#"To confirm the second factor enter "1234" into the form."#) Section { - TextField("1234", text: $state.binding.code) + TextField("1234", text: $state.binding.code) .keyboardType(.numberPad) } @@ -30,9 +30,9 @@ public struct TwoFactorView: View { UIApplication.shared.sendAction( #selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil ) - Task { - await $state.submitButtonTapped() - } + Task { + await $state.submitButtonTapped() + } } .disabled(state.isSubmitButtonDisabled) @@ -42,9 +42,9 @@ public struct TwoFactorView: View { } } } - .alert(state.flow.alert, isPresented: $state.binding.flow.isSelected(.alert)) { - Button("Ok") {} - } + .alert(state.flow.alert, isPresented: $state.binding.flow.isSelected(.alert)) { + Button("Ok") {} + } .disabled(state.isFormDisabled) .navigationTitle("Confirmation Code") } @@ -60,14 +60,14 @@ private extension TwoFactor { NavigationStack { TwoFactorView( store: Store(TwoFactor(token: "deadbeef")) - .transformDI { - $0.authenticationClient.login = { @Sendable _, _ in - AuthenticationResponse(token: "deadbeef", twoFactorRequired: false) - } - $0.authenticationClient.twoFactor = { @Sendable _, _ in - AuthenticationResponse(token: "deadbeef", twoFactorRequired: false) - } - } + .transformDI { + $0.authenticationClient.login = { @Sendable _, _ in + AuthenticationResponse(token: "deadbeef", twoFactorRequired: false) + } + $0.authenticationClient.twoFactor = { @Sendable _, _ in + AuthenticationResponse(token: "deadbeef", twoFactorRequired: false) + } + } ) } } diff --git a/Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorUIKit/TwoFactorViewController.swift b/Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorUIKit/TwoFactorViewController.swift index 199318b..92f16ac 100644 --- a/Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorUIKit/TwoFactorViewController.swift +++ b/Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorUIKit/TwoFactorViewController.swift @@ -1,12 +1,12 @@ -import VDStore +import Combine import TwoFactorCore import UIKit -import Combine +import VDStore public final class TwoFactorViewController: UIViewController { public let store: Store - private var cancellableSet: Set = [] + private var cancellableSet: Set = [] public init(store: Store) { self.store = store @@ -63,36 +63,36 @@ public final class TwoFactorViewController: UIViewController { var alertController: UIAlertController? - store.publisher - .removeDuplicates() - .sink { [weak self] state in - guard let self else { return } - activityIndicator.isHidden = state.isActivityIndicatorHidden - codeTextField.text = state.code - loginButton.isEnabled = state.isLoginButtonEnabled - - if state.flow.selected == .alert, - alertController == nil - { - alertController = UIAlertController(title: state.flow.alert, message: nil, preferredStyle: .alert) - alertController?.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil)) - present(alertController!, animated: true, completion: nil) - } else if state.flow.selected != .alert, alertController != nil { - alertController?.dismiss(animated: true) - alertController = nil - } - } - .store(in: &cancellableSet) + store.publisher + .removeDuplicates() + .sink { [weak self] state in + guard let self else { return } + activityIndicator.isHidden = state.isActivityIndicatorHidden + codeTextField.text = state.code + loginButton.isEnabled = state.isLoginButtonEnabled + + if state.flow.selected == .alert, + alertController == nil + { + alertController = UIAlertController(title: state.flow.alert, message: nil, preferredStyle: .alert) + alertController?.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil)) + present(alertController!, animated: true, completion: nil) + } else if state.flow.selected != .alert, alertController != nil { + alertController?.dismiss(animated: true) + alertController = nil + } + } + .store(in: &cancellableSet) } @objc private func codeTextFieldChanged(sender: UITextField) { - store.state.code = sender.text ?? "" + store.state.code = sender.text ?? "" } @objc private func loginButtonTapped() { - Task { - await store.submitButtonTapped() - } + Task { + await store.submitButtonTapped() + } } } diff --git a/Examples/TicTacToe/tic-tac-toe/Tests/AppCoreTests/AppCoreTests.swift b/Examples/TicTacToe/tic-tac-toe/Tests/AppCoreTests/AppCoreTests.swift index dd43d2d..656ee79 100644 --- a/Examples/TicTacToe/tic-tac-toe/Tests/AppCoreTests/AppCoreTests.swift +++ b/Examples/TicTacToe/tic-tac-toe/Tests/AppCoreTests/AppCoreTests.swift @@ -1,9 +1,9 @@ import AppCore import AuthenticationClient -import VDStore import LoginCore import NewGameCore import TwoFactorCore +import VDStore import XCTest final class AppCoreTests: XCTestCase { diff --git a/Examples/TicTacToe/tic-tac-toe/Tests/GameCoreTests/GameCoreTests.swift b/Examples/TicTacToe/tic-tac-toe/Tests/GameCoreTests/GameCoreTests.swift index 164dec5..b3934c8 100644 --- a/Examples/TicTacToe/tic-tac-toe/Tests/GameCoreTests/GameCoreTests.swift +++ b/Examples/TicTacToe/tic-tac-toe/Tests/GameCoreTests/GameCoreTests.swift @@ -1,5 +1,5 @@ -import VDStore import GameCore +import VDStore import XCTest final class GameCoreTests: XCTestCase { diff --git a/Examples/TicTacToe/tic-tac-toe/Tests/LoginCoreTests/LoginCoreTests.swift b/Examples/TicTacToe/tic-tac-toe/Tests/LoginCoreTests/LoginCoreTests.swift index 9ab506d..4f8ab8b 100644 --- a/Examples/TicTacToe/tic-tac-toe/Tests/LoginCoreTests/LoginCoreTests.swift +++ b/Examples/TicTacToe/tic-tac-toe/Tests/LoginCoreTests/LoginCoreTests.swift @@ -1,7 +1,7 @@ import AuthenticationClient -import VDStore import LoginCore import TwoFactorCore +import VDStore import XCTest final class LoginCoreTests: XCTestCase { diff --git a/Examples/TicTacToe/tic-tac-toe/Tests/NewGameCoreTests/NewGameCoreTests.swift b/Examples/TicTacToe/tic-tac-toe/Tests/NewGameCoreTests/NewGameCoreTests.swift index a43a218..ac4256e 100644 --- a/Examples/TicTacToe/tic-tac-toe/Tests/NewGameCoreTests/NewGameCoreTests.swift +++ b/Examples/TicTacToe/tic-tac-toe/Tests/NewGameCoreTests/NewGameCoreTests.swift @@ -1,6 +1,6 @@ -import VDStore import GameCore import NewGameCore +import VDStore import XCTest final class NewGameCoreTests: XCTestCase { diff --git a/Examples/TicTacToe/tic-tac-toe/Tests/TwoFactorCoreTests/TwoFactorCoreTests.swift b/Examples/TicTacToe/tic-tac-toe/Tests/TwoFactorCoreTests/TwoFactorCoreTests.swift index 8f918d6..a84a183 100644 --- a/Examples/TicTacToe/tic-tac-toe/Tests/TwoFactorCoreTests/TwoFactorCoreTests.swift +++ b/Examples/TicTacToe/tic-tac-toe/Tests/TwoFactorCoreTests/TwoFactorCoreTests.swift @@ -1,6 +1,6 @@ import AuthenticationClient -import VDStore import TwoFactorCore +import VDStore import XCTest final class TwoFactorCoreTests: XCTestCase { diff --git a/README.md b/README.md index f2bec2a..b092476 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ import PackageDescription let package = Package( name: "SomeProject", dependencies: [ - .package(url: "https://github.com/dankinsoid/VDStore.git", from: "0.27.0") + .package(url: "https://github.com/dankinsoid/VDStore.git", from: "0.28.0") ], targets: [ .target(name: "SomeProject", dependencies: ["VDStore"]) diff --git a/Sources/VDStore/Dependencies/Clock.swift b/Sources/VDStore/Dependencies/Clock.swift index 47c9cb3..98b184e 100644 --- a/Sources/VDStore/Dependencies/Clock.swift +++ b/Sources/VDStore/Dependencies/Clock.swift @@ -1,25 +1,25 @@ -#if (canImport(RegexBuilder) || !os(macOS) && !targetEnvironment(macCatalyst)) +#if canImport(RegexBuilder) || !os(macOS) && !targetEnvironment(macCatalyst) @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) -extension StoreDIValues { +public extension StoreDIValues { - /// The current clock that features should use when a `ContinuousClock` would be appropriate. - /// - /// By default, a live `ContinuousClock` is supplied. - /// - /// See ``suspendingClock`` to override a feature's `SuspendingClock`, instead. - public var continuousClock: any Clock { - get { get(\.continuousClock, or: ContinuousClock()) } - set { set(\.continuousClock, newValue) } - } - - /// The current clock that features should use when a `SuspendingClock` would be appropriate. - /// - /// By default, a live `SuspendingClock` is supplied. - /// - /// See ``continuousClock`` to override a feature's `ContinuousClock`, instead. - public var suspendingClock: any Clock { - get { get(\.suspendingClock, or: SuspendingClock()) } - set { set(\.suspendingClock, newValue) } - } + /// The current clock that features should use when a `ContinuousClock` would be appropriate. + /// + /// By default, a live `ContinuousClock` is supplied. + /// + /// See ``suspendingClock`` to override a feature's `SuspendingClock`, instead. + var continuousClock: any Clock { + get { get(\.continuousClock, or: ContinuousClock()) } + set { set(\.continuousClock, newValue) } + } + + /// The current clock that features should use when a `SuspendingClock` would be appropriate. + /// + /// By default, a live `SuspendingClock` is supplied. + /// + /// See ``continuousClock`` to override a feature's `ContinuousClock`, instead. + var suspendingClock: any Clock { + get { get(\.suspendingClock, or: SuspendingClock()) } + set { set(\.suspendingClock, newValue) } + } } #endif diff --git a/Sources/VDStore/Dependencies/Date.swift b/Sources/VDStore/Dependencies/Date.swift index 9b79448..5ef54f7 100644 --- a/Sources/VDStore/Dependencies/Date.swift +++ b/Sources/VDStore/Dependencies/Date.swift @@ -1,16 +1,16 @@ import Foundation -extension StoreDIValues { - - /// A dependency that returns the current date. - /// - /// By default, a "live" generator is supplied, which returns the current system date when called - /// by invoking `Date.init` under the hood. When used in tests, an "unimplemented" generator that - /// additionally reports test failures is supplied, unless explicitly overridden. - public var date: DateGenerator { - get { get(\.date, or: DateGenerator { Date() }) } - set { set(\.date, newValue) } - } +public extension StoreDIValues { + + /// A dependency that returns the current date. + /// + /// By default, a "live" generator is supplied, which returns the current system date when called + /// by invoking `Date.init` under the hood. When used in tests, an "unimplemented" generator that + /// additionally reports test failures is supplied, unless explicitly overridden. + var date: DateGenerator { + get { get(\.date, or: DateGenerator { Date() }) } + set { set(\.date, newValue) } + } } /// A dependency that generates a date. @@ -18,30 +18,30 @@ extension StoreDIValues { /// See ``StoreDIValues/date`` for more information. public struct DateGenerator: Sendable { - private var generate: @Sendable () -> Date - - /// A generator that returns a constant date. - /// - /// - Parameter now: A date to return. - /// - Returns: A generator that always returns the given date. - public static func constant(_ now: Date) -> Self { - Self { now } - } - - /// The current date. - public var now: Date { - get { self.generate() } - set { self.generate = { newValue } } - } - - /// Initializes a date generator that generates a date from a closure. - /// - /// - Parameter generate: A closure that returns the current date when called. - public init(_ generate: @escaping @Sendable () -> Date) { - self.generate = generate - } - - public func callAsFunction() -> Date { - self.generate() - } + private var generate: @Sendable () -> Date + + /// A generator that returns a constant date. + /// + /// - Parameter now: A date to return. + /// - Returns: A generator that always returns the given date. + public static func constant(_ now: Date) -> Self { + Self { now } + } + + /// The current date. + public var now: Date { + get { generate() } + set { generate = { newValue } } + } + + /// Initializes a date generator that generates a date from a closure. + /// + /// - Parameter generate: A closure that returns the current date when called. + public init(_ generate: @escaping @Sendable () -> Date) { + self.generate = generate + } + + public func callAsFunction() -> Date { + generate() + } } diff --git a/Sources/VDStore/Dependencies/Dismiss.swift b/Sources/VDStore/Dependencies/Dismiss.swift index b663403..e1ecaf1 100644 --- a/Sources/VDStore/Dependencies/Dismiss.swift +++ b/Sources/VDStore/Dependencies/Dismiss.swift @@ -1,60 +1,60 @@ import SwiftUI -extension StoreDIValues { - - public var dismiss: () -> Void { - get { get(\.dismiss, or: _dismiss) } - set { set(\.dismiss, newValue) } - } - - public var pop: () -> Void { - get { get(\.pop, or: _pop) } - set { set(\.pop, newValue) } - } +public extension StoreDIValues { + + var dismiss: () -> Void { + get { get(\.dismiss, or: _dismiss) } + set { set(\.dismiss, newValue) } + } + + var pop: () -> Void { + get { get(\.pop, or: _pop) } + set { set(\.pop, newValue) } + } } private func _dismiss() { - guard let root = UIApplication.shared.windows.first(where: \.isKeyWindow)?.rootViewController else { - return - } - let topController = root.topPresented - if topController.presentingViewController != nil { - topController.dismiss(animated: true) - } + guard let root = UIApplication.shared.windows.first(where: \.isKeyWindow)?.rootViewController else { + return + } + let topController = root.topPresented + if topController.presentingViewController != nil { + topController.dismiss(animated: true) + } } private func _pop() { - guard let root = UIApplication.shared.windows.first(where: \.isKeyWindow)?.rootViewController else { - return - } - let topController = root.topPresented - if let navController = topController.navController, navController.viewControllers.count > 1 { - navController.popViewController(animated: true) - } + guard let root = UIApplication.shared.windows.first(where: \.isKeyWindow)?.rootViewController else { + return + } + let topController = root.topPresented + if let navController = topController.navController, navController.viewControllers.count > 1 { + navController.popViewController(animated: true) + } } private extension UIViewController { - - var topPresented: UIViewController { - presentedViewController?.topPresented ?? self - } - - var navController: UINavigationController? { - (self as? UINavigationController) ?? navigationController ?? children.navController - } + + var topPresented: UIViewController { + presentedViewController?.topPresented ?? self + } + + var navController: UINavigationController? { + (self as? UINavigationController) ?? navigationController ?? children.navController + } } private extension [UIViewController] { - - var navController: UINavigationController? { - for viewController in self { - if let navController = viewController as? UINavigationController { - return navController - } - if let navController = viewController.children.navController { - return navController - } - } - return nil - } + + var navController: UINavigationController? { + for viewController in self { + if let navController = viewController as? UINavigationController { + return navController + } + if let navController = viewController.children.navController { + return navController + } + } + return nil + } } diff --git a/Sources/VDStore/Store.swift b/Sources/VDStore/Store.swift index 39cd964..baefdb6 100644 --- a/Sources/VDStore/Store.swift +++ b/Sources/VDStore/Store.swift @@ -319,11 +319,11 @@ public struct Store: Sendable { } public nonisolated func withDIValues(operation: () throws -> T) rethrows -> T { - try StoreDIValues.$current.withValue(diModifier, operation: operation) + try StoreDIValues.$current.withValue(diModifier, operation: operation) } public nonisolated func withDIValues(operation: () async throws -> T) async rethrows -> T { - try await StoreDIValues.$current.withValue(diModifier, operation: operation) + try await StoreDIValues.$current.withValue(diModifier, operation: operation) } func forceUpdateIfNeeded() { diff --git a/Sources/VDStore/Utils/DIPublisher.swift b/Sources/VDStore/Utils/DIPublisher.swift index 8255665..c6cabf3 100644 --- a/Sources/VDStore/Utils/DIPublisher.swift +++ b/Sources/VDStore/Utils/DIPublisher.swift @@ -49,7 +49,7 @@ struct DISubscriber: Subscriber { func execute(_ operation: () -> T) -> T { // StoreDIValues.$current.withValue(values) { - operation() + operation() // } } } diff --git a/Sources/VDStore/ViewStore.swift b/Sources/VDStore/ViewStore.swift index 74ff1f9..66707fc 100644 --- a/Sources/VDStore/ViewStore.swift +++ b/Sources/VDStore/ViewStore.swift @@ -34,8 +34,8 @@ public struct ViewStore: DynamicProperty { projectedValue.binding } - public init(_ store: Store) { - if store.di.isViewStore { + public init(_ store: Store) { + if store.di.isViewStore { property = .store(store) } else { property = .stateObject(