【Swift】 @Observable に @MainActor をつけると おまえは 実質 ViewModel だろ

Swift のアーキテクチャパターンまわりの新しい記事を眺めてると、

@Observable には、明示的に @MainActor をつけましょう。

その親 View にも @MainActor をつけるのが推奨されます。

などと書いてるのを多く見かける。

「全部 UI スレッドでは?」

などと思いながら、あれこれやってみました。


import SwiftUI

@MainActor
struct TestWebRequest: View {
  @State private var responseBody = ""

  //private let url = URL(string: "https://wttr.in/?format=3")!

  //private var background = Background()
  private var viewModel = TestViewModel()

  var body: some View {
//    Text("\(responseBody)")
//    Text("\(background.responseBody)")
    Text("\(viewModel.responseBody)")

//      .onAppear { // OK
//        responseBody = try! String(contentsOf: url)
//      }

//      .onAppear {
//        // Passing argument of non-sendable type '(any URLSessionTaskDelegate)?'
//        // outside of main actor-isolated context may introduce data races
//        Task {
//          let (data, _) = try! await URLSession.shared.data(from: url)
//          responseBody = String(data: data, encoding: .utf8)!
//        }
//      }

//      .task {
//        // Passing argument of non-sendable type '(any URLSessionTaskDelegate)?'
//        // outside of main actor-isolated context may introduce data races
//        let (data, _) = try! await URLSession.shared.data(from: url)
//        responseBody = String(data: data, encoding: .utf8)!
//      }

//        .onAppear {
//          Task {
//            responseBody = await Background.request(url: url)
//          }
//        }

//      .task {
//        responseBody = await background.request(url: url)
//      }

//      .task {
//        responseBody = await background.request(url: url)
//      }

      .task {
        //await background.request(url: url)
        await viewModel.request()
      }
  }
}

@MainActor
@Observable
final class TestViewModel {
  var responseBody = ""

  private let background = Background()

  func request() async {
    responseBody = await background.request()
  }
}

//class Background: @unchecked Sendable {
final class Background: Sendable {
  private let url = URL(string: "https://wttr.in/?format=3")!

  func request() async -> String {
    let (data, _) = try! await URLSession.shared.data(from: url)
    return String(data: data, encoding: .utf8)!
  }
}

//actor Background {
//  private let url = URL(string: "https://wttr.in/?format=3")!
//
//  nonisolated func request() async -> String {
//    let (data, _) = try! await URLSession.shared.data(from: url)
//    return String(data: data, encoding: .utf8)!
//  }
//}

#Preview {
  TestWebRequest()
}


こうなりますか。

そろそろ消えてほしかった ViewModel。

いや、それとも、

おすすめ定番パターンを Apple はアナウンスしてもよくない ?

いろいろ混乱します。



【Swift6 Concurrency】 新しい @Observable クラスの書き方を知らないやつwww

私です。

将来に向けて厳しくした Xcode 設定でのどうなるかの検証です。


サンプルコード。

最もよくある非同期処理のパターンの一つだと思います。

ただ WEB リクエストしてそれを表示するやつ。



👉 【SwiftUI】シンプルに HTTPリクエスト でお天気情報取得 hatena-bookmark

どうなるか。

 

🧑🏻‍💻 Passing argument of non-sendable type 'Requester' outside of main actor-isolated context may introduce data races

一応、動きますが。

こんな警告でました。

Passing argument of non-sendable type 'Requester' outside of main actor-isolated context may introduce data races

なんなんですかね。

なんかおかしいですか。

 

🧑🏻‍💻 Non-final class 'Requester' cannot conform to 'Sendable'; use '@unchecked Sendable'

無邪気に Sendable を継承します。

一行のみの変更です。


