[Swift][iOS] Xcode Playground を使って効率よく開発を進める


はじめに

Playground を使ってちょっとしたコードを書いたりすることはあると思いますが、Kickstarter の iOSアプリ のように Playground をアプリ開発に活用すると得られるメリットが大きいなと感じて導入した時のメモです。


TL;DR

この手法を取り入れると、以下のメリットが得られます。


  • 複数解像度での UI 確認が爆速でできる

  • 画面単位での確認が爆速でできる

  • 依存関係が少ない構成にしやすい


    • メンテナンスが楽になる

    • テストがやりやすくなる

    • 複数人での開発に合う(分担開発がやりやすい)



もう少し具体的には、

* 「iPhone5s の時だけ文字が見切れる、といった問題の対応がすぐできる」

* 「アプリの奥深くにある画面の動作確認をするために画面遷移をしなくてよくなる」

* 「プロトコルをしっかり定義しておけば、UIの実装とAPI呼び出しの実装の担当者を分けることができる」

といったメリットがあります。

デメリットと感じるかは人それぞれだと思いますが、この手法を採用する場合、Cocoa touch framework を追加して、Playground で確認したい UI を Cocoa touch framework の中で開発する必要があります。


Xcode Playground

Playground はコードを簡単に実行できる環境ですが、WWDC2018 の Getting the Most out of Playgrounds in Xcode の中で自作の Cocoa touch framework を呼び出す例が紹介されています。

この仕組みをアプリのUI開発に活用しているのが冒頭の Kickstarter のアプリです。


やり方


開発中のアプリを Workspace に追加する

Workspace を作って、その中に開発しているアプリの Project を追加します。

※Cocoapods を使用していて Workspace を使っている場合はこの作業はスキップできます。


Workspace に Playground を追加する

メニューから Playground を追加します。

Screen Shot 2018-12-07 at 22.24.46.png


Playground で使用するコードを追加する

これは Kickstarter のリポジトリからコピペしてきます。playground 直下の Sources に置きます。playground 直下の Sources は public 扱いで他の Playground Page からアクセスできるコードを置きます。


各 UI を確認する Playground Page を追加する

確認したい画面ごとに Playground Page を作りましょう。ここでは起動時に表示する "ようこそ" 画面(WelcomeViewController)を確認する Playground Page "WelcomePage" を作成します。

Screen Shot 2018-12-07 at 0.54.40.png


Playground で確認する ViewController は Cocoa touch framework 側に置く

ここがひとつポイントになります。Playground からアクセスできるのは Cocoa touch framework で、Playground から Application target にあるコードは見えません。そのため Playground を使用する場合の構成は、下記のようになります。

Architecture.png

実際には他にも Cocoa touch framework を作って分割する場合もあるかもしれませんが、重要なのは、

- 各 UI の表示ロジック

- データを表す struct (DDDでいう Value Object や Entity)

- データに対して何(CRUDなど)を行うかを定義したプロトコル

だけを Cocoa touch framework 側に持たせて、


  • データを処理する具体的な実装(通信するのか、ローカルファイルから読み込むのかなど)← これがプロトコルの実装

  • 画面間の遷移(Tabbar や NavigationController とか)

などは Application Target 側に持たせておく方がいいかなと思っています。(別の Cocoa touch framework に移してもいいと思います)

そうすることで、Cocoa touch framework 側では "単一の画面の表示" だけに関心が切りだされ、App Target 側で各画面を組み合わせてアプリを作っていくことに関心を分けることができます。関心ごとを分離することで作業分担がしやすくなります。

どんなアーキテクチャでもプロトコルを境界に UI とデータの扱い、など関心ごとを分離することが、クラス間の依存関係を疎にするポイントですね。疎にすることである部分のコードの変更の影響範囲を限定的にすることができ、チームでの開発にスピードを与えます。


Playground Page で画面を表示する

ここからは Playground を操作していきます。Live View 上に View を表示するためにPlaygroundSupportを、自前のUI(ここではWelcomeViewController)を使用するために作成した Cocoa touch framework をインポートしておきます。

import UIKit

import PlaygroundSupport
import 自分のFramework


WelcomeViewController を生成する

WelcomeViewController を生成します。これは通常のアプリ開発の時にも当てはまりますが、ViewController 生成用の static 関数(ここでは make 関数という名前にしています)を作っておくとインスタンスを生成する時に便利です。

struct DependencyData {

let welcomeText: String
}

final class WelcomeViewController: UIViewController {
private lazy var dependency: DependencyData = { fatalError() }()
static func make(withDependency dependency: DependencyData) -> WelcomeViewController {
let vc = UIStoryboard(name: "Welcome", bundle: Bundle(for: self))
.instantiateViewController(withIdentifier: String(describing: self)) as! WelcomeViewController
vc.dependency = dependency
return vc
}
}

ここのサンプルコードでは ViewController で使用するデータ(DependencyData)を make関数実行時に引数として渡すようにしています。


WelcomeViewController を表示する

WelcomeViewController を生成できたら表示していきます。生成したものを Kickstarter から持ってきたコードを使って表示します。表示するための display関数を作成します。

let dependency = DependencyData(welcomeText: "Playground を使ったUI開発をやってみませんか?\nPlayground を使ったUI開発をやってみませんか?\nPlayground を使ったUI開発をやってみませんか?")

