SwiftUI これからのアーキテクチャはこれに決めた「ActorStreamMVVM」

未分類

ActorStreamMVVM 2025

import SwiftUI

/// Actor + AsyncStream ベースの共有マネージャ
actor BaseManager<T> {
    private var continuation: AsyncStream<Result<T, Error>>.Continuation
    let stream: AsyncStream<Result<T, Error>>

    init() {
        var cont: AsyncStream<Result<T, Error>>.Continuation!
        self.stream = AsyncStream { continuation in
            cont = continuation
        }
        self.continuation = cont
    }

    func send(_ value: T) {
        continuation.yield(.success(value))
    }

    func sendError(_ error: Error) {
        continuation.yield(.failure(error))
    }

    func finish() {
        continuation.finish()
    }
}


@MainActor
final class BaseViewModel<T>: ObservableObject {
    @Published var latestValue: T?
    @Published var lastError: String?
    
    private let manager: BaseManager<T>
    private var task: Task<Void, Never>?
    
    init(manager: BaseManager<T>) {
        self.manager = manager
        startObserving()
    }
    
    private func startObserving() {
        task = Task {
            for await result in await manager.stream {
                switch result {
                case .success(let value):
                    latestValue = value
                case .failure(let error):
                    lastError = error.localizedDescription
                }
            }
        }
    }
    
    func stopObserving() {
        task?.cancel()
        task = nil
    }
}

struct BaseView<T: CustomStringConvertible>: View {
    @StateObject private var viewModel: BaseViewModel<T>
    
    init(manager: BaseManager<T>) {
        _viewModel = StateObject(wrappedValue: BaseViewModel(manager: manager))
    }
    
    var body: some View {
        VStack(spacing: 20) {
            if let value = viewModel.latestValue {
                Text("最近値: \(value.description)")
                    .font(.headline)
            } else {
                Text("まだデータなし")
                    .foregroundColor(.secondary)
            }
            
            if let error = viewModel.lastError {
                Text("エラー: \(error)")
                    .foregroundColor(.red)
            }
            
            Button("購読停止") {
                viewModel.stopObserving()
            }
            .padding()
            .background(Color.blue.opacity(0.2))
            .cornerRadius(8)
        }
        .padding()
    }
}

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
        .padding()
    }
}

#Preview {
    ContentView()
}


@main
struct ActorStreamMVVMComplitedTemplateApp: App {
    private static let manager = BaseManager<String>()
    // OtherBaseManager
//    private static let otherManager = BaseManager<Int>()
    
    var body: some Scene {
        WindowGroup {
            BaseView(manager: Self.manager)
                .task {
                    // デモ用: 成功と失敗を交互に送信
                    for i in 1...10 {
                        if i % 3 == 0 {
                            await Self.manager.sendError(NSError(domain: "DemoError", code: i, userInfo: nil))
                        } else {
                            await Self.manager.send("データ #\(i)")
                        }
                        try? await Task.sleep(nanoseconds: 1_000_000_000)
                    }
                    await Self.manager.finish()
                }
        }
    }
}

プロンプトで伝えたい場合は、

私が愛用するアーキテクチャプロンプト:
 # Actor + AsyncStream + MVVM アーキテクチャテンプレート

 以下のSwiftUIアーキテクチャパターンを実装してください:

 ## アーキテクチャ構成

 ### 1. BaseManager<T> (Actor層)
 - `actor`キーワードを使用したスレッドセーフなデータマネージャ
 - `AsyncStream<Result<T, Error>>`でデータストリームを提供
 - メソッド:
   - `send(_ value: T)`: 成功値を送信
   - `sendError(_ error: Error)`: エラーを送信
   - `finish()`: ストリーム終了

 ### 2. BaseViewModel<T> (ViewModel層)
 - `@MainActor`でメインスレッド実行を保証
 - `ObservableObject`でSwiftUIとの連携
 - プロパティ:
   - `@Published var latestValue: T?`: 最新の成功値
   - `@Published var lastError: String?`: エラーメッセージ
 - ActorからのAsyncStreamを購読し、UIを更新

 ### 3. BaseView<T> (View層)
 - ジェネリック型`T: CustomStringConvertible`を受け取る
 - `@StateObject`でViewModelを管理
 - データ表示とエラーハンドリングのUI

 ## 実装要件

 ```swift
 // この構造に従って実装してください:

 actor YourManager<T> {
     // BaseManager<T>を継承または同様の構造
 }

 @MainActor
 final class YourViewModel<T>: ObservableObject {
     // BaseViewModel<T>を継承または同様の構造
 }

 struct YourView<T: CustomStringConvertible>: View {
     // BaseView<T>を参考にした実装
 }
 */

コメント

タイトルとURLをコピーしました