【SwiftUI】@Observable と @Binding vs @Bindable 👀

例えば、Binding を含むこういうのがあったとして。


struct Parent: View {
  @State private var isOn = false

  var body: some View {
    VStack {
      Rectangle().fill(isOn ? .yellow : .gray)
      Toggle(isOn ? "ON" : "OFF", isOn: $isOn)
    }
  }
}

親子分けした場合。


struct Parent: View {
  @State private var isOn = false

  var body: some View {
    VStack {
      Rectangle().fill(isOn ? .yellow : .gray)
      Child(isOn: $isOn)
    }
  }
}

struct Child: View {
  @Binding var isOn: Bool

  var body: some View {
    Toggle(isOn ? "ON" : "OFF", isOn: $isOn)
  }
}

👉 【SwiftUI】再描画の伝播 - @State と @Binding hatena-bookmark

@State の数が増えてくるとつらいので、

まとめて外出しにする方法はないのかな、と。

そして、子 View などへ上手に持ち運びたい。

 

👀 @Observable と @Bindable

@Observable と @Bindable を使います。

Observable

Observable プロトコルの適合性を定義および実装します。

概要
このマクロは、カスタム タイプに監視サポートを追加し、タイプをObservableプロトコルに準拠させます。

👉 Observable() | Apple Developer Documentation hatena-bookmark

Bindable

監視可能なオブジェクトの可変プロパティへのバインディングの作成をサポートするプロパティラッパータイプ。

概要
このプロパティ ラッパーを使用して、Observable プロトコルに準拠するデータ モデル オブジェクトの可変プロパティへのバインディングを作成します。

👉 Bindable | Apple Developer Documentation hatena-bookmark

ドキュメントのコードを参考にしながら、

とりあえずは、クラスにまとめて外出しします。


@Observable
class StateModel: Identifiable {
  var isOn = false
  var label: String { isOn ? "ON" : "OFF" }
  var color: Color { isOn ? .yellow : .gray }
}

struct Parent: View {
  @Bindable private var model = StateModel()

  var body: some View {
    VStack {
      Rectangle().fill(model.color)
      Toggle(model.label, isOn: $model.isOn)
    }
  }
}

 

👀 子に渡す

Binding を含む @State は、子の @Binding で受けてましたが。

@Observable クラスはどうやって渡すのか。

知ってる範囲であれこれ試してみました。

どうやら、これが標準的で良さげです。


@Observable
class StateModel: Identifiable {
  var isOn = false
  var label: String { isOn ? "ON" : "OFF" }
  var color: Color { isOn ? .yellow : .gray }
}

struct Parent: View {
  private let model = StateModel()

  var body: some View {
    VStack {
      Rectangle().fill(model.color)
      Child(model: model) // *
    }
  }
}

struct Child: View {
  var model: StateModel

  var body: some View {
    @Bindable var model = model // *
    Toggle(model.label, isOn: $model.isOn)
  }
}

試しながら思ったのは、

「クラスインスタンスのまま持ち歩いて、使う直前で @Bindable で Binding 化する」

のが良いですわ。

持ち回りがシンプルで楽だから。

データ自体は @Bindable なくても変化を検知して順方向には流れる。

ただ、いきなり出てきて意味不明で気持ちが悪かった。


@Bindable var model = model

公式ドキュメントでもあちこちにあって、気にはなっていました。

 

👀 まとめ

最後に、ゴミを置いておきます。

どのコメントブロックもきちんと動きます。

いや、動いてるように見えているだけかもしれません。


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

単純に GET によるレスポンスボディを取得したい。

こんなにシンプルな感じでかけるとは!


let url = URL(string: "https://example.com")!
for try await line in url.lines {
  print(line)
}

👉 task(priority:_:) | Apple Developer Documentation hatena-bookmark

一行ごとに取れるようです。

試してみます。


import SwiftUI
import PlaygroundSupport

struct SampleView: View {
  @State private var message = "Loading..."

  var body: some View {
    Text(message)
      .task {
        message = await getWeather()
      }
  }

  private func getWeather() async -> String {
    let url = URL(string: "https://wttr.in/?format=3")!

    do {
      var lines: [String] = []
      for try await line in url.lines {
        lines.append(line)
      }
      return lines.joined()
    } catch {
      return "Faild to load"
    }
  }
}

PlaygroundPage.current.setLiveView(
  SampleView()
    .frame(width: 300, height: 300)
)

簡単にレスポンスを確認するときに使えそうです。

👉 lines | Apple Developer Documentation hatena-bookmark
👉 chubin/wttr.in: :partly_sunny: The right way to check the weather hatena-bookmark

 

😅 まさか String() でこんなことができるとか


print(
  (try? String(contentsOf: URL(string: "https://wttr.in/?format=3")!)) 
    ?? "Error!"
)

// Kisarazu, Japan: ⛅️  +25°C


【SwiftUI + SwiftData】@Query とは? - MV (Model + View) Pattern

C も P も VM もないんですね!

【SwiftUI + SwiftData】@Query とは? - MV Pattern ( Model + View )

「@Query」を基本的に、どう理解しておくか。


import SwiftUI

// Model
struct Product {

  static var all: [Product] { get async }
}

// View
struct ProductList: View {
  @State private var products: [Product] = []

  var body: some View {
    VStack {

    }
    .task {
      products = await Product.all
    }
  }
}

SwiftData を使うと以下のように書ける。


import SwiftUI
import SwiftData

// Model
struct Product {

}

// View
struct ProductList: View {
  @Query private var products: [Product]

  var body: some View {
    VStack {

    }
  }
}

「モデルデータコレクションの非同期取得」 ですね。

@State と同様に変更を監視しています。

👉 Stop using MVVM for SwiftUI | Apple Developer Forums hatena-bookmark