SwiftUIのサンプルで気づいた点をメモ

新しい技術については、必要に迫れられることがない限り、体系立てた知識の整理ができていません。あまり利用しない技術についてはすぐ忘れてしまうのです。(笑)
SwiftUIについても数年前から、調べては忘れてを繰り返しているのですが、再度、調べる際にメモがあると早く思い出せるようなので、今回もメモしておきます。

【PropertyWrapperの内部変数にアクセス】

PropertyWrapperを定義したプロパティに「_」をつけることで、PropertyWrapperの内部変数にアクセスする。
(ObjCのメンバ変数の表記に似ているので抵抗がありますが、間違いみたいではないのでメモしておきます。)

@Binding var isPresented: Bool

init(isPresented: Binding) {
    self._isPresented = isPresented
}

【$ と projectedValue】

「$」をつけると射影プロパティにアクセスできる。また、内部変数(_をつけた変数)から projectedValue というプロパティへアクセスするのと等価になる。

_isPresented.projectedValue

$isPresented

【SwiftUI Non-constant range 警告】

Non-constant range: argument must be an integer literal」という warning が出力される場合は、以下の様に対処できる。

ForEach(0 ..< myArray.count) { index in
    ...
}
	↓
ForEach(0 ..< myArray.count, id: \.self) { index in
    ...
}

参考:SwiftUI ForEachのNon-constant range警告

【KeyPath の利用】

List など作成する場合に表示データを以下の様な DataModel で表現した場合、初期化パラメータに KeyPath を利用した記述ができる。