func display(device: Device, orientation: Orientation) {
let vc = WelcomeViewController.make(withDependency: ())
_ = vc.view
vc.inject(dependency: dependency)

let (parent, _) = playgroundControllers(device: device, orientation: .portrait, child: vc)
PlaygroundPage.current.liveView = parent
}

playgroundControllers 関数の中では、

1. 親となる ViewController を生成して

2. WelcomeViewController を addChild() して

3. 指定された画面サイズに親の ViewController のサイズを調整する

ということをしています。こうすることで画面サイズが簡単に変えられるようになっています。


display 関数を呼び出す

これで準備完了です。Assistant Editor から Live View を開いてから呼び出してみましょう。device の所に表示したい端末の enum 値を指定します(Kickstarter から持ってきたコードに定義されています)

display(device: .phone5_5inch, orientation: .portrait)

ここの例では 5.5インチサイズの端末の画面を表示しています。

Play1.gif

ViewController が Playground 上で動作しているのが確認できます。Button のイベントも動作し、ポップアップを表示することもできます。このように Playground を活用することで実機や Simulator をデバッグ実行することなく開発中の ViewController の確認ができます。


Playground の step 実行

また Xcode10 から Playground を step 実行ができるようになりました。step 実行を活用することで、複数の画面サイズの端末で表示するコードを続けて書いておいて、

display(device: .phone5_5inch, orientation: .portrait)

display(device: .phone4_7inch, orientation: .portrait)
display(device: .phone4inch, orientation: .portrait)
display(device: .pad, orientation: .portrait)

表示したい画面サイズの行まで step 実行することで簡単に各サイズでの動作を確認することができます。

↓のgifでは、

1. 5.5inch 画面で表示

2. 4.7inch 画面で表示

3. 4 inch 画面で表示

を step 実行をしながら確認しています。

Play2.gif

この例では、4.7inch、4inch画面で、画面中央の文言が全て表示できずに途中で見切れているのが確認できます。

実際のアプリ開発に取り入れると、このように「画面の小さい端末で表示がおかしい」などの問題が出た時にすぐに対処できるようになります。


Dependency Injection を活用した UI の確認

この手法で開発を進める時のポイントとして、「表示に必要なデータ・処理などの依存するものは、全て外部から注入できるようにしておく」ことです。いわゆる Dependency Injection(DI) を活用します。

先ほどの例で、画面中央の文言が見切れていましたが、表示する文言は DependencyData という struct で注入されていました。

let dependency = DependencyData(welcomeText: "Playground を使ったUI開発をやってみませんか?\nPlayground を使ったUI開発をやってみませんか?\nPlayground を使ったUI開発をやってみませんか?")

ここで表示する文字を減らして、4.7inch や 4inch の端末でも文言が全て表示できるようにしてみます。

Play3.gif

文字数を減らすと小さい画面サイズの端末でも全ての文言が表示できることが簡単に確認できました。

この例では ViewController が依存していたものが struct だけでしたが、実アプリ開発では「データを取ってくる」ことを表す関数を持った protocol を注入する場合もあると思います。

ちなみに ViewController 側のコードを修正した場合は Cocoa touch framework をビルドしないと Playground 側で反映されないので注意です。


まとめ

Playground を活用して、特定の画面をすぐに表示・動作を確認できることを試しました。


こんな時に活用できそう

アプリ開発の現場では「iPhone5s でレイアウトが崩れる」という話を聞いた場合、

1. iPhone5s 端末を探してくる、または Simulator を起動する

2. アプリを Debug 実行する(デバイス登録してないと登録するところから・・)

3. レイアウトが崩れる現象を確認する

4. コードを修正する

5. 2へ

といった作業を繰り返したり、

また、設定画面の中の深い階層の画面を実装している場合、

1. コードを書く

2. アプリを Debug 実行する

3. 開発中の画面までボタンをタップしたりして遷移する

4. 画面・挙動を確認する

5. コードを修正する

6. 2へ

という「非効率なのは分かってるけど他に選択肢なさそうだなー」と思っていたことが Playground を使うことで必要な部分にだけ注力できるようになるのではないでしょうか。

また、「あるデータを取得して、取得したデータを画面に表示する」機能を作る場合、

1. データを取得する関数を定義した Protocol A を定義する

2. Protocol A に依存する ViewController B を定義する

3. X さんは Protocol A が DI で渡される前提で ViewController B を実装する

4. Y さんは Protocol A を実現する class を実装する

5. 二人の実装が終わったら DI で組み合わせて動作

という風にスムーズに複数人で開発することもできると思います。


というわけで

開発中のプロジェクトに Playground を追加してみませんか?


おまけ

Cocoapods を使っているプロジェクトで Playground 実行をしようとするとエラーになることがあります。

ここの記事が参考になりました。

https://learnappmaking.com/cocoapods-playground-how-to/


  • Playground 実行時に Couldn’t lookup symbols でエラーになる

Podfile に下記スクリプトを追加すると Playground が実行できるようになります。

post_install do |installer|

installer.pods_project.build_configurations.each do |config|
config.build_settings.delete('CODE_SIGNING_ALLOWED')
config.build_settings.delete('CODE_SIGNING_REQUIRED')
end

installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['CONFIGURATION_BUILD_DIR'] = '$PODS_CONFIGURATION_BUILD_DIR'
end
end
end