class Requester: Sendable {

Non-final class 'Requester' cannot conform to 'Sendable'; use '@unchecked Sendable'

と出ました。

よって、結局、以下の2つのどちらかで警告は消えます。


final class Requester: Sendable {


class Requester: @unchecked Sendable {

ここら辺でこれだけつまずいてるのだけども。

👉 Sendable | Apple Developer Documentation hatena-bookmark

 

🤔 気持ち悪さ

よくこの @Observable クラスが、

いわゆる「MVVM でいうところの ModelView」

となってるコードをよく見かけます。

これが「アクター境界」を越えていいの ?

 

🤔 関連記事



【Swift】最もシンプルに実行時間を計測する

これくらいでいいよなあ。


let start = Date()
try? await Task.sleep(for: .milliseconds(1234))
let end = -start.timeIntervalSinceNow
print(String(format: "%.3f", end))
// 1.246



【SwiftData】@Query の検索条件や並び順を変更する

@Query は Observable で便利です。

引数に、検索条件や並び順を書いておけば、

監視して変更があれば、更新してくれます。


👉 【SwiftData】@Query の引数と Descriptor の関係 hatena-bookmark

しかし、この検索条件や並び順の基準は

ユーザーの意図によって変更の必要があることのほうが多いです。

どうやって、書き換えるか。


struct UserListView: View {
  @Query var users: [User]
  
  init(sort: SortDescriptor<User>) {
    _users = Query(sort: [sort])

👉 How to dynamically change a query's sort order or predicate - a free SwiftData by Example tutorial hatena-bookmark


struct BirdsSearchResults<Content: View>: View {
  @Binding var searchText: String
  @Query private var birds: [Bird]
  private var content: (Bird) -> Content
    
  init(searchText: Binding<String>, @ViewBuilder content: @escaping (Bird) -> Content) {
    _searchText = searchText
    _birds = Query(sort: \.creationDate)
    self.content = content
  }

👉 sample-backyard-birds/Multiplatform/Birds/BirdsSearchResults.swift at 1843d5655bf884b501e2889ad9862ec58978fdbe · apple/sample-backyard-birds hatena-bookmark

どうやら、今現在としては、


_birds = Query(sort: \.creationDate)

というような

「_ (アンダースコア)」

を使った内部的な Query の書き換えが本筋といったところでしょうか。

Query の引数には、

SortDescriptorFetchDescriptor もあるので、

複雑な条件により抽出も並び替えも可能です。

👉 【SwiftData】@Query の引数と Descriptor の関係 hatena-bookmark

しかし、他の方法ないんですかね。


【Swift】URLSession で JSONを POST して JSON のレスポンスを受け取る

先日、紹介しましたこれ。

👉 httpbin.org hatena-bookmark


https://httpbin.org/anything/{anything}

に対して JSONを POST するとそのまま JSONで返ってきます。

キー名は「json」です。

※ 以下では POST してないので null。


{
  "args": {},
  "data": "",
  "files": {},
  "form": {},
  "headers": {
    "Accept": "application/json",
    "Accept-Encoding": "gzip, deflate, br, zstd",
    "Accept-Language": "ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7",
    "Content-Length": "0",
    "Host": "httpbin.org",
    "Origin": "https://httpbin.org",
    "Priority": "u=1, i",
    "Referer": "https://httpbin.org/",
    "Sec-Ch-Ua": "\"Google Chrome\";v=\"125\", \"Chromium\";v=\"125\", \"Not.A/Brand\";v=\"24\"",
    "Sec-Ch-Ua-Mobile": "?0",
    "Sec-Ch-Ua-Platform": "\"macOS\"",
    "Sec-Fetch-Dest": "empty",
    "Sec-Fetch-Mode": "cors",
    "Sec-Fetch-Site": "same-origin"
  },
  "json": null,  
  "method": "POST",
  "url": "https://httpbin.org/anything/{anything}"
}

練習してみます。

Web-API を使うには必須です。


struct User: Codable {
  var name: String
  var age: Int
}

struct Anything: Codable {
  var json: User
}


let session = URLSession.shared
let url = URL(string: "https://httpbin.org/anything/{anything}")!
let user = User(name: "John", age: 18)

var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.httpBody = try JSONEncoder().encode(user)

let (data, response) = try await session.data(for: request)
print((response as! HTTPURLResponse).statusCode)
//print(String(data: data, encoding: .utf8)!)

let receivedUser = (try JSONDecoder().decode(Anything.self, from: data)).json

たくさんのキーを持つ JSON がレスポンスで返ってくる場合は、

必要なキーだけを struct のプロパティ(この場合は「json」)

にすれば良いようです。

これで、

「JSON を使った POSTリクエスト」

「JSON のレスポンスからクラスオブジェクトを取得」

ができますね!

 

🙆🏻‍♂️ 参考

👉 【Swift】 JSONEncoder / JSONDecorder の基本的な使い方 hatena-bookmark