最近とあるコンテスト(物は言いよう、ですが)向けにiOSのプロトタイプアプリを作ったので、意識して取り入れたこと5つです。
いずれも新しい発想ではありませんが、今後のiOSアプリ開発で意識し続けたいことです。
1. SwiftPM の Package.swift で依存ライブラリ管理
従来 CocoaPods や Carthage でやってきたライブラリ依存関係の管理ですが、今回は SwiftPM でやりました。
今回、アーキテクチャーは MVVM で、View 以外をパッケージに小分けしたんですが、パッケージの場合はプロジェクト直下の場合と違って、依存関係を Package.swift に書けます。
dependencies: [
.package(path: "../Model"),
.package(url: "https://github.com/ReactiveX/RxSwift.git", from: "6.5.0"),
],
targets: [
.target(
name: "Repository",
dependencies: ["Model", "RxSwift"]),
.testTarget(
name: "RepositoryTests",
dependencies: ["Repository"]),
]
依存関係以外にも、対象バージョンを
platforms: [
.iOS(.v15),
.macOS(.v12),
],
と書いたり。iOSアプリですが.macOS
も書かないと Swift UI のプレビューが使えません。
これの何がよいかというと、脱.pbxproj
(の差分管理の問題)が進むんですよね!
詳しくは、以下の記事に書かれています。
最初 Package.swift は提供してくれるライブラリ側が書くものでアプリは書かないものだと思ってたんですが、こういう使い方もあったんですね。
2. ViewModel の I/O
入力と状態変化の両方が発生する ViewModel の場合、各フィールドやメソッドをどこまでpublic
にするかは考えどころです。
今回は、ViewModelの中にViewModelInputとViewModelOutputのインスタンスを持たせ、ViewModelInputには入力値を受け取る変数や入力補助メソッドしか持たせず、
public class MenuListViewModelInput: ObservableObject {
@Published public var searchingText: String = ""
@Published public var selectedMenus: [MenuViewModel] =
MenuListViewModel.allMenus
public func selectAll() {
・・・
}
}
かたやViewModelOutputには、違うファイルからはget
しかできないフィールドを持たせました。
public class MenuListViewModelOutput: ObservableObject {
@Published fileprivate (set) public var fetchedMenus: [MenuViewModel] = []
@Published fileprivate (set) public var errorMessage: String = ""
}
これにより、機能へのアクセスコントロールを強化できたことと、SwiftUIの@EnvironmentObject
によって例えば入力しか行わないViewにはViewModelInputだけを注入するといった、余分な依存関係の排除が可能になりました。
ちなみにViewModel本体は、ViewModelInputとViewModelOutputの2つのインスタンスとInitializer、定数くらいしか持たない構成です。
3. View はとにかく小分け
SwiftUI を使い慣れたり、元々 Web をやってたという人からすると当たり前の習慣ですが、SwiftUI の View は切り出して使いまわしがしやすいです。
だから例えばある View のコードを書いててデザインを含んだボタンを作ったら、そのボタンを1つのコンポーネントとして切り出せます。
import SwiftUI
struct SelectableButton: View {
let text: String
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
Text(text)
.padding(6)
.font(.system(size: 12, weight: .black, design: .default))
.foregroundColor(.white)
.background(isSelected ? Color.red : Color.gray)
.clipShape(Capsule())
}
}
}
このとき気をつけることは、抽象的なもの( ViewModel とか)には依存してよいが、具象を持つもの(ボタンを押したときの処理とか)には極力依存しないことです。
上記の例だと action
、つまりボタンを押したときの処理を外から与えるようにしているところですね。
ただし、具象に依存せざるを得ない場合はあります。View 同士です。MenuListView が MenuView(リスト中の1個) を持たざるを得ない場合とかです。
潔癖症レベルになる必要はなく、やれる範囲で再利用を心がければ十分だと思います。
4. グリッドビューを超簡単に実装
グリッドビューに画像を表示するアプリなんですが、従来の UICollectionView だと色々作り込まないといけないので、着手までに腰が重いです。
今回は Swift UI の LazyVGrid と、子 View には画像を非同期で読み込んでくれる AsyncImage を使い、合計 40 行程度で実装できました。
しかし、スクロールしてから戻るとビューが破棄されており、再び通信が走ってしまうので、一度読み込んだ画像をキャッシュしてくれる CachedAsyncImage に置き換えました。
これも Web 界隈の後追いで、使いたいコンポーネントを手軽に取ってこれるようになりましたね。
自分自身も何らかの View を第一人者として作ったらパッケージで公開したいと思います。
5. Concurrency
Swift 5.5 で追加された async
/ await
をサーバー通信に使いました。今回高度なことはしていませんが、これまた着手するのに腰が重かった通信まわりが数行で書けるようになり、コールバック地獄も昔話になりました。
この話は詳しい記事が他にいくらでもあるので、割愛します。
5選を均等のボリュームで書くつもりでいたら、4つ目くらいで持ち時間が残り10分になったので、一旦ここで投稿します。もし需要がありそうならもっと追記します。