MoTLab -GO Inc. Engineering Blog-MoTLab -GO Inc. Engineering Blog-

Xcode Previews をより使いやすくするための3つの工夫


タクシーアプリ「GO」の iOS アプリを開発している久利です。今回は Xcode Previews をより使いやすくするための3つの工夫についてご紹介します。


はじめに

タクシーアプリ「GO」の特徴として、多種多様な状態があることが挙げられます。ユーザーの状態、タクシーの状態、地図の状態等、様々な要素の影響を受け、状態毎に View の表示を切り替える必要があります。

An image from Notion

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"
}
An image from Notion

GO では、解像度が大中小と SafeArea がある端末を網羅できる形で、上記の3端末にしています。previewDevice(_:) の引数は PreviewDevice という型ですが、ExpressibleByStringLiteralに準拠しているため、文字列を渡すことができます。

渡す文字列に関しては、% xcrun simctl list devicetypesで取得できるものが対象です。

画面の一部として表示する View の確認

モーダルとして 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 を準備するだけで、他は通常のプレビューと同じ実装になります。

An image from Notion

Xcode Template を使ったボイラープレートの生成

プレビュー表示するためのボイラープレートを Xcode Template 使って自動生成できるようにしました。

An image from NotionAn image from Notion
// ___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 に定義している名前とディレクトリ名を一致させることで、チェックボックスの状態によって利用するテンプレートを分岐することができます。

An image from NotionAn image from Notion

最後に

Xcode Previews を使い、複数の端末での確認やパターン別の確認が早くなり、開発の効率を上げることができました。また、使いやすくすることでプレビューを作成すること自体の負荷を下げることができました。

しかしながら、実際に使っていく中で課題もいくつか見つかりました。

  • そもそものビルド時間が長いのでプレビュー表示まで時間がかかる
  • 少し修正しただけで Resume になることが多い
  • プレビューでエラーが発生した際の原因が分かりにくい

ビルド時間が長い問題に関しては、以前からの課題になっているので、モジュール化の検討をしている最中です。まだまだ改善できることがある状況なので、案件と並行しつつ日々改善していければなと思っています。


We're Hiring!

📢
Mobility Technologies ではともに働くエンジニアを募集しています。

興味のある方は 採用ページ も見ていただけると嬉しいです。

Twitter @mot_techtalk のフォローもよろしくお願いします!