タクシーアプリ「GO」の iOS アプリを開発している久利です。今回は Xcode Previews をより使いやすくするための3つの工夫についてご紹介します。
タクシーアプリ「GO」の特徴として、多種多様な状態があることが挙げられます。ユーザーの状態、タクシーの状態、地図の状態等、様々な要素の影響を受け、状態毎に View の表示を切り替える必要があります。
迎車〜乗車中までのViewの切り替わり
開発段階では、Mock 環境(タクシーアプリ開発で開発効率を上げるためのデバッグメニューのAPIレスポンスの変更でご紹介)で、APIレスポンスをコード上で変更しながら View が正しい状態かを確認していました。
Mock の変更 -> ビルド -> シミュレーターで確認の手順だとビルド時間が長いこともあり、確認に時間がかかることが課題でした。
そこで、Xcode Previews を導入し View の状態毎にプレビューを作成することで、開発の効率化を行いました。
View のほとんどは UIKit で実装されてたので、導入に関してはメルペイさんのXcode Previewsを用いたUIKitベースのプロジェクトの開発効率化を参考にさせていただきました。(いつも参考になる記事ありがとうございます。)
導入後、Xcode Previews をより使いやすくする上で実施した3つの工夫についてご紹介します。
「iPhone SE だと文字が切れてます」とレビューや QA で発覚することはないでしょうか?
プレビューでは previewDevice(_:) を指定することで、任意の端末サイズでプレビューを確認することができます。デフォルトは現在 Xcode で選択されている端末です。
一度に複数の端末で確認をしたいので、以下の様な enum を準備して確認しています。
enum Device: String, CaseIterable {
case iPhoneSE1st = "iPhone SE (1st generation)"
case iPhone8 = "iPhone 8"
case iPhone13ProMax = "iPhone 13 Pro Max"
}
GO では、解像度が大中小と SafeArea がある端末を網羅できる形で、上記の3端末にしています。previewDevice(_:) の引数は PreviewDevice という型ですが、ExpressibleByStringLiteralに準拠しているため、文字列を渡すことができます。
渡す文字列に関しては、% xcrun simctl list devicetypesで取得できるものが対象です。
モーダルとして View の高さ分だけ表示する画面や、画面の一部として横幅は端末に合わせるけど高さは可変で表示したいことがあると思います。
そのまま対象の View をプレビューで表示してしまうと、View が全画面に表示されてしまい、実際の見え方と異なる場合があります。
この問題を解消するために PreviewContainerViewController を作り、画面の一部としてプレビューを表示できるようにしました。
// PreviewContainerViewController.swift
import Foundation
import EasyPeasy
class PreviewContainerViewController<Child: UIViewController>: UIViewController {
enum Position {
case top
case left
case right
case bottom
case center
}
let child: Child
let position: Position
init(child: Child, position: Position = .center) {
self.child = child
self.position = position
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = Color.black40()
view.addSubview(child.view)
addChild(child)
child.didMove(toParent: self)
switch position {
case .top:
child.view.easy.layout(
Top(),
Left(),
Right()
)
case .left:
child.view.easy.layout(
Top(),
Left(),
Bottom()
)
case .right:
child.view.easy.layout(
Top(),
Right(),
Bottom()
)
case .bottom:
child.view.easy.layout(
Left(),
Right(),
Bottom()
)
case .center:
child.view.easy.layout(
Center()
)
}
}
}
PreviewContainerViewController を継承した class を準備するだけで、他は通常のプレビューと同じ実装になります。
プレビュー表示するためのボイラープレートを Xcode Template 使って自動生成できるようにしました。
// ___FILEHEADER___
import SwiftUI
private struct Wrapper: UIViewControllerRepresentable {
typealias UIViewControllerType = Container___VARIABLE_productName___
let inputs: [Container___VARIABLE_productName___.Input]
func makeUIViewController(context: UIViewControllerRepresentableContext<Wrapper>) -> UIViewControllerType {
Container___VARIABLE_productName___(child: ___VARIABLE_productName___())
}
func updateUIViewController(_ uiViewController: Container___VARIABLE_productName___, context: UIViewControllerRepresentableContext<Wrapper>) {
inputs.forEach {
uiViewController.apply(input: $0)
}
}
}
struct ___VARIABLE_productName____Preview: PreviewProvider {
static var previews: some View {
Group {
ForEach(Device.allCases, id: \.self) { device in
Wrapper(inputs: [])
.previewDevice(PreviewDevice(rawValue: device.rawValue))
.previewDisplayName(device.rawValue)
}
}
}
static var platform: PreviewPlatform? = .iOS
}
final class Container___VARIABLE_productName___: PreviewContainerViewController<___VARIABLE_productName___> {
}
// MARK: - InputAppliable
extension Container___VARIABLE_productName___: InputAppliable {
enum Input {
// Add view update pattern.
}
func apply(input: Input) {
// Call view update method.
// e.g. child.updateText()
}
}
画面の一部として表示する View の確認の章で挙げた PreviewContainerViewController を利用する/しないのパターンで生成できるようにしています。
TemplateInfo.plist の Options に Type が checkBox の項目を追加します。Identifier に定義している名前とディレクトリ名を一致させることで、チェックボックスの状態によって利用するテンプレートを分岐することができます。
テンプレートのディレクトリ構成
Xcode Previews を使い、複数の端末での確認やパターン別の確認が早くなり、開発の効率を上げることができました。また、使いやすくすることでプレビューを作成すること自体の負荷を下げることができました。
しかしながら、実際に使っていく中で課題もいくつか見つかりました。
ビルド時間が長い問題に関しては、以前からの課題になっているので、モジュール化の検討をしている最中です。まだまだ改善できることがある状況なので、案件と並行しつつ日々改善していければなと思っています。
興味のある方は 採用ページ も見ていただけると嬉しいです。
Twitter @mot_techtalk のフォローもよろしくお願いします!