【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」

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

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

 

🤔 関連記事



【Apple WWDC24】Swift ネタをサンプルコードとともに眺めていくページ

いよいよです。



👉 WWDC24 - Apple Developer hatena-bookmark

Apple 公式の GitHub では、

「sample」

の prefix をつけてリポジトリを公開していますよね。

洗練されていて、Swift の方向性も示されてるようで。

👉 Repository search results hatena-bookmark

いや、もしかしたら、GitHub 公開は遅れるかもしれん。

(更新中 ...)



【Swift】なんとなくメソッド名が長い気がする

慣れかもしれんけども、

メソッド名、というか、

メソッド名 + 省略できない引数名、というか。

全般的に長い。

つい作ってしまう。


extension String {
  func trim(_ chars: CharacterSet = .whitespaces) -> String {
    self.trimmingCharacters(in: chars)
  }
  func separatedBy(_ separator: String) -> [String] {
    self.components(separatedBy: separator)
  }
}

しかし、クラス名を省略できるのもいいところ。

みなさんは、どう思ってますか ?

 

2024-01-30 追記:

やっぱり長すぎ。なんなのこれ。

エディタに収まらなくね?

インデント2でも改行入れる隙がまったくない。

短い別名とか付けるの?


【Xcode】Auto-Completion がおかしい 不具合の理由 → DerivedData

なぜか、

通常プロジェクトと Playground では Auto-Completion が違う。

マルチなクロージャーを含む


Button(action:label:)

を Auto-Completion で生成して、全クロージャー展開しようとするとできない。

せっかく Playgorund で Completion の使い方を掴んだような気がしてたのに!


 

🛠️ 症状

クロージャー展開しようとすると以下の感じ。

Playground 側は OK。


// Playground

Button {
  code
} label: {
  code
}

// Button(action: <#T##() -> Void#>, label: <#T##() -> View#>)

フォーカスの当たったままコピーすると設定記述的なものがテキストで取得できる。

これが、既存プロジェクト側では、なぜか、展開できない。


// Project

Button(action: {}, label: {
        Text("Button")
 })

// Button(action: /*@START_MENU_TOKEN@*/{}/*@END_MENU_TOKEN@*/, label: {
//   /*@START_MENU_TOKEN@*/Text("Button")/*@END_MENU_TOKEN@*/
//    })

なんやこれ。

プロジェクト内に同様な記述があると思って探しみたけど見当たらない。

登録されている Completions を


Editor
  ↓
Show Completions

で確認しても問題はなさそう。

あれこれやっていると、直ることがある。


- Xcode 再起動直後だけ一瞬直る。
- しかし、2回目ぐらいから戻っておかしくなる。

よって、キャッシュか何かを読み込んでいる、と想像。

 

🛠️ 修正・対応方法

これでいけた。


rm -rf ~/Library/Developer/Xcode/DerivedData/*

👉 Xcode Quick Fix - Clear Cache hatena-bookmark

以下でも直ったのかもしれない。


Product

  ↓

Clean Build Folder...

  +

Option


👉 [Xcode][小ネタ] DerivedDataの削除についての備忘録 | DevelopersIO hatena-bookmark

 

🛠️ まとめ

DerivedData を削除することで効果が出る不具合。


- Build Failed
- ストレージが肥大
- Completion がおかしい

キャッシュのようなもので消しても問題ないらしい。

場所はデフォルトで、


~/Library/Developer/Xcode/DerivedData

で以下メニューから確認できる。


File

 ↓

{project name} Settings...

少し前に、Xcode をバージョンアップデートしたのでそれが影響しているのかもしれないです。