Open
28

iOS 開発について読んだ記事のメモ

https://fivestars.blog/swiftui/swiftui-hud.html

airpods を接続したときに出てくるやつを自作する話。中身を柔軟に設定できるようにするためプロパティを @ViewBuilder にしている。一定時間で自動的に消えて欲しいので、 onAppear の中で DispatchQueue.main.asyncAfter を設定して消している。移動 && opacity を変えながら登場する/消えるアニメーションを .transition(AnyTransition.move(edge: .top).combined(with: .opacity)) で実現。いちいち ZStack を作ってカスタムコンポーネントを呼び出すのが面倒なので Viewextension にしておいてあげると便利。

https://fivestars.blog/swiftui/app-state.html

アプリ全体で共有する状態の話。 mainApp@StateObject として初期化して .environmentObject で注入する。使う View では @EnvironmentObject から参照する。

@main
struct FiveStarsApp: App {
  @StateObject var appWideState = AppWideState()

  var body: some Scene {
    WindowGroup {
      ContentView()
        .environmentObject(appWideState) // injected in the environment
    }
  }
}

// child view
struct HomeView: View {
  @EnvironmentObject var state: AppWideState // environment object

  var body: some View {
    VStack {
      Button("go to settings") {
        state.selectedTab = .settings // sets the state from the environment object
      }
      Text("Home")
    }
  }
}

@Published なプロパティの値が変わると@EnvironemntObject として AppWideState を参照している View は裏側にいるものも含めてすべて再描画されるため、@Published なプロパティや @EnvironmentObject から AppWideState を参照する View が増えてくるとパフォーマンスの問題が起こる。

AppWideState を本当に observe したい View はしょうがないが、AppWideState の値を更新するために参照している View では observe しないようにしたい。これは、状態に階層を作ることで実現できる。

class TabViewState: ObservableObject {
  @Published var selectedTab: Tab = .home
}

class AppStateContainer: ObservableObject {
  var tabViewState = TabViewState()
}

とすると、 AppStateContainer のみを observe している VIew では selectedTab の変更によっては再描画が走らなくなる。つまり、

  var body: some Scene {
    WindowGroup {
      ContentView()
        .environmentObject(appStateContainer)
        .environmentObject(appStateContainer.tabViewState)
    }
  }

のように注入して、selectedTab を observe したい View では @EnvironmentObject TabViewState、 更新はしたいが observe しなくて良い View では @EnvironementObject AppStateContainer とすれば良い。

https://fivestars.blog/swiftui/design-system-composing-views.html

SwiftUI では View に対して View を渡すことで画面を柔軟に組み立てていくことができる。やり方は3つ。

型を指定する

TextImage など特定の型を init の引数に指定する。適当な型を渡されても意味がない時に使う。

Generic views

ジェネリクスを使って、View に従う型なら何でも受け入れられるようにする。以下の label には TextImageVStack も渡すことが可能。自由に View を渡したいが、あまりその VIew が複雑にならない場合に有効。

struct SomeView<Label: View>: View {
  init(label: Label)
}

@ViewBuilder

クロージャで View を組み立てる方法で、SwiftUI で View を渡す方法としてはポピュラー。@ViewBuilder で渡される View は、その View の主要な要素であることが多い。Buttoninit が一つの例。

struct Button<Label> : View where Label : View {
  init(action: @escaping () -> Void, @ViewBuilder label: () -> Label)
}

ForEach では、表示して欲しい View が引数に対してどのように描画されるかの手続きだけを知らせておいて実際の引数はあとで渡すようになっている。これは @ViewBuilder@escaping にすることで実現されている。

extension ForEach where Content: View  {
  public init(
    _ data: Data, 
    id: KeyPath<Data.Element, ID>, 
    @ViewBuilder content: @escaping (Data.Element) -> Content
  )
}

https://swiftbysundell.com/tips/passing-methods-as-swiftui-view-actions/

SwiftUI をやってるとなんでもかんでも body の中に書きがちだけど、一部を別の View に切り出したり、そこまでする必要のないものは単なる property にしてもOKだし、共通化できるロジックはメソッドにできるようにしようという話。

https://swiftbysundell.com/articles/using-multiple-computed-properties-to-form-a-swiftui-view-body/

SwiftUI の body を分割するとき、View を新しく作るのもいいが単なる property にするのも良いという話。

struct ProfileView: View {
    var user: User

    var body: some View {
        ZStack {
            LinearGradient(
                gradient: user.profileGradient,
                startPoint: .top,
                endPoint: .bottom
            )
            .edgesIgnoringSafeArea(.all)

            ScrollView {
                VStack(spacing: 15) {
                    Text(user.name)
                        .font(.title)
                    Text(user.biography)
                        .multilineTextAlignment(.leading)
                }
                .padding()
                .foregroundColor(.white)
            }
        }
    }
}

struct ProfileView: View {
    var user: User

    var body: some View {
        ZStack {
            background
            foreground
        }
    }
}

private extension ProfileView {
    var background: some View {
        LinearGradient(
            gradient: user.profileGradient,
            startPoint: .top,
            endPoint: .bottom
        )
        .edgesIgnoringSafeArea(.all)
    }

    var foreground: some View {
        ScrollView {
            VStack(spacing: 15) {
                Text(user.name)
                    .font(.title)
                Text(user.biography)
                    .multilineTextAlignment(.leading)
            }
            .padding()
            .foregroundColor(.white)
        }
    }
}

