Skip to content

Instantly share code, notes, and snippets.

@treastrain
Created August 6, 2025 21:00
Show Gist options
  • Save treastrain/eaf03702c6e489c2c9eac01025c40c6f to your computer and use it in GitHub Desktop.
Save treastrain/eaf03702c6e489c2c9eac01025c40c6f to your computer and use it in GitHub Desktop.
//
// ContentView.swift
// Playground
//
// Created by treastrain on 2025/08/07.
//
public import Observation
import SwiftUI
// MARK: - ObservableValue
@Observable
public final class ObservableValue<Value> {
private(set) var value: Value
public init(_ value: Value) {
self.value = value
}
public func update(_ newValue: Value) {
value = newValue
}
}
extension ObservableValue: @unchecked Sendable where Value: Sendable {}
// MARK: - MainActorObservableValue
@Observable @MainActor @dynamicMemberLookup
public final class MainActorObservableValue<Value> {
public private(set) var value: Value
@ObservationIgnored private var task: Task<(), Never>?
public init<O: Observable>(base: O, for keyPath: KeyPath<O, Value>) where Value: Sendable {
value = base[keyPath: keyPath]
task = Task { [weak self] in
for await newValue in Observations({ base[keyPath: keyPath] }) {
self?.value = newValue
}
}
}
isolated deinit {
task?.cancel()
}
public subscript<T>(dynamicMember keyPath: KeyPath<Value, T>) -> T {
value[keyPath: keyPath]
}
}
// MARK: - StateMachine
public protocol StateMachine<State>: Actor {
associatedtype State: Sendable
nonisolated var state: ObservableValue<State> { get }
}
// MARK: - MainActorObservableValue initializer for StateMachine
extension MainActorObservableValue {
public convenience init<S: StateMachine>(base: S) where S.State == Value {
self.init(base: base.state, for: \.value)
}
}
// MARK: - Examples
enum ExampleState: Sendable {
case state1(message: String)
case state2(message: String)
case state3
var isStable: Bool {
switch self {
case .state1, .state2: true
case .state3: false
}
}
var message: String {
switch self {
case .state1(let message): message
case .state2(let message): message
case .state3: "Loading..."
}
}
}
protocol ExampleObjectProtocol: StateMachine<ExampleState> {
nonisolated func eventA()
nonisolated func eventB(param: Int)
}
actor ExampleObject: ExampleObjectProtocol {
nonisolated let state: ObservableValue<ExampleState>
init(initialState: ExampleState) {
state = ObservableValue(initialState)
}
nonisolated func eventA() {
Task {
await eventAInternal()
}
}
private func eventAInternal() async {
state.update(.state3)
try? await Task.sleep(for: .seconds(1))
state.update(.state1(message: "event-a"))
}
nonisolated func eventB(param: Int) {
Task {
await eventBInternal(param: param)
}
}
private func eventBInternal(param: Int) async {
state.update(.state3)
try? await Task.sleep(for: .seconds(1))
state.update(.state2(message: "event-b-\(param)"))
}
}
struct ExampleView<Object: ExampleObjectProtocol>: View {
private let object: Object
private let state: MainActorObservableValue<Object.State>
init(observing object: Object) {
self.object = object
self.state = MainActorObservableValue(base: object)
}
var body: some View {
VStack {
Text(state.message)
Text("State: \(state.isStable ? "Stable" : "Unstable")")
Button("Event A") {
object.eventA()
}
Button("Event B") {
object.eventB(param: Int(Date.now.timeIntervalSince1970))
}
}
.disabled(!state.isStable)
}
}
struct ContentView: View {
let model = ExampleObject(initialState: .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