Created
August 6, 2025 18:28
-
-
Save treastrain/9ffa5fbc000a2b8b3d355b17ddebfa23 to your computer and use it in GitHub Desktop.
https://github.com/Kuniwak/BLEMacroApp/blob/master/DESIGN.md の Swift 6.2 対応版(`ViewBinding` に `@dynamicMemberLookup` 付き)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Combine | |
import SwiftUI | |
public protocol StateProtocol: Sendable {} | |
public protocol StateMachineProtocol<State>: Actor { | |
associatedtype State: StateProtocol | |
nonisolated var state: State { get } | |
nonisolated var stateDidChange: AnyPublisher<State, Never> { get } | |
} | |
public typealias ConcurrentValueSubject = CurrentValueSubject | |
extension ConcurrentValueSubject: @retroactive @unchecked Sendable where Output: Sendable, Failure: Sendable {} | |
/* | |
public final class ConcurrentValueSubject<Output, Failure: Error>: Subject, Sendable { | |
private nonisolated(unsafe) let subject: CurrentValueSubject<Output, Failure> | |
public init(_ value: Output) { | |
subject = CurrentValueSubject(value) | |
} | |
public nonisolated var value: Output { | |
get { subject.value } | |
set { subject.value = newValue } | |
} | |
public nonisolated func send(_ value: Output) { | |
subject.send(value) | |
} | |
public nonisolated func send(completion: Subscribers.Completion<Failure>) { | |
subject.send(completion: completion) | |
} | |
public nonisolated func send(subscription: any Subscription) { | |
subject.send(subscription: subscription) | |
} | |
public nonisolated func receive<S: Subscriber>(subscriber: S) | |
where Failure == S.Failure, Output == S.Input { | |
subject.receive(subscriber: subscriber) | |
} | |
} | |
*/ | |
@dynamicMemberLookup | |
public final class ViewBinding<State>: ObservableObject { | |
@Published public private(set) var state: State | |
private var cancellable: AnyCancellable? | |
public init<StateMachine: StateMachineProtocol>(source: StateMachine) | |
where StateMachine.State == State { | |
self.state = source.state | |
self.cancellable = source.stateDidChange | |
.receive(on: DispatchQueue.main) | |
.assign(to: \.state, on: self) | |
} | |
public subscript<T>(dynamicMember keyPath: KeyPath<State, T>) -> T { | |
state[keyPath: keyPath] | |
} | |
} | |
public protocol ExampleModelProtocol: StateMachineProtocol<ExampleState> { | |
nonisolated func eventA() | |
nonisolated func eventB(param: Int) | |
} | |
public enum ExampleState: StateProtocol { | |
case state1(message: String) | |
case state2(message: String) | |
public var isStable: Bool { | |
switch self { | |
case .state1: true | |
case .state2: false | |
} | |
} | |
public var message: String { | |
switch self { | |
case .state1(let message): message | |
case .state2(let message): message | |
} | |
} | |
} | |
public actor ExampleModel: ExampleModelProtocol { | |
private let stateDidChangeSubject: | |
ConcurrentValueSubject<ExampleState, Never> | |
public nonisolated var state: ExampleState { stateDidChangeSubject.value } | |
public nonisolated(unsafe) let stateDidChange: AnyPublisher<ExampleState, Never> | |
public init(startsWith initialState: ExampleState) { | |
guard initialState.isStable else { | |
preconditionFailure("state must be stable") | |
} | |
let stateDidChangeSubject = ConcurrentValueSubject<ExampleState, Never>( | |
initialState | |
) | |
self.stateDidChangeSubject = stateDidChangeSubject | |
stateDidChange = stateDidChangeSubject.eraseToAnyPublisher() | |
} | |
public nonisolated func eventA() { | |
Task { | |
await self.eventAInternal() | |
} | |
} | |
private func eventAInternal() async { | |
stateDidChangeSubject.value = .state1(message: "event-a") | |
} | |
public nonisolated func eventB(param: Int) { | |
Task { | |
await self.eventBInternal(param: param) | |
} | |
} | |
private func eventBInternal(param: Int) async { | |
stateDidChangeSubject.value = .state2(message: "event-b-\(param)") | |
} | |
} | |
public struct ExampleView: View { | |
private let model: any ExampleModelProtocol | |
@StateObject private var state: ViewBinding<ExampleState> | |
public init(observing model: any ExampleModelProtocol) { | |
self.model = model | |
self._state = StateObject(wrappedValue: ViewBinding(source: model)) | |
} | |
public var body: some View { | |
VStack { | |
Text(state.message) | |
Text("State: \(state.isStable ? "Stable" : "Unstable")") | |
Button("Event A") { | |
model.eventA() | |
} | |
Button("Event B") { | |
model.eventB(param: Int(Date.now.timeIntervalSince1970)) | |
} | |
} | |
} | |
} | |
struct ContentView: View { | |
let model = ExampleModel(startsWith: .state1(message: "state1")) | |
var body: some View { | |
ExampleView(observing: model) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Combine ではなく Swift Observation を採用した版: https://gist.github.com/treastrain/eaf03702c6e489c2c9eac01025c40c6f