のようにする。分割した property が body と同じく some View になっていることに注意。新たな View を作る場合と違って、

  • プロパティを Binding にして渡すみたいな煩雑さがなくなる
  • コードを分けたいだけなのに新たな struct を定義しないいけない面倒がなくなる

というメリットがある。

当然、分割した View を他の View からも使いたい場合は新たな View を定義する必要がある。

https://swiftbysundell.com/articles/the-lifecycle-and-semantics-of-a-swiftui-view/

SwiftUI の View は value なんだから UIViewController の感覚でいてはいけないという話。

View の body は画面の設計図のようなもので、viewWillAppear のように処理を書くものではない。副作用のある処理を body に書くと、再描画のたびにその処理が走ってしまうため。viewWillAppear でやりたくなる ViewModel を更新するような処理は onAppear でやる。

同じように、init の中で副作用のある処理を書くのもやめる。NavigationLinkdestination のように、SwiftUI の View は画面にまだ表示されていないときや、そもそも使われない場合でも初期化される(これは value としては不自然なことではない)。例えば NotificationCenter の background 移行の通知を受け取る処理を書く場合は init でやると意図しないタイミングで発火される可能性があるので、View が 表示されているときにしか実行されない onReceive でやる。

https://swiftbysundell.com/tips/observing-combine-publishers-in-swiftui-views/

SwiftUI で通知を受け取るのは onReceive でやろうという話。毎回同じような処理を書くことになる場合は View の extension を作ってあげると良い。

extension View {
    func onNotification(
        _ notificationName: Notification.Name,
        perform action: @escaping () -> Void
    ) -> some View {
        onReceive(NotificationCenter.default.publisher(
            for: notificationName
        )) { _ in
            action()
        }
    }

    func onAppEnteredBackground(
        perform action: @escaping () -> Void
    ) -> some View {
        onNotification(
            UIApplication.didEnterBackgroundNotification,
            perform: action
        )
    }
}

https://swiftbysundell.com/articles/property-wrappers-in-swift/

状態の性質を持つ property では、property の値が変化した時になんらかの処理をトリガーしたいことがある。例えば、バリデーションや変換、通知など。これを再利用しやすいかつシンプルな形で書くための機能が property wrappers。実際に実装してみるとわかるが、getsetdidSet の再利用しやすい形と考えるとわかりやすい。裏側では処理を走らせつつ、使うときにはラップされていない普通のプロパティと同じように使えるところが良い。

property wrappers はある生の値をに対してなんらかのロジックを追加するという形でラップしたもの。実装は class もしくは struct に @propertyWrapper アトリビュートをつけることで行う。wrappedValue という stored propety を持つことが必要。

@propertyWrapper struct Uppercased {
    var wrappedValue: String {
        didSet { wrappedValue = wrappedValue.uppercased() }
    }
    
    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue.uppercased()
    }
}

struct User {
    @Uppercased var firstName: String
    @Uppercased var lastName: String
    
    var fullName: String { "\(firstName) \(lastName)" }
}

var u = User(firstName: "john", lastName: "appleseed")
print(u.fullName) // JOHN APPLESEED
u.firstName = "ken"
print(u.fullName) // KEN APPLESEED

property wrapper が init(wrappedValue:) を定義している場合は、その property には初期値を持たせることが可能。

struct Document {
    @Capitalized var name = "Untitled document"
}

wrappedValue 以外の property を持たせることもできる。外部から使うというより、主に property wrapper が追加する処理の中で使われることが多そう。

@propertyWrapper struct UserDefaultsBacked<Value> {
    let key: String
    var storage: UserDefaults = .standard

    var wrappedValue: Value? {
        get { storage.value(forKey: key) as? Value }
        set { storage.setValue(newValue, forKey: key) }
    }
}

struct SettingsViewModel {
    @UserDefaultsBacked<Bool>(key: "mark-as-read")
    var autoMarkMessagesAsRead
}

property wrapper のいいところの一つは素の property と同じようにアクセスできるところだが、特に SwiftUI では逆に property wrapper の実装そのもの(struct or class)にアクセスしたいことがある。これを projected value と呼び、projectedValue property を追加して、$ をつけてアクセスすることができる。
これは勘違いで、projectedValue は property wrapper そのものでもいいし関係ない値でも良い。property wrapper そのものには、private ではあるが _ をつけてアクセスすることが可能。https://zenn.dev/muijp/scraps/e86de48425d8ee#comment-0d112f294bc7fb 参照。

@propertyWrapper final class Flag<Value> {
    var projectedValue: Flag { self }
    ...
}

{
    ...
    $flag
    ...
}

https://www.swiftbysundell.com/clips/3/

SwiftUI では子はデフォルトでは親の中心に配置される。*Stack はデフォルトでは子と同じだけの大きさになる。*Stack の中に Spacer を入れることで Stack を広げつつ要素を寄せることができる。

https://www.swiftbysundell.com/articles/swiftui-layout-system-guide-part-1/

SwiftUI のレイアウトの基礎的な話。

View に modifier を適用した時、View 自体を変更するのではなく新しい View で対象を包んでいることもある。例えば、以下のコードでは赤色は Image 自体ではなく、frame が生成したコンテナ View に適用される。これが modifier を適用する位置によって結果が全く異なってくる原因。

        Image(systemName: "calendar")
            .frame(width: 50, height: 50)
            .background(Color.red)

https://www.swiftbysundell.com/articles/swiftui-layout-system-guide-part-2/

SwiftUI のレイアウトの基礎的な話2。

.aspectRatio() でアスペクト比を保つ。主に Image に対して resizable() と一緒に使われる。