struct DataModel: Identifiable {
    let id = UUID()
    let name: String
    var subItems: [DataModel]?
}

  init(
    data: Data,
    children: KeyPath<Data.Element, Data?>
	:
	省略
	:

// パラメータ指定時には以下の様に KeyPath を用いて記述できる
children: \.subItems

SwiftUI、UIKit の相互利用について

現行のアプリにSwiftUIを用いる場合、UIKitとの相互利用について考慮する必要があるため、調べてみました。

【UIKit → SwiftUI】

UIHostingController クラス

【SwiftUI → UIKit】

UIViewRepresentable プロトコル
UIViewControllerRepresentable プロトコル

SwiftUIでUIKitのUIButtonを利用するサンプルは以下の通り

import SwiftUI

struct CounterView: View {
    @State var text: String = "Button_title"
    
    var body: some View {
        VStack(alignment: .leading) {
            CustomButton(text: $text)
        }
    }
}

struct CustomButton: UIViewRepresentable {
    @Binding var text: String

    func makeUIView(context: Context) -> UIButton {
        let button = UIButton(frame: CGRect(x: 0, y: 0, width: 200, height: 44))
        button.setTitle(self.text, for: .normal)
        button.setTitleColor(.black, for: .normal)
        button.addTarget(context.coordinator, action: #selector(Coordinator.didTapCustomButton(sender:)), for: .touchUpInside)
        return button
    }

    // SwiftUI → UIkit
    // ビューの状態が更新されるたび呼び出される
    func updateUIView(_ uiView: UIButton, context: Context) {
        print("\(#function)")
    }

    static func dismantleUIView(_ uiView: UIButton, coordinator: ()) {
        print("\(#function)")
    }
    
    // UIKit → SwiftUI
    func makeCoordinator() -> Coordinator {
        return Coordinator(button: self)
    }

    class Coordinator {
        var button: CustomButton

        init(button: CustomButton) {
            self.button = button
        }

        @objc func didTapCustomButton(sender: UIButton) {
            print("\(#function)")
        }
    }
}

【参考】

一部の画面だけSwiftUIを使いたいとき
UIViewRepresentable を理解して SwiftUI の足りないところを UIKit で補う

SwiftUIの個人的なメモ

SwiftUIもそろそろ業務で利用されるかと思い、少しずつ情報を整理しています。
というわけで個人的なメモを以下に記します。

2022年秋には、iOS16が提供されることにより市場のアプリのサポートバージョンは、iOS14、15、16が一般的になると思います。結果、SwiftUIもそろそろ積極的に採用されるとの判断です。
実際調べてみると、SwiftUIはiOSバージョンにより挙動の差異があるようです。
この点は、以下のページで詳しく報告されています。

iOS バージョン による SwiftUI の機能差分・制限まとめ

SwiftUIを採用するということは、UIKitからの移行ということだと改めて認識しました。現行のままでは、移行しきれないUIデザインというのもあるのでしょうね。
UIKitのあのコントロールは、SwiftUIではどのコントロールに該当するのかというのは、以下のページが参考になりました。

[Swift] SwiftUIのチートシート

SwiftUIサンプルコードを見ていて、新しく見つけたProperty Wrapperは以下の通り。

@mainエントリポイント
@PublishedCombineフレームワークが提供。Publisherを生成する。
$を付加して、Publisherにアクセスできる。
@ObservedObjectSwiftUIの監視対象となり、参照しているViewが自動的に再描画される。
@State変数の変更が監視され、変更時にViewが再描画される。
アクセスは宣言されたView内のみとなり、変更時には$を付与する必要がある。
@StateObject再描画されても値は保持される。
@EnvironmentObjectアプリ内でデータを共有する。
@Environment環境値を取得する。
@FocusStateiOS15で導入。表示要素のフォーカス制御を行う。
SwiftUI Property Wrapper

リアクティブプログラミング手法では、UIとモデルのバインディングがどのように行うのか?ということが一つのテーマであったと思うが、SwiftUIはProperty Wrapperという形で提供している。その点も踏まえて、今後はSwiftUIと相性の良いアーキテクチャについても考えたい。

リアクティブプログラミング、Combine の個人的なメモ

時間があるとリアクティブプログラミングの調査は行っているのですが、毎回まとめる前に忘れてしまうということを繰り返していたので、今回はまとめてみた。
業務でいつも最新の手法のみを使っているわけではないので、習得してもまとめておかないと忘れちゃうんですよね。(笑)

リアクティブプログラミングの定義

非同期イベントの処理を宣言的な記述で書くプログラミング。

もう少しイメージしやすい表現だと、データ流れをイベントのストリームとして表現し、ストリームに応じた振る舞いをさせる考え方。

Combine概要

Combine が登場したのは 2019 年 6 月の WWDC。
iOS 13.0以上で使用できる。
オブジェクト間でイベントを伝える仕組みを提供する。

主要な要素と役割は、以下の通り。

  • Publisher – 時間の経過に沿ってイベントを伝達する役割
  • Operator – イベントを作成、もしくは処理する役割
  • Subscriber – Publisher から送信されたイベントを処理する役割
  • Cancellable – イベントをキャンセルできることを表現する役割
  • Subscription – Publisher と Subscriber の購読を表現する役割

subjectに対して、複数のsubscribeを行うことができる。
複数のsubscriptionを扱う場合、storeメソッドでまとめて保持する。
Publisherは、sendメソッドでイベントをpublishする。

参考: Apple Developer Documentation | Combine

Publisher イベントを発行する

Publisher を subscribe することでイベントを受信できる。
PassthroughSubject クラス
CurrentValueSubject クラス

Justとは 値を1回だけ出力するPublisher。
Futureは、通信処理などで利用していたコールバック処理をCombineで実装する場合に使用する。

Subscriber イベントを購読する

イベントを受信する(購読する)subscribe処理
assignメソッド
sinkメソッド

var subscriptions = Set()

Operator 流れてくる値を加工する

map
filter
compactMap   
combineLatest など

APIリクエストを行い、結果集合をメンバ変数に格納するサンプル

import Foundation
import Combine

final class SearchUserViewModel: ObservableObject {
    @Published var text = ""
    @Published var users = [User]()
    private lazy var subject = PassthroughSubject<Void, Never>()
    private var cancellables = Set<AnyCancellable>()
    private let searchUserModel: SearchUserModelProtocol
    
    init(searchUserModel: SearchUserModelProtocol) {
        self.searchUserModel = searchUserModel
        
        subject.sink(receiveValue: { _ in
            searchUserModel.fetchUser(query: self.text)
                .receive(on: RunLoop.main)
                .sink(receiveCompletion: { completion in
                    switch completion {
                    case .finished:
                        break
                    case .failure(let error):
                        print(error)
                    }
                }, receiveValue: {
                    self.users = $0
                })
                .store(in: &self.cancellables)
            })
            .store(in: &self.cancellables)
    }
    
    func searchButtonTapped() {
        subject.send()
    }
}

参考:【Combine】APIとの通信処理にCombineを取り入れる

非同期処理で待ち合わせ制御を行っている場合、自由にキャンセル処理を行うにはNSOperationを選択した方がよい

APIリクエスト処理の不具合に対処していて、非同期処理について調べたのでメモしておきます。

iOSアプリをSwiftで実装している場合、いくつかの実装方法があり、お手軽に対処するためにGCD(Grand Central Dispatch)、DispatchGroup、DispatchSemaphoreを選択したのですが、以下のような【非同期処理の直列化】を行おうとした場合、処理途中で自由に処理をキャンセルできず(割り込めない)、結局、Foundation.Operation(NSOperation)で実装することとなりました。
キャンセル処理が出来ないことなんてあるのか?と思いますが、実際にキャンセル処理は処理完了時にしか出来ないようでした。

【非同期処理】
タスク1 --->
タスク2 --->
タスク3 --->
後処理  --->

【非同期処理の直列化】
タスク1 --->
タスク2     --->
タスク3         --->
後処理              --->

※ 後処理は全タスクの終了を待ち合わせてから処理する

参考にしたURLは以下の通り。

【参考URL】
Foundation.Operationの並列オペレーションがよくわからない人向けの説明
【Swift】非同期処理「Operation」について
swift で直列に非同期処理

Swift5 で文字列検索

久しぶりに Swift で文字列検索処理を書こうとして、 index(of:) を使おうとしたら、思うような結果が得られず、ネットを検索しても新旧の情報が入り交じっていて、よく分からなかったので、まとめた。

配列の最初に一致したインデックス番号を取得

index(of:) → firstIndex(of:)
メソッド名が変更されているようだが、結果を利用することができなかった。
文字列から文字を検索して、単純にインデックス番号が取得したかったのだが、そういうものではないらしい。 😂
Index とか Range とかの意味を整理する必要があって、数分で内容を理解して使用することができなかった。

文字列から文字を検索できるけど…

let str = "abcabc"
let firstIndex = str.firstIndex(of: "c")
print(type(of: firstIndex), firstIndex ?? 0)

出力結果 :
Optional Index(_rawBits: 131329)

Index型は、文字列の範囲指定に使用するものらしい…

文字列から文字を検索して、出現するインデックスを取得

以下の方法であれば、インデックス番号が取得できる。

let str = "abcabc"
let indexes = str.enumerated().compactMap { $0.element == "c" ? $0.offset : nil }
print(type(of: indexes), indexes)
// compactMap は配列のデータから nil を取り除きたい場合に使用する関数

出力結果 :
Array [2, 5]

NSRange で文字列中で指定した文字列が最初に出現する範囲を検索して取得

NSRange で取得するのが便利そう。

var str = "abcdef"
let range = NSString(string: str).range(of: "cde")
print("location:", range.location, "length:", range.length)

出力結果 :
location: 2 length: 3

文字の出現回数を正規表現で取得

こちらも便利です。

let string = "abcdefgabcdefg"
let pattern = NSRegularExpression.escapedPattern(for: "a")
let regex = try! NSRegularExpression(pattern: pattern, options: .caseInsensitive)
print(regex.numberOfMatches(in: string, range: NSRange(0..<string.utf16.count)))

出力結果 :
2

Swiftの範囲についてもよく知らなかったので、まとめた。

これだけ知っていれば良いと思う。

Range0..<30 ~ 2
ClosedRange0…30 ~ 3
let range1 = 0..<3
print("type: \(type(of: range1))")
print("lower:\(range1.lowerBound) upper:\(range1.upperBound) count:\(range1.count)")
print("range1.contains(0) \(range1.contains(0))")
print("range1.contains(1) \(range1.contains(1))")
print("range1.contains(2) \(range1.contains(2))")
print("range1.contains(3) \(range1.contains(3))")
print("range1.contains(4) \(range1.contains(4))")

出力結果 :
type: Range<Int>
lower:0 upper:3 count:3
range1.contains(0) true
range1.contains(1) true
range1.contains(2) true
range1.contains(3) false
range1.contains(4) false
let range2 = 0...3
print("type: \(type(of: range2))")
print("lower:\(range2.lowerBound) upper:\(range2.upperBound) count:\(range2.count)")
print("range2.contains(0) \(range2.contains(0))")
print("range2.contains(1) \(range2.contains(1))")
print("range2.contains(2) \(range2.contains(2))")
print("range2.contains(3) \(range2.contains(3))")
print("range2.contains(4) \(range2.contains(4))")

出力結果 :
type: ClosedRange<Int>
lower:0 upper:3 count:4
range2.contains(0) true
range2.contains(1) true
range2.contains(2) true
range2.contains(3) true
range2.contains(4) false

Swift のキーワードを今更ながら調べる

最近ずっと避けてきた Swift のキーワードを勉強しています.(^_^;
正直ジェネリクスを利用したヘッダーはノリでは分かる,もしくは,分かった気になれますが,完全に理解するのは私にはとても忍耐がいる作業です.

参考になったページをメモしておきます.

typealias
associatedtype
ジェネリクスの<>の記述が分からない場合に参考になると思います.

【Swift】associatedtypeの使いどころ
typealiasというSwiftの仕様を把握する
→ 言語仕様が変更されて一部,associatedtype にする必要がある.
【Swift】ジェネリクスが使われているメソッドを理解する

RxSwift 個人メモ

iOS アプリの Reactive プログラミングは,最終的には Apple 謹製のフレームワークになるかと思いますが,世の中には既に RxSwift を利用した iOS アプリが多数存在しながらも,学習コストが比較的高い分野のため,開発フェーズ後はメンテできる人がおらず,手つかずになっているアプリも沢山あると想像しています.

最初にRxSwiftに入門した際には,概念を把握するのにとても苦労しました.
理由は,体系的に説明された資料が見つけられず,ネットの断片的な資料を探すことから始めたため,概要の把握が思いのほか大変であったためです.

個人的なメモですが,最初に知っておくべき概念,用語,何度も見返したい参考ページをまとめます.

▼ Rx とは?
Rx(Reactive X)とは,「オブザーバパターン」「イテレータパターン」「関数型プログラミング」の概念を実装している拡張ライブラリ.
Rxの導入メリットは,「値の変化を検知できる」「非同期の処理を簡潔に書ける」こと.
値の変化には,変数値の変化やUIの変化も含まれる.

▼ Rxの三要素
– Observable
– Operator
– Scheduler

▼ Reactive Programming 概要
SwiftでReactive Programming

▼ RxSwift で参考になった資料
RxSwiftの機能カタログ
俺はこれでRxSwiftを学んだ!リファレンス集
[RxSwift] shareReplayをちゃんと書いてお行儀良くストリームを購読しよう
[Rx入門] Subject詳解 / Hotな、ColdなObservableのこと
RxSwiftのobserveOnとsubscribeOnを理解する // Speaker Deck
よく使いそうなRxSwiftのオペレーターの勉強をしてみた(flatMap, flatMapFirst, flatMapLatest 等)
RxSwift のスケジューラ
→ observeOn,subscribeOn の違いについて参考になった.
RxSwift4で廃止になった Variable のリプレイス

▼ サンプル コード
RxExample MVVM のその先へ(Fat ViewModel の倒し方)
RxSwiftとUIライブラリの表現を組み合わせたサンプル紹介
リバース・エンジニアリング – RxSwift

▼ 用語 について
Observable イベント発生元
Observer イベント処理

▼ Observable の破棄
– disposeBagで破棄する.
– subscribeメソッドの戻り値Disposableインスタンスに対してdispose()メソッドを呼び出す方法.

▼ Driver/Signal
Driver
subscribe()後,イベントを1つ流してほしい場合に使用する.
(UITextField の入力文字列,UIButton の有効/無効状態など)
Signal
subscribe()後,イベントが発生するまで,イベントを流さないで欲しい場合に使用する.
(UIButton のタップイベント,アニメーションの完了イベントなど)

▼ Hot/Cold
Subject から送出される値が「Hot」,それ以外が「Cold」.
Cold を Hot に変換するには,以下のメソッドを使用する.
Publish()メソッドは,Hot 変化オペレータ.Connect()メソッドは,ストリーム開始.

– share(replay: 1)は,Cold Observable を Hot Observable へ変換するためのオペレータ.

▼ Hot な Observable の特徴
– subscribe されなくても動作する
– 複数の箇所で subscribe したとき、全ての Observable で同じイベントが同時に流れる.

▼ Cold な Observable の特徴
– subscribe したときに動作する
– 単体では意味がない
– 複数の箇所で subscribe した時,それぞれの Observable でそれぞれのイベントが流れる.

▼ Subject/Relay
Subject「通信処理や DB 処理等」エラーが発生したときにその内容によって処理を分岐させたい
Relay UI に値を Bind する
– PublishSubject
– BehaviorSubject
– PublishRelay
– BehaviorRelay

Swift の associatedtype を最短で理解できたページ

associatedtype キーワードがよくわからなかったので調べたのだが,ジェネリックがどうとか難しいページが多かった.
キーワードの意味だけをわかった気になりたかったのだが…
以下のページを見ると2分くらいで理解できました.

associatedtype とは,protocol を定義する時には型が決まっていないものに使用する.
protocol 実装側で型を決められるようになる.

swiftの謎キーワードだったassociatedtype, mutating, subscriptについて