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

iOS 開発で用いられるアーキテクチャまとめ

少し時間がとれたので、iOS開発におけるアーキテクチャについて整理してみることにしました。
最近では、iOS開発におけるアーキテクチャについて語られる際には、MVVMとCleanアーキテクチャについて触れられることが多いように感じています。

そのため、MVVMとCleanアーキテクチャについて比較しながら理解を深めようとするのですが、用語が異なるため毎回混乱しています。
理由の一因として、MVVMのオブジェクトは役割分担が比較的明確に理解することができるのに対し、Cleanアーキテクチャの資料の多くは、概念に終始しているため実装例は様々な解釈が存在し、単純に比較することができないという点があると感じています。

個人的には、Cleanアーキテクチャの用語について馴染みがあまりない部分があるので、改めて用語についてまとめてみることにしました。

MVC、MVP、MVVM アーキテクチャ

このアーキテクチャは、それぞれ3階層構造で似ています。
View ←→ Controller ←→ Model
View ←→ Presenter ←→ Model
View ←→ ViewModel ←→ Model

Clean アーキテクチャ

【メリット】

  • ビジネスロジックが明確になる
  • FW、UI、DataStoreに依存しない
  • テストが導入しやすい

【デメリット】

  • 各層にインターフェイスが用意されるため、コード量が多くなる

ドメイン駆動開発(DDD)を意識して、ビジネスロジックをUIやFWから分離し、それぞれの階層ごとに役割と責務を分けたアーキテクチャ。

大別するとPresentation、Domain、Data Layer の3つの階層
Presentation Layer ←→ Domain Layer ←→ Data Layer

View ←→ Presenter ←→ UseCase(Translater、Model) ←→ Repository ←→ DataStore(Entity)

Presentation Layer

UIの表示やイベント ハンドリングを行う
ビジネスロジックは含まない
例えば、UseCase から取得したデータを成功、失敗に応じて処理し、View に通知する

Domain Layer

ビジネス ロジック、ビジネスルールを表す責務を負う

UseCase

どのようにデータを取得、生成するか実装する
例えば、Repository から取得したデータを加工し、ストリームに流す

Translater

UseCase で取得した Entity から View の表示に最適な Model を生成する

Repository

UseCase で取得したいデータの CRUD I/F を用意する
例えば、DataStore からデータを取得し、ストリームに流すなど

Data Layer

通信、永続化データを扱う

Data Store

複数の DataStore を扱う場合は、Factory パターンを用いて Repository がデータ種別を意識しない設計にすること
例えば、UserDefaults からデータを取得するなど

Entity

DataStore で扱うことができる性的なモデル

個人のまとめ

  • VC、Modelが肥大化するのはプログラマが対処すべき
  • Translaterの実装例がイメージしづらいのでサンプルを探したい
  • 依存関係を一方向のみとし、相互依存を排除する
  • 外部ライブラリを用いてアーキテクチャを実装することは避けたい
    (DI、Repository を SwiftTask (Promiseライブラリ) でラップなど)

参考資料について

アーキテクチャの種類について効率よく知るには、「iOSアプリ設計パターン」が参考になりました。

クリーンアーキテクチャ完全に理解した
個人的には学術的な理解は不要で、まずはなんとなく使えるレベルでOKだと考えているので、少しボリュームがあると感じましたが、用語、概念についてざっと知りたい場合に良いドキュメントだと思います。

CleanArchitectureRxSwift
RxSwiftを用いたサンプルですが、Cleanアーキテクチャの概念を理解するには良いサンプルだと思います。

SwiftUI + Combine で MVVM & Clean Architecture な設計を考えてみた
Cleanアーキテクチャに、SwiftUI、Combineなどをどのように用いるのか?という点について、とても参考になりました。ボリュームも抑えられているので、サクッと理解できると思います。

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 で直列に非同期処理

プロキシ環境下で cocoapods インストールエラーが発生する場合の対処方法

プロキシ環境下での環境構築時に経験した問題の対処方法をメモしておきます.

プロキシ環境下で cocoapods のインストール時に以下のエラーが発生した.

$ sudo gem install -n /usr/local/bin cocoapods

ERROR:  Could not find a valid gem 'rails' (>= 0), here is why:
Unable to download data from https://rubygems.org/ - no such name (https://rubygems.org/latest_specs.4.8.gz)

理由は,sudo コマンドに http_proxy 環境変数が適用されないため.
-E オプションを付加するとログインアカウントの環境変数を引き継いでくれる.

$ sudo -E gem install -n /usr/local/bin cocoapods

無事,cocoapods のインストールが行えました.

Apple WATCH 音楽再生中のコントロール画面を非表示にする方法

普段通勤途中に iPhone, Apple WATCH, AirPods Pro で音楽,YouTube を楽しんでいます.
その際,Apple WATCH がこのようなコントロールが表示されることがずっと不満でした.
WATCH 側で操作を行うことがないのに,音楽再生中はコントロール画面が表示され,通勤中はずーとお気に入り壁紙がキャンセルされることになります.

iOS14 で確認したところ,音楽再生中のコントロール画面を非表示にするには,以下の項目をオフにすれば良いようです.
WATCH アプリの [一般] – [画面のスリープ解除] – [オーディオAppを自動起動]


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

iOSシミュレータでVPNできない場合に確認する項目

最近,諸事情によりテレワークしています.
そのため自宅からお客様の環境へアクセスする際にVPNを使用しているのですが,iOSシミュレータがVPNを介して通信できないことがありました.
その際に確認した点をメモしておきます.

VPN接続ができたとしても,終日作業していると何らかの原因でVPN接続が切れることがありました.
通信ができない場合は,まずVPNの接続確認,再接続など確認した方がよいかもしれません.
また,VPNネットワーク 詳細… オプション 「すべてのトラフィックをVPN接続経由で送信」をチェックして通信状態が改善するか?確認すると解決できることがあるようです.(私はこれで改善できました.)

参考:Run app in iOS simulator with VPN connection