Skip to content

Instantly share code, notes, and snippets.

@treastrain
Created August 6, 2025 18:28
Show Gist options
  • Save treastrain/9ffa5fbc000a2b8b3d355b17ddebfa23 to your computer and use it in GitHub Desktop.
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` 付き)
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)
}
}
@treastrain
Copy link
Author

Combine ではなく Swift Observation を採用した版: https://gist.github.com/treastrain/eaf03702c6e489c2c9eac01025c40c6f

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment