Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

@Published var をメインスレッドで受け取る方法

解決したいこと

Modelを Published var として ViewModel内に保持し、ViewからObserveしています。

モデルの処理がバックグラウンドスレッドに入った時に、モデルの値をPublishすると、Xcodeのスレッドチェッカーが反応して

Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.

というワーニングが出ます。

ContentView.swift
@StateObject var viewModel = ViewModel()
ViewModel.swift
class ViewModel: ObservableObject {

    @Published var model = Model()
    var thisValue:String {
        return model.thisValue // この値をViewで使いたい
    }

Model.swift
struct Model {
    var thisValue:String = "value" // この値をバックグラウンドスレッドで変更して、メインスレッドでパブリッシュしたい

メインスレッドでモデルの値を受け取る方法をしりたいのですが、
なかなかわからず質問させていただきました。

教えていただけr教えていただけるととても嬉しいです。

4

4Answer

適当に作った参考例貼っておきますね^^

View
struct TestView: View {
    @StateObject var viewModel = ViewModel()
    var body: some View {
        VStack{
            TextField("入力してください。", text: $viewModel.viewText)
            Button {
                viewModel.viewModelFunc()
            } label: {
                Text("判定:"+viewModel.result)
            }
        }
    }
}
viewModel
class ViewModel: ObservableObject {
    private var model = Model()
    @Published var result = ""
    @Published var viewText = ""
    
    func viewModelFunc() {
        model.modelFunc(thisValue: viewText) { [weak self] error in
            self?.result = error
        } success: { [weak self] success in
            self?.result = success
        }
    }
}
model
class Model {
    func modelFunc(thisValue: String, failure: @escaping (String) -> Void, success: @escaping (String) -> Void) {
        if thisValue.isEmpty {
            return DispatchQueue.main.async {
                failure("error")
            }
        } else {
            return DispatchQueue.main.async {
                success("success")
            }
        }
    }
}

これでthisValueをどのスレッドで取得しても返すのはメインスレッドになるかと思います。
また以上のものだとviewModelがmodelに依存しているので必要に応じて疎結合(DI)の処理をすると良いかと^^

2Like

Comments

  1. @john-rocky

    Questioner

    ありがとうございます。

    ご提示いただいたように、ModelをClassにすることで解決しました。

    コードまで書いていただいて、本当にありがとうございます。

上記の例ですとそのまま使用してもエラーは出ないと思いますので具体的な解決策案を出すことができませんが、model.thisValueに対してDispatchQueue.main.asyncで囲ってあげるとエラーは消えそうかなと思います。(非同期処理でメインスレッドに返してあげる際に書きます。)
書き方としては以下のような感じです。

DispatchQueue.main.async {
                    //model.thisValueなどここにメインスレッドで返したい結果を書く
                }

参考になりそうな記事も貼っておきます。
SwiftUIでObservedObjectなオブジェクトでメインスレッドで実行するよう設定するSwiftUIらしい書き方が知りたい

1Like

Comments

  1. @john-rocky

    Questioner

    ありがとうございます。
    DispatchQueue.main.asyncのクロージャの中から値を返す方法がわからなくて、詰まっております。リンクも熟読いたします。

This answer has been deleted for violation of our Terms of Service.

次のコードとXcode15.4環境で検証してみます。

ContentView.swift
import SwiftUI

class ViewModel: ObservableObject {
  @Published var model: Model = .init() // <- warning
  //@MainActor
  func foo(_ a: String) async {
    try! await Task.sleep(nanoseconds: 500_000_000)
    model.thisValue = a
  }
  var thisValue: String {
    model.thisValue
  }
}
struct Model {
  var thisValue: String = "OK"
}
struct ContentView: View {
  @StateObject var viewModel: ViewModel = .init()
  var body: some View {
    VStack {
      Image(systemName: "globe")
        .imageScale(.large)
        .foregroundStyle(.tint)
      Text("Hello, world! \(viewModel.thisValue)")
    }
    .task {
      await viewModel.foo("NG")
    }
    .padding()
  }
}

Debug実行時にwarning(Publishing changes from...)がEditor(紫色)とConsole(TYPE=Fault)に出ます。コメント//@MainActorを外せば出なくなります。@MainActorは、これを付与したメソッドをMainActorクラスのプロパティへ変えるプロパティラッパーで、ドキュメントにはMainActorクラスはthe main dispatch queueと等価だと記載があります。
このプロパティラッパーはスケジューラー(OS)にMain Threadのイベントループ(RunLoop)の待ち行列(Queue)へメソッドの実行を登録(Dispatch)するように依頼してます。
プロジェクトを新規作成してContentView.swiftを上記内容に書き換えて試してみて下さい。
別解もあります。コメントはそのままにコードを書き換えます。

-     model.thisValue = a
+     DispatchQueue.main.async { [weak self] in
+      self?.model.thisValue = a
+    }

swift - Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) - Stack Overflow
やさしい @MainActor #Swift - Qiita

追記:
次の環境だと@MainActorの付与無しでもwarningが出ない。
struct ContentViewが@MainActorが付与されたのと同等の振る舞いをしている様だ。

@MainActor @preconcurrency
protocol View

Xcode付属のDocumentsによると以前には無かった@MainActorがViewに付与されている。

$xcodebuild -version
Xcode 16.0
Build version 16A5230g
0Like

Your answer might help someone💌