LoginSignup
37

More than 1 year has passed since last update.

(at)PublishedがどのようにSwiftUIのViewを更新するのか

Last updated at Posted at 2020-02-16

(at)PublishedがどのようにSwiftUIのViewを更新するのか

@Published属性がどのようにSwiftUIのViewを更新するのかについて書いておきます。

WWDC19の動画を見るとそもそも@Published属性がまだ存在していないのと、その当時はObservableObject型がBindableObject型であったり、@ObservedObject属性が@ObjectBinding属性だったりするのでややこしいためです。

私の結論としては@Publishedあると便利なんだけど、便利すぎるし内部が想像しづらいので基礎を知らないと無駄にSwiftUI.Viewを更新させることになると思います。さらに、コードを書き換えてるときや他人のコードで意味不明に@Publishedされたコードを見るかもしれません。そういうときに知っておきたいことを書いています。

@ObservedObject属性として指定されたObservableObjectはSwiftUIにsubscribeされている

  • Viewは@ObservedObject指定したObservableObjectobjectWillChangeをsubscribeしている
    • subscribeし発火されるとViewを更新するようにしている
    • ObservableObjectobjectWillChangeを使い発火させればいい
  • @PublishedはプロパティのwillSetでobjectWillChangeを使い発火させるのを暗黙的に行っている

objectwillchange_subscribe_from_swiftui.png

objectWillChangeとは何か

objectWillChangeはCombineで定義されているObservableObjectを見るとわかります。

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol ObservableObject : AnyObject {

    /// The type of publisher that emits before the object has changed.
    associatedtype ObjectWillChangePublisher : Publisher  // 制約の追加 Swift4.1(SE-0157) 
       = ObservableObjectPublisher  // デフォルトで利用する型
         where Self.ObjectWillChangePublisher.Failure == Never // 条件

    /// A publisher that emits before the object has changed.
    var objectWillChange: Self.ObjectWillChangePublisher { get }
}
  • プロパティobjectWillChangeSelf.ObjectWillChangePublisherを返す
  • associatedtypeのObjectWillChangePublisher
    • Publisherプロトコルを制約として追加している
    • 言い換えるとObservableObjectはパブリッシャーを保持している
    • デフォルトではObservableObjectPublisherクラスを使う
      • さらにエラーは流さない
      • おそらくカスタマイズしたPublisherも使える

ObservableObjectPublisherとは何か

Combineで用意されたObservableObjectPublisherクラスを確認してみます。

/// The default publisher of an `ObservableObject`.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
final public class ObservableObjectPublisher : Publisher {

    /// The kind of values published by this publisher.
    public typealias Output = Void

    /// The kind of errors this publisher might publish.
    ///
    /// Use `Never` if this `Publisher` does not publish errors.
    public typealias Failure = Never

    public init()

    /// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
    ///
    /// - SeeAlso: `subscribe(_:)`
    /// - Parameters:
    ///     - subscriber: The subscriber to attach to this `Publisher`.
    ///                   once attached it can begin to receive values.
    final public func receive<S>(subscriber: S) where S : Subscriber, S.Failure == ObservableObjectPublisher.Failure, S.Input == ObservableObjectPublisher.Output

    final public func send()
}
  • OutputはVoidなのでイベントは (Void) -> Voidのクロージャ
    • .sink { (Void) -> Void in ... }
      • SwiftUIのViewとしては更新されたらbodyが実行される
        • 使用されるオブジェクトのプロパティは全て読みだされるので
          • イベント時のOutputがVoidでも特に気にしない
  • どのような場合にカスタマイズする?
    • Outputを変更したい場合がある?
      • 思いつかないな...

必ずしも@Publishedを使う必要はない

@Publishedを使いたいのは次の優先度のとき

  • ViewがBindingなオブジェクトを取り出してバインディングしたい(読み込みと書き込み)
  • ObservableObjectの中でfilterflatMapしたい
  • objectWillChange.send()を自動でやりたい

RxSwiftのようなUIからストリームを作れる仕組みがないために、Bindingでの値のやり取りが楽なので@Publishedを使いたい。本当はそれ一択かもしれない。

SwiftUIガイドブック

BOOTHで電子書籍として販売しています。

読者のターゲットは次のような人を想定しています

  • SwiftUIのレンダリングシステムが自動で更新されるタイミングを知らない
    • 更新のきっかけになる$ObservedObject@Publishedにもやもやしている
    • SwiftUIのViewが自動で更新されるタイミングやその範囲にもやもやしている
  • @State@Binding@ObservedObjectの違いがよくわからない
    • @Environment@EnvironmentObjectの違いもまだ知らない
  • $オペレータを雰囲気で使って雰囲気で修正している
    • コンパイルエラーになるたびに修正している
    • propertyWrapperとdynamicMemberLookupを理解して使っていない

SwiftUIでpropertyWrapperとdynamicMemberLookupがあやふやだなーという気持ちになったままコードを書くことも多いのではないかと思ってます。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37