.frame(maxWidth: .infinity) は可能な限り幅を広げるために使うが、同時に複数の要素を同じ幅で配置する時にも使える。

https://www.swiftbysundell.com/articles/swiftui-layout-system-guide-part-3/

SwiftUI のレイアウトの基礎的な話3。

SwiftUI では View が自分自身のサイズを決める。ShapeSpacer は可能な限り大きくなろうとするので、同じ *Stack の中で並ぶと conflict が起きる場合がある。このような場合、 Stack 内で小さくできる要素をまず可能な限り小さくし、残ったスペースを大きくなろうとする要素で等分することになる。

この処理に割って入るためには、 layoutPriority でレイアウトを決定する順番を指定してあげればよい。例えば、最低限の大きさはほしいけどそれ以外はどうでもいい要素などには以下のようにすれば他の要素を優先してレイアウトしてくれる。

            DoudemoYoi()
                .layoutPriority(-1)
                .frame(minHeight: 100)

確実に想定通りのサイズで表示したい要素には .fixedSize() を使う。これでその要素が望むサイズが使われるようになる。

https://www.swiftbysundell.com/articles/swiftui-state-management-guide/
  • @State はその View or 子 View の中でしか使わない状態を作るのに使う。private にするのが良い
  • @State は value semantics を持たないといけない。reference の場合は @Binding がうまく動かないことがある
  • @State がラップした値ではなくそのもの(= Binding)にアクセスしたい場合は変数名の prefix として $ をつける

https://www.donnywals.com/wrapping-your-head-around-property-wrappers-in-swift/

property wrappers の proposal はかなり物議を醸したらしく、4回目でようやく受け入れられたらしい。これにより、言語仕様に入った段階ではかなりこなれた機能になっていたと考えられる。property wrappers の意義は property にある性質を持たせたいときにいちいちそのための処理を書かなくて良いようにすること。例えば lazy な property を作るには毎回そのための処理を書くのではなく単に lazy と書けば良い。このようなキーワードを開発者が自作できるようにするのが property wrappers。

@Published var myProperty = 10

という property があったら、

  • myProperty: Int
  • $myProperty: Published.Publisher
  • _myProperty: Published<Int>

_myProperty は private なので定義したオブジェクトの中でしかアクセスできないことに注意。これが明示的に定義しなくてもアクセスできる理由は、 @ExampleWrapper var myProperty = 10 をコンパイラが以下のように変換するから。

private var _myProperty: ExampleWrapper<Int> = ExampleWrapper<Int>(wrappedValue: 10)

var myProperty: Int {
  get { return _myProperty.wrappedValue }
  set { _myProperty.wrappedValue = newValue }
}

$myProperty はすべての property wrapper が持つわけではなく、projected value を定義した wrapper のみが持つ。projected value はその property wrapper の別の側面や特別なインターフェースを提供するのに使われる。private な _myProperty を公開するために使ってもいいし、まったく別のもののために使っても良い。例えば、 @Published は publisher 、 @State は binding を projected value にしている。

@Published を自分で作ってみる例。

@propertyWrapper
struct MyPublished<Value> {
    var wrappedValue: Value {
        get { subject.value }
        set { subject.send(newValue) }
    }

    var projectedValue: CurrentValueSubject<Value, Never> {
        get { subject }
    }

    private let subject: CurrentValueSubject<Value, Never>

    init(wrappedValue: Value) {
        self.subject = CurrentValueSubject(wrappedValue)
    }
}

class MyObject {
    @MyPublished var myValue = 1
}

let object = MyObject()
object.$myValue.sink(receiveValue: { v in
    print("received \(v)")
})

object.myValue = 2
object.myValue = 3

https://alisoftware.github.io/swift/closures/2016/07/25/closure-capture-1/

escaping な closure の中で、自身で定義していない object を使う場合、その object はキャプチャされる。ここで、デフォルトでは variable の参照がキャプチャされ、値は実行時に評価されることに注意。これは、 object が value semantics に従う場合でもそう。

func demo1() {
  var value = 42
  print("before closure: \(value)")
  delay(1) {
    print("inside closure: \(value)") // 1337, not 42!
  }
  value = 1337
  print("after closure: \(value)")
}

closure の実行時ではなく定義時の値を使いたい場合は、その object を capture list に入れてあげる。

func demo2() {
  var value = 42
  print("before closure: \(value)")
  delay(1) { [value] in
    print("inside closure: \(value)") // 42!
  }
  value = 1337
  print("after closure: \(value)")
}

(この辺自信なし)
reference semantics に従う object の場合、値が参照のため capture list に入れても結局 closure に参照が渡され無意味な気がするがそんなことはない。その object が let なら無意味だが var なら closure の定義 -> 実行の間に再代入が起こったときに、 capture list に入っていれば定義時の object が、入っていなければ実行時の object が使われるという違いがある。

func demo3() {
  var pokemon = Pokemon(name: "Pikachu")
  print("before closure: \(pokemon)")
  delay(1) { [pokemon] in
    print("inside closure: \(pokemon)") // Pikachu!
  }
  pokemon = Pokemon(name: "Mewtwo")
  print("after closure: \(pokemon)")
}

https://swiftrocks.com/memory-management-and-performance-of-value-types.html

struct / class では value / reference semantics の違いが一番大きいが、それに関連してパフォーマンスの特性も異なってくる。

プロセスのメモリ空間には stack と heap がある。

stack は allocation / deallocation がシンプルで速い。とくに固定サイズの value type のみを使う場合などスコープ内で使うメモリの量が事前にわかるとき、スコープに入ったときにその分 stack pointer を進め、スコープから抜けたときに戻すだけで良い。このような場合、value type が reference type を含んだり逆に reference type に含まれていたりすることがなければ、寿命は static で、reference counting が必要ないのでパフォーマンスが良い。value type を使うとコピーが大量に走ることを心配してしまう場合も多いが、話が stack で完結する場合には copy-on-assignment でも軽い処理なので気にしなくて大丈夫。ここで、組み込みの value type は copy-on-write だが、自分で作った value type は copy-on-assignment であることに注意。

スコープを超えて生き残るデータは heap に allocate される。heap では、stack と違って thread safety を気にしなければいけないのも allocate が重い原因である。

単純に、value type -> stack、reference type -> heap に allocate されると考えてはいけない。以下の時、value type であっても heap に allocate される。

  • protocol / generics の制約でサイズがコンパイル時には決まらない
  • 自身が reference type の子である場合

また、reference type を子として含む場合は、自身は stack に allocate されたとしても子を生かすために reference counting のオーバヘッドをうけ、パフォーマンスが悪化する。例えば、以下のように reference type の子を含む struct / class を大量にコピーする必要がある場合は struct は copy-on-assignment が走るのに対して class では reference count を増やすだけなので、 class の方が速い!また、子の数が増えたとき class のコピーではやることは変わらないが struct のコピーではやることが増えるので パフォーマンスの差はどんどん広がっていく。

struct HugeDynamicStruct {
    var emptyClass = EmptyClass()
    var emptyClass2 = EmptyClass()
    var emptyClass3 = EmptyClass()
    var emptyClass4 = EmptyClass()
    var emptyClass5 = EmptyClass()
    var emptyClass6 = EmptyClass()
    var emptyClass7 = EmptyClass()
    var emptyClass8 = EmptyClass()
    var emptyClass9 = EmptyClass()
    var emptyClass10 = EmptyClass()
}

class HugeClass {
    var emptyClass = EmptyClass()
    var emptyClass2 = EmptyClass()
    var emptyClass3 = EmptyClass()
    var emptyClass4 = EmptyClass()
    var emptyClass5 = EmptyClass()
    var emptyClass6 = EmptyClass()
    var emptyClass7 = EmptyClass()
    var emptyClass8 = EmptyClass()
    var emptyClass9 = EmptyClass()
    var emptyClass10 = EmptyClass()
}

もしちょっとの工夫で struct のサイズを固定できる、すなわち stack に allocate できるようになる場合はそうした方が良い。

// before
struct DeliveryAddress {
    let identifier: String
    let type: String
}

// after
struct DeliveryAddress {
    enum AddressType {
        case home
        case work
    }
    let identifier: UUID
    let type: AddressType
}

https://github.com/Swinject/Swinject/tree/master/Documentation
  • 用語

    • Service: 依存のインターフェースを表すプロトコル
    • Component: Service を実装している型
    • Factory: Component を生成する関数 or クロージャ
    • Container: Component のインスタンス
  • Service のインスタンスと Component のインスタンスは同じものを指す

  • 基本的な使い方

    • container に service と対応する component を register する。 register には service の型と factory を渡す
      • factory の中で別の service が欲しくなる場合がある。その時は、 resolve を呼ぶことができる
    • ある service を使いたい箇所で resolve を呼ぶ
  • 登録する component に名前をつけることができる。 resolve するときにも名前を指定する

container.register(Animal.self, name: "cat") { _ in Cat() }
container.register(Animal.self, name: "dog") { _ in Dog() }

let cat = container.resolve(Animal.self, name: "cat")
  • register には依存性以外の追加の引数を渡すことが可能!
container.register(Animal.self) { _, name in
    Horse(name: name)
}

let animal = container.resolve(Animal.self, argument: "Spirit")
  • ある service について component を register するとき、container の内部ではその component を識別するための key が振られる

  • key は以下によって構成される

    • service の型
    • name
    • 追加の引数の数と型
  • 登録された key が完全に既存のものと被ってしまった場合、 component は上書きされる。逆に、部分的に被った場合は resolve する際に名前や引数によって component の呼び分けができる

  • Injection の種類

    • Initializer injection : init に依存を渡す。基本はこれで良い
    • Property injection : 初期化後に依存性をプロパティに直接 set する。その依存が optional の時などに使う
    • Method injection : Property injection と似ているが、メソッドを介して依存を渡す
  • Object scope は container の中で component のインスタンスがどう共有されるかの設定

  • Swinject では .inObjectScope に適切な scope を渡すことで設定する

    • swift では value はそもそも共有されようがない(毎回コピーされる)ので、factory が value を返す時は object scope は無視される
container.register(Animal.self) { _ in Cat() }
	.inObjectScope(.container)
  • scope 一覧

    • .transient : インスタンスは共有されない。 resolve が呼ばれるたび毎回新しいインスタンスを生成して返す
    • .graph : デフォルト。インスタンスは基本共有されないが、ある service を resolve する時に同じ service が複数回 resolve された場合、それらは共有される。つまり、 ServiceAServiceBLogger に、 ServiceBLogger に依存している場合 Logger のインスタンスは1回しか生成されず共有される
    • .container : インスタンスはシングルトンになる。つまり、インスタンスは container の内部や、 その子 container との間でも共有される。最初に resolve された時にインスタンスが生成され、その後はそのインスタンスがずっと使われる
    • .weak : 基本的には .container と同じだが、resolve された component への強参照が1つもなくなった時点で共有されなくなり、次に resolve された際にまた新しくインスタンスが生成される
  • assembly / assembler を使うと、同じレイヤーの service の登録を一箇所にまとめて書くことができる

  • assembly が global な container を提供しているのでそれに対して register を行い、 assembler で assembly を保持しておく。resolve は assembly から行う。assembler への参照がないと container が deallocate されてしまうことに注意

class ServiceAssembly: Assembly {
    func assemble(container: Container) {
        container.register(FooServiceProtocol.self) { r in
           return FooService()
        }
    }
}

class ManagerAssembly: Assembly {
    func assemble(container: Container) {
        container.register(FooManagerProtocol.self) { r in
           return FooManager(service: r.resolve(FooServiceProtocol.self)!)
        }    }
}

let assembler = Assembler([
    ServiceAssembly(),
    ManagerAssembly()
])

let fooManager = assembler.resolver.resolve(FooManagerProtocol.self)!

https://medium.com/@dima.cheverda/xcode-9-templates-596e2ed85609
  • テンプレートファイルのpath
    • default: /Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/File Templates
      • /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/ にもある?
    • user defined: ~/Library/Developer/Xcode/Templates/File Template
      • この下にディレクトリを作れば、xcodeにはグループとして認識される
  • テンプレートの実態は .xctemplate というprefixのディレクトリ
    • 中には設定ファイルである TemplateInfo.plist とテンプレートファイル、.xib などのリソースを含む
  • 設定項目
    • Icon
    • Kind : Xcode.IDEKit.TextSubstitutionFileTemplateKind にしておけばOK。もう一つの選択肢として Xcode.IDECoreDataModeler.ManagedObjectTemplateKind がある
    • Options に作りたい変数を代入する
      • Type : text, static, checkbox, combo, popup
  • テンプレートに使えるマクロ
    • ___FILEBASENAME___
    • ___FILEHEADER___
    • `VARIABLE_variable_name

http://jeanetienne.net/2017/08/27/xcode-template.html

http://jeanetienne.net/2017/09/10/advanced-xcode-template.html
  • .plistKind キーを含む必要がある
    • Xcode.IDECoreDataModeler.ManagedObjectTemplateKind は CoreData 関連のテンプレートを作るときに使うよう
  • AllowedTypes : 保存できるファイルの種類を制限するためのもの。 Only lonely enforced(?)
    • public.swift-source
    • public.objective-c-source
  • Platforms
    • com.apple.platform.macosx
    • com.apple.platform.iphoneos
    • com.apple.platform.watchos
    • com.apple.platform.appletvos
  • Options を使う場合の変更点として、 DefaultCompletionNameAllowedTypes は不要になる。ファイルの名前を入力させなくなるため
    • productName options を指定するとファイルの名前が聞かれなくなる
  • Options の要素
    • Name : Labelに表示される文字
    • Description : Labelにマウスオーバーしたときにpopupされる文字
    • Type : text, static, checkbox, combo, popup
    • Required : true なら入力されるまで Next を disabled にする
    • Identifier
    • Default : デフォルト値。他の options から変数を使えるらしい
    • Values : combopopup の場合の選択肢
    • RequiredOptions : このoptionを enabled にするかどうかを条件によって指定する Dictionary。dict のキーは他の options の identifier、 値は popupcombo の値のsubset出なければいけない
    • SortOrder : 並び順。指定されなければ Options そのままの順番で表示される
  • ___VARIABLE_optionName___ で options の値にアクセスできる
  • options の値によって、使うテンプレートファイルを切り替えることが可能。例えば Also create a XIB file と同じことをやりたい時など
    • テンプレート直下にバリエーションごとのディレクトリを切って、特定の規則にのっとって命名すればOK
      • combo / popup : 選択肢の名前
      • checkbox : true の場合は identifier をappendする

https://www.raywenderlich.com/books/combine-asynchronous-programming-with-swift/v2.0

1. Hello, Combine!

  • UI インタラクションは非同期なものなので、非同期プログラミングの方法は多く開発されてきた

    • NotificationCenter
    • delegate パターン
    • GCD / Operations
    • クロージャ
  • ある程度のサイズのアプリでは、以上の手法が全部それぞれに使われているので非同期プログラミングは難しくなっている。この状況を解決するのが Combine。SwiftUI だけでなく、NotificationCenter や Core Data にも Combine が統合されているので非同期処理を統一的に書くことができる

  • Combine は Rx に似ているが、Rx に準拠しているわけではない

  • Combine の主な3つの構成要素は publishers, operators, subscribers

  • publishers は時間の流れの中でイベントを発火させることができる。publishers の中身は、重い計算やネットワークコール、UI イベントなど色々なものがありえる。そのいずれにおいても、発火させるイベントの種類は以下の3つ。逆に、この3つの組み合わせは色々なことが表せるいい枠組みであるとも言える

    • publisher の Output に指定されている型の値
    • 完了(成功)
    • 完了(publisher の Failure に指定されている型のエラー)
  • いったん完了したら、その後はイベントは流れない

  • publishers にはエラーハンドリングの仕組みが組み込まれている

  • operators は publisher を受け取って別の型、あるいは同じ型の publisher を返す。 operators はチェインすることができる

  • subscribers は publishers が発火するイベントを受け取って”何か”をする。例えば、画面に表示したり、サーバに値を送ったりなど

  • subscribers の働きをする組み込みの operators は2つ

    • sink : 値 / 完了を受け取って行う動作をクロージャで指定する
    • assign : 値をバインドしたい対象を key path で指定する
  • subscriber が値の subscribe を開始したときに大元の publisher が “activate” される。つまり、subscribe されない限り publisher は動作しないしイベントを発火しない

  • 組み込みの subscribers は Cancellable に準拠しているため、 publisher - operators - subscriber のチェインは Cancellable を返すことになる。 Cancellable がメモリ解放されたとき同時に subscription 全体をキャンセルし、関連するメモリも解放してくれる

    • Cancellable をプロパティにすれば ViewController と同時に消えるため、 subscription の寿命も自動的に ViewController と同じにすることができる
    • 実際には、 [AnyCancellable] オブジェクトを ViewController のプロパティにして、 subscription をその中に投げ込んでいくことになるだろう
  • Combine は言語に組み込まれているので言語の private な feature を使うことができる。つまり、我々が Swift では実現できないことをやってくれているかもしれない

  • Combine はアプリに特定のアーキテクチャを強制するフレームワークではない。MVC / MVVM / VIPER でもなんでも Combine を使うことができる

    • ただし、Combine を使えば ViewController はいらなくなる、あるいは薄くなる。subscription で View に値をバインドしてしまえば ViewController の仕事はなくなるため

2. Publishers & Subscribers

  • NotificationCenter に Combine が組み込まれているので、 .publisher で publisher を返すことができる
    let myNotification = Notification.Name("MyNotification")

    let publisher = NotificationCenter.default
        .publisher(for: myNotification)
        .sink(receiveCompletion: {
            print("received notification completion", $0)
        }, receiveValue: {
            print("received notification value", $0)
        })

    NotificationCenter.default.post(name: myNotification, object: nil)
  • sink は Rx の subscribe だが、 assign(to:on:)bind に対応する。 assign する先のプロパティは KeyPath で指定する
    class Object {
        var value: String = ""
    }

    let o = Object()

    ["Hello", "World", "!"].publisher
        .assign(to: \.value, on: o)
  • assign(to:) で publisher から流れる値を別の publisher にそのまま流すことができる。KeyPath ではなく、publisher を直接指定する
    • @Published な変数名の前に $ をつければ中身の publisher にアクセスできるので、例えば以下のようになる
    • assign(to:) で作られるsubscriptionは Cancellable に代入できない。 subscription の寿命は内部でよしなに制御してくれ、値を流す先のプロパティがいなくなれば自動的にキャンセルされるため。この特性を活かして、循環参照を作らないために assign(to:on:) の代わりに使われることがある
    class Object {
        @Published var value: Int = 0
    }

    let o = Object()

    o.$value.sink(receiveValue: { print($0) })

    (1...10).publisher
        .assign(to: &o.$value)
  • subscription は AnyCancellable を返すが、これは Cancellable protocol を実装している。 Cancellablecancel() を要求するため、subscription は cancel() できるということになる。
  • subscription をキャンセルしない場合、ARC が subscription を deinit するまでsubscriptionは続くことになる
  • アプリケーションコードで subscription の返り値の AnyCancellable を無視した場合、subscribe しているスコープを抜けた時点で自動的にキャンセルされてしまうことに注意

[image:75CB64EF-E86F-451D-98ED-D339D69D8020-1339-00000FFF17E9C93C/original.png]

  • 組み込みの operators である sinkassign で足りない場合は、自分で custom subscriber を定義することができる
final class IntSubscriber: Subscriber {
    typealias Input = Int
    typealias Failure = Never

    func receive(subscription: Subscription) {
      // ここで値のリクエストを行う。
      // - .none : 値を1つも受け取らない
      // - .max : 受け取る値の最大個数を指定
      // - .unlimited : 無限に値を受け取る
      subscription.request(.max(3))
    }
    
    // 5
    func receive(_ input: Int) -> Subscribers.Demand {
		// 値を受け取って何かする
		print(input)
      // 返り値で受け取る値の数を追加することもできる
      // .none, .max, .unlimited
      return .none
    }
    
    // 6
    func receive(completion: Subscribers.Completion<Never>) {
		// completion を受け取って何かする
      print("Received completion", completion)
    }
}
  • Future は1つだけ非同期に値を返すときに使う
    • Future については、subscriber がなくても処理が始まることに注意
    func futureIncrement(integer: Int, afterDelay delay: TimeInterval) -> Future<Int, Never> {
        Future<Int, Never> { promise in
            DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
                promise(.success(integer + 1))
            }
        }
    }

    let future = futureIncrement(integer: 3, afterDelay: 3)

    future
        .sink(receiveCompletion: {
            print($0)
        }, receiveValue: {
            print($0)
        })
        .store(in: &subscriptions)
  • Subject は値を手動で送りつけることができる publisher で、値の生成元が Combine ではなくても Combine の subscriber に値を送ることができる

    • PassthroughSubject : PublishRelay
      • 値を送るのは send(_:)
    • CurrentValueSubject : BehaviorRelay
      • 値を取得するのは .value
      • 値を送るのは send(_:) か、 .value に直接セットすることもできる
  • publisher に対して eraseToAnyPublisher() を呼ぶと、具体的な型を消去できる。つまり、 Future なのか PassthroughSubject なのか CurrentSubject なのかわからなくなる

    • 当然、もとが Subject だったとしても erase した後は send が呼べなくなることに注意

16. Error Handling

  • publishers は OutputFailure を型パラメータとしてもつ

  • FailureNever な publishers はエラーを吐かず、完了するときは必ず成功することになる

  • assign(to:on:)receiveValue のみを引数にもつ sink は、 Never の publishers に対してしか使えない

  • 大抵の operators には try の prefix をつけたエラーを投げられるバージョンの operator が存在する。try* の中で throw することで、 subscribers にエラーを送ることができる

  • try* の上流ではエラーの型が指定されていたとしても、下流では Swift.Error に丸められてしまうことに注意。これは、swift では typed throw をサポートしていないため

  • mapError でエラーの変換ができる。変換後のエラーの型も指定できるので ^ で Swift.Error になってしまったエラーを再び特定の型に戻すことも可能

17. Schedulers

  • scheduler はクロージャをどうやっていつ実行するかを表すプロトコル。未来に実行されるある操作のための環境を提供するものであるとも言える

  • scheduler は thread を使っているが、両者はイコールではない。1つの scheduler が thread を切り替えながら一連の操作を行うこともある

  • scheduler を分類する2軸

    • foreground / background
    • serial / concurrent
  • scheduler を指定するための operators

    • subscribe(on:) : 自分より上流での publish がどの scheduler で行われるかを指定する
      • 重い計算を background で行いたいときなどに使う
    • receive(on:) : 自分より下流での subscribe がどの scheduler で行われるかを指定する
      • UI 更新を foreground で行いたいときなどに使う
  • scheduler を何も指定しなければ、subscription は現在の thread で行われる

  • Scheduler protocol に従う実装は以下。最も重要でよく使われるのは DispatchQueue

    • ImmediateScheduler : 現在のスレッドで、即 action を実行する scheduler。デフォルトではこの scheduler が使われる
    • RunLoop : DispatchQueue の前身。現在でも使う例としては、 Timer の publish は main thread に紐づく RunLoop.main で実行されるらしい
    • DispatchQueue
    • OperationQueue
  • DispatchQueue には serial / concurrent があるが、使い分けは以下

    • serial : 一度に一つの action が、順番通りに実行される。action の順番を守りたい時や共有リソースにロックなしでアクセスしたい時などに使う
    • concurrent : 可能な限りの action を同時に行う。お互いに関係ない操作や pure computation をなんでもいいから大量にこなしたいときに使う
  • 全ての DispatchQueue は thread pool を共有している。serial queue は action が渡されたとき、その pool の中の空いている thread を使う。つまり、同じ queue の2つの連続実行が異なる thread で行われることもありうる

  • DispatchQueue.main は serial、DispatchQueue.global は concurrent

https://www.vadimbulavin.com/modern-mvvm-ios-app-architecture-with-combine-and-swiftui/
  • MVVM の目的は、プレゼンテーションロジックとビジネスロジックをそれぞれ UI から切り離すこと。これによりテスタビリティとメンテナンス性が向上する
  • View 自身は何の判断もせず、受動的であるべきなので自身で ViewModel からデータを pull してはいけない。一方で、ロジックとビューを分離し、かつテストできるようにするためにViewModel は View の存在を知ってはいけない。この2つを両立させるのが Data Binding
  • 双方向データバインディングは View と ViewModel の連携がスパゲッティになるので良くない。イベントと状態を分けるべき。この記事ではライブラリを使ってるけど、やるとしたら View -> ViewModel はイベントで、 ViewModel -> View は @Published の Data Binding でやるのがいいのかな
  • 状態を表す変数の数が増えるたびに View のロジックが階乗で複雑になっていく。また、競合する状態をどう扱えばいいかわからないこともある。例えば、 isLoadingtrueerrorMessage が not-nil のときどう表示すべきか。変数の数を増やすのではなく、一つの変数が表す状態の数を増やすのがいいかもしれない

https://developer.apple.com/videos/play/wwdc2018/418/

右上の <- -> ボタンを押すと現在開いているファイルを任意のコミットと比較できるのを知らなかった。特定のファイルの変更を追うのに便利そう。それ以外はとくにいい機能はなかった。

https://developer.apple.com/videos/play/wwdc2020/10028
  • よい widget が備える特徴
    • glanceable
      • widget はミニアプリではない。重要な情報だけを一目で分かるように表示すべき
    • relevant
      • smart stack でユーザのコンテクストに合った widget を表示する
    • personalized
      • 設定のUIは開発者が頑張らなくても自動的に生成される(?)
      • できるだけ3つのサイズをサポートすべき
  • ユーザはホームスクリーンに何回も、少しの時間だけ滞在する。ホームを開くたびいちいちロードしてローディングアイコンが表示されるのは避けたい。そのために、widget は background で動作し、タイムラインという形で view の時系列を提供する
  • なんらかのアクションがあったときアプリから widget のタイムラインを更新することも可能
  • widget を定義する上で登場する概念
    • kind
      • 一つのアプリに対して複数の widget を作ることができる
    • configuration
      • kind は2種類の configuration を support できる
        • StaticConfiguration: ユーザによるカスタマイズなし
        • IntentConfiguration: ユーザによるカスタマイズあり
    • supportedFamilies
      • small / medium / large
      • デフォルトでは全てををサポートする
    • placeholder
      • 全ての kind は placeholder を実装する必要がある
      • ユーザデータの表示はなし
      • なんのアプリの widget かは分かるのが理想的
    • widget は stateless である必要がある
      • スクロールなし / ビデオやアニメーションなし / switch のようなインプットなし
      • タップのみをサポートする
        • 遷移先として、small には global な widgetURLを、medium / large ではそれに加えて内部に複数の Link を設定できる
  • widget の View は3種類
    • placeholder
    • snapshot
      • システムがその widget の view をほしい時にすぐ表示するための view。とはいえ、単なる画像ではなくちゃんとした widget である必要はある
      • たいていの場合は timeline の1枚目と同じ view になる
    • timeline
      • 時系列として連続した一連の widget の view 。どの時刻にどの view を表示するか指定できる
      • timeline が返す view はシリアライズされて disk に保存される。これにより、多くの widget の多くの timeline entry を表示することが可能になる
      • TimelineProvider プロトコルに準拠して設定する
  • reload
    • ReloadPolicy を設定して、どういうタイミングでシステムに widget を起こして再描画を求めてほしいかを指定できる
      • atEnd
      • after(date: Date)
      • never
    • よくみられる widget ほどよく reload される
    • environment が変更された時は必ず reload される
    • background notification を受け取ったときやアプリでユーザがなんらかの操作をしたときには手動で reload をすることができる
  • personalization
    • intents を利用してユーザにパラメータを設定させることができる

https://betterprogramming.pub/build-your-first-widget-in-ios-14-with-widgetkit-9b893423e815
  • Text(“date, style: .relative”) とすれば現在時刻から相対的に見た時刻を表示可能。widget であっても毎秒アップデートされる!
  • widget のサイズは @Environment(\.widgetFamily) var widgetFamily で取得できる。サイズによって表示するコンポーネントを変えたい場合は widgetFamily を見て分岐すればよい

https://developer.apple.com/videos/play/wwdc2020/10034

https://developer.apple.com/videos/play/wwdc2020/10035

https://developer.apple.com/videos/play/wwdc2020/10036
  • 利用可能な widget のサイズを制限するには .supportedFamilies modifier を使う
  • widget の placeholder を返すのに .isPlaceholder(true) が便利。これは widget ではなく SwiftUI の機能のようだが、テキストや画像をいい感じに placeholder っぽくしてくれる
  • stack の中の widget はシステムが状況を見て出すので、そのためのヒントとして entry に relevance をデフォルトで設定できるようになっている。 relevance は float
  • new file -> intent で検索して設定を追加するための intent を作成できる。メインのアプリと widget どちらの target member にも設定することに注意
    • ぽちぽち設定すると intent ができる
  • api にリクエストした内容をもとに widget を描画することも可能で、TimelineProvider のメソッドが view を返すのではなく completion で渡すようになっているのはこのため。リクエストの callback の中で completion を呼べばよい

https://www.youtube.com/watch?v=XzE1jmGKTrw
  • SwiftUI を使う理由
    • iOS 開発の未来だから
    • 少ないコードで多くのことができる(darkmode 対応など)
  • SwiftUI を使わない理由
    • 不安定。iOS のマイナーバージョンによっても挙動が変わったりする
    • 無計画に入れるとつらくなってくる
    • UIKit のままでも何の問題もないので、事業の方が優先度が高いかもしれない
  • 移行前にやっておいた方がいいこと
    • UI の要素をコンポーネント化しておく
    • storyboard をなるべく使わない。View を細かく分けたり、コードで UI を作ったり
    • UIKit の宣言的な API を使う。StackView や DiffableDataSource など
  • Tips
    • 末端の画面から移行するのがよい
      • AppDelegate や TabBarController など他の画面から依存されている画面から始めるのは難しい。どこからも依存されていない画面がよい
    • 1つの画面で UIKit / SwiftUI を混ぜすぎない
    • 簡単な画面から移行しよう
      • 単なる TableView や Form などが簡単かも

https://learnappmaking.com/pass-data-between-views-swiftui-how-to/

view にデータを渡す方法のまとめ

View のプロパティとして渡す

struct BookRow: View
{
    var book: Book

    var body: some View {
        VStack {
            Text(book.title)
            Text(book.author)
        }
    }
}

// -- 

List(books) { currentBook in
    BookRow(book: currentBook)
}
  • シンプル。これでよい場合はこれがよい
  • データが更新されても UI は更新されない。また、データを渡される側から更新を伝えることもできない

@Binding

struct LivingRoom: View
{
    @State private var lightsAreOn = false

    var body: some View {
        Toggle(isOn: $lightsAreOn) {
            Text("Living room lights")
        }
    }
}

データを渡される側でプロパティが @Binding になっている以外は上のアプローチと同じ。しかし、

  • 双方向バインディングになり、データ更新により UI も更新されるし、データを渡される側からも更新を伝えられる

@EnvironmentObject

@main
struct BookApp: App
{
    var book = Book(title: "The Hitchhiker's Guide to the Galaxy", author: "Douglas Adams")

    var body: some Scene {
        WindowGroup {
            DetailView()
                .environmentObject(book)
        }
    }
}

// --

struct DetailHeader: View
{
    @EnvironmentObject var book: Book

    var body: some View {
        VStack {
            Text(book.title)
            Text(book.author)
        }
    }
}
  • 複数の階層から同じ object を見たい場合に有効
  • object が ObservableObject に従う場合はデータ更新により自動で UI も更新される
    • その場合は object は class のインスタンスである必要がある
  • 同じ型の object は同時に1つしか渡せない

ObservableObject / @ObservedObject or @StateObject

struct DetailView: View
{
    @ObservedObject var book: Book

    var body: some View {
        VStack {
            Text(book.title)
            Text(book.author)
        }
    }
}
  • ObservableObject に従う class は @ObservedObject もしくは @StateObject として利用できる
    • 2つの違いはどこで生成されるか。その View で生成される場合は @StateObject、別の場所で生成されて渡されてくる場合は @ObservedObject
  • @Published なプロパティが更新された場合に、自動的に UI も更新される
作成者以外のコメントは許可されていません