Swiftによるシリーズアプリの共通部分を切り出して一括管理する

シリーズ物のアプリにおいて機能はほとんど同じでパッケージ毎の違いは文言や色の違いだけの場合も多いかと思います。そのようなときのTipsを共有します。モジュールとしてクラスを切り出して分けることも可能です。

方針としては大きく以下の3つのステップを取ります。

- R.swiftの導入による文言や色の一元管理
- CocoaPodsのパッケージ化
- アプリごとに異なる部分の切り出し

# R.swiftによる文言や色の一元管理
最初に共通部分を切り出す準備をします。シリーズによって文言や色が違う場合について取り上げます。

以下の3つの流れとなります。

- R.swiftの導入
- 色を管理するテキストファイルをclr拡張子へ変換ツールの導入
- ビルド時のR.swiftの実行のスクリプトを追加

## R.swiftの導入
### What's R.swift?
[R.swift](https://github.com/mac-cain13/R.swift)とは文字列や色などのリソースを一元管理する他、リソース指定時のタイポを防ぐ他、予測変換や静的解析による事前のバグ防止をすることができるものです。Androidでの概念を持ち込んだものとなります。

### 利用方法
GitHubページでの[README](https://github.com/mac-cain13/R.swift)では以下のような例が取り上げられています。
R.swiftの使用前では以下のようなコードがあるとします。

```
let icon = UIImage(named: "settings-icon")
let font = UIFont(name: "San Francisco", size: 42)
let viewController = CustomViewController(nibName: "CustomView", bundle: nil)
let string = String(format: NSLocalizedString("welcome.withName", comment: ""), locale: NSLocale.current, "Arthur Dent")
```

R.swiftの使用後は以下のようになります。

```
let icon = R.image.settingsIcon()
let font = R.font.sanFrancisco(size: 42)
let viewController = CustomViewController(nib: R.nib.customView)
let string = R.string.localizable.welcomeWithName("Arthur Dent")
```

#### リソースの指定方法
文字列について、こちらはAndroidでのstring.xmlに相当するものとなります。
プロジェクトで対象のグループを右クリックで[New File...]を選択。
Strings Fileを選択して[Next]を選択。
Save asにファイル名(ここでは`sample`)を入力して[Create]を選択。
`sample.strings`が生成されるので、以下のように記述します。

```
"sampleTest1" = "hoge";
"sampleTest2" = "fuga";
```

例えば、"hoge"という文字列にアクセスするためには以下のように指定します。

```
R.string.sample.sampleTest1()
```

色については、こちらはAndroidでのcolor.xmlに相当するものとなります。
R.swiftでは、clr形式のバイナリファイルを利用します。

```
A0A0A0 primary_text
```

例えば、上記のようなテキストファイルからclrファイルを生成したときは以下のように指定してUIColorの値を取得します。

```
R.color.default.primary_text()
```

clrファイルの作成方法は後述する。

### 導入
Podfileに以下を追加する

```
pod 'R.swift', '~>3.2.0'
```

Terminalで以下のコマンドを実行します。

```
$ pod install
```



## HexToClrの導入
R.swiftで色の読み込みをclrファイルから行うがこちらはバイナリファイルで扱いが難しいので、16進数で表記したRGBのテキストファイルで管理してビルド時にclrファイルに変換して管理します。

[ColorTools](https://github.com/ramonpoca/ColorTools)のHtml2Clrを使用すると上記の変換が可能である。しかし、Html2Clrでは指定した色と微妙に異なる色となることがある。これはキャリブレーションによる影響で、[こちら](http://qiita.com/noppefoxwolf/items/5132d173b7d82579932a)の記事を書かれた方が公開している[HexToClr](https://github.com/noppefoxwolf/HexToClr )というツールを使用すると良い。

以下、Terminalで作業をする。

```
$ https://github.com/noppefoxwolf/HexToClr
$ cd HexToClr
```

Swift Package Managerによるビルドをします。

```
$ swift build
```

すると、`.build/debug/`以下に`HexToClr`の実行ファイルが生成されます。
Home直下にコピーして動作確認してみる。

```
$ cp .build/debug/HexToClr ~
```

色を16進数で定義するファイルを作成する。

```
$ vim sample.colors
```

例えば、以下のように作成する。

```
424242 primary_text
000000 primary_dark_text
717171 primary_light_text
```

```
$ ./HexToClor sample.colors
```

出力として`Done`と表示され、`sample.clr`が生成されれば良い。

プロジェクトに実行ファイルを移動しておきます。

```
$ mkdir path/to/＜プロジェクト名＞/＜プロジェクト名＞/tools
$ mv ~/HexToClr path/to/＜プロジェクト名＞/＜プロジェクト名＞/tools/
```

プロジェクトから[Add Files to ...]を選択し、`path/to/＜プロジェクト名＞/＜プロジェクト名＞/tools/HexToClr`を選択する。

## ビルド時のR.swiftの適用

プロジェクトのサイドメニューの一番上の＜プロジェクト名＞を選択。
Build Phases中で[+]を選択し、[New Run Script]を選択。
現れる[Run Script]を展開し、`Type a script or drag a script file from your workspace to insert its path`とプレースホルダーで表示されている箇所をクリックし、以下のスクリプトを入力する。

```
$SRCROOT/＜プロジェクト名＞/tools/HexToClr $SRCROOT/＜プロジェクト名＞/default.colors
```

`Run Script`の表記をダブルクリックして、`Color Converter`と変換しておく。

追加で[+]を選択し、[New Run Script]を選択。
以下のスクリプトを指定する。

```
$SRCROOT/Pods/R.swift/rswift $SRCROOT/＜プロジェクト名＞
```

`Run Script`の表記をダブルクリックして、`Build R.swift`に変えておく。

文字列と色を定義するファイルを用意しておく。
Terminalから以下のコマンドを実行する。

```
$ touch path/to/＜プロジェクト名＞/＜プロジェクト名＞/default.colors
```

ビルドするとR.generated.swiftが生成される。ただし、Xcodeのサイドメニューには現れないので、このファイルを置きたい階層で右クリックをして[Add Files to "＜プロジェクト名＞"]を選択。

ファイル選択画面が現れるので、生成された```＜プロジェクト名＞/＜プロジェクト名＞/R.generated.swift```を選択。

`default.clr`も生成されるので同様にプロジェクトに追加しておく。

以上で準備が完了です。

# CocoaPodsのパッケージ化
シリーズアプリでアプリごとに異なる文字列や色、モジュール等を管理するためにCocoaPodsのパッケージ化をする。
文字列や色以外にもモジュールとして別途クラスを定義したファイルをライブラリに含めることも可能です。

## CocoaPodsの自作ライブラリの作成
Xcodeで[File]から[New]->[Project]を選択。
Cocoa Touch Frameworkを選択した状態で[Next]を選択。
Product Name: PackageLibSample1として[Next]->[Create]を選択。

注意点として、異なるアプリでも共通に`PackageLib.podspec`というように`PackageLib`を作成し、アプリごとに異なるプロジェクト名(ここでは`PackageLibSample1`)は、GitHubでのプロジェクト作成時の名前指定またはレポジトリのURLで使用します。

```
$ pod init
$ pod spec create PackageLib
```

`PackageLib.podspec`が生成されるのでこちらを編集します。

```
vim PackageLib.podspec
```

```
#
#  Be sure to run `pod spec lint PackageLib.podspec' to ensure this is a
#  valid spec and to remove all comments including this before submitting the spec.
#
#  To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html
#  To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/
#

Pod::Spec.new do |s| 

  # ―――  Spec Metadata  ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
  #
  #  These will help people to find your library, and whilst it
  #  can feel like a chore to fill in it's definitely to your advantage. The
  #  summary should be tweet-length, and the description more in depth.
  #

  s.name         = "PackageLib"
  s.version      = "0.0.1"
  s.summary      = "A short description of PackageLib."

  # This description is used to generate tags and improve search results.
  #   * Think: What does it do? Why did you write it? What is the focus?
  #   * Try to keep it short, snappy and to the point.
  #   * Write the description between the DESC delimiters below.
  #   * Finally, don't worry about the indent, CocoaPods strips it!
  s.description  = <<-DESC
                   DESC

  s.homepage     = "https://github.com/＜GitHubユーザ名＞/PackageLibSample1.git"
  # s.screenshots  = "www.example.com/screenshots_1.gif", "www.example.com/screenshots_2.gif"


  # ―――  Spec License  ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
  #
  #  Licensing your code is important. See http://choosealicense.com for more info.
  #  CocoaPods will detect a license file if there is a named LICENSE*
  #  Popular ones are 'MIT', 'BSD' and 'Apache License, Version 2.0'.
  #

  # s.license      = "MIT (example)"
  # s.license      = { :type => "MIT", :file => "FILE_LICENSE" }


  # ――― Author Metadata  ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
  #
  #  Specify the authors of the library, with email addresses. Email addresses
  #  of the authors are extracted from the SCM log. E.g. $ git log. CocoaPods also
  #  accepts just a name if you'd rather not provide an email address.
  #
  #  Specify a social_media_url where others can refer to, for example a twitter
  #  profile URL.
  #

  s.author             = { "xxxxx" => "xxxxxxxxxx@gmail.com" }
  # Or just: s.author    = "xxxxx"
  # s.authors            = { "xxxxx" => "xxxxxxxxxx@gmail.com" }
  # s.social_media_url   = "http://twitter.com/xxxxx"

  # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
  #
  #  If this Pod runs only on iOS or OS X, then specify the platform and
  #  the deployment target. You can optionally include the target after the platform.
  #

  # s.platform     = :ios
  s.platform     = :ios, "8.0"

  #  When using multiple platforms
  # s.ios.deployment_target = "5.0"
  # s.osx.deployment_target = "10.7"
  # s.watchos.deployment_target = "2.0"
  # s.tvos.deployment_target = "9.0"


  # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
  #
  #  Specify the location from where the source should be retrieved.
  #  Supports git, hg, bzr, svn and HTTP.
  #

  s.source       = { :git => "https://github.com/＜GitHubユーザ名＞/PackageLibSample1.git", :tag => "#{s.version}" }


  # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
  #
  #  CocoaPods is smart about how it includes source code. For source files
  #  giving a folder will include any swift, h, m, mm, c & cpp files.
  #  For header files it will include any header in the folder.
  #  Not including the public_header_files will make all headers public.
  #

  s.source_files  = "Classes", "PackageLib/**/*.{h,m,swift,strings}"
  s.exclude_files = "Classes/Exclude"

  # s.public_header_files = "Classes/**/*.h"


  # ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
  #
  #  A list of resources included with the Pod. These are copied into the
  #  target bundle with a build phase script. Anything else will be cleaned.
  #  You can preserve files from being cleaned, please don't preserve
  #  non-essential files like tests, examples and documentation.
  #

  # s.resource  = "icon.png"
  # s.resources = "Resources/*.png"

  # s.preserve_paths = "FilesToSave", "MoreFilesToSave"


  # ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
  #
  #  Link your library with frameworks, or libraries. Libraries do not include
  #  the lib prefix of their name.
  #

  # s.framework  = "SomeFramework"
  # s.frameworks = "SomeFramework", "AnotherFramework"

  # s.library   = "iconv"
  # s.libraries = "iconv", "xml2"


  # ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
  #
  #  If your library depends on compiler flags you can set them in the xcconfig hash
  #  where they will only apply to your library. If you depend on other Podspecs
  #  you can include multiple dependencies to ensure it works.

  # s.requires_arc = true

  # s.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" }
  s.dependency "R.swift", "~> 3.2.0"

end
```

### 注意点
- s.descriptionをs.summaryより長くしないと注告。
- s.homepageとs.sourceのURLを先ほどpushしたgithubのURLに変更する。
- s.licenseは個人用に自作したものなのでコメントアウトしておく。
- s.platformは```:ios, "5.0"```となっている箇所のコメントアウトを外して、```:ios, "8.0"```に修正する。
- s.source_filesは、```s.source_files  = "Classes", "Classes/**/*.{h,m}"```となっているところを```s.source_files  = "Classes", "PackageLib/**/*.{h,m,swift,strings}"```のようにパスの変更と拡張子にswiftを追加する。
- s.dependencyに`R.swift`の依存を追加する。これをしないと本体からライブラリ呼び出し時にR.swiftを認識できない。


podspecファイルの検査をするにあたってGitHubで`PackageLibSample1`というプロジェクトを作成しておきます。


```
$ pod lib lint PackageLib.podspec --allow-warnings
```

WARNはいくつか出るが、`PackageLib passed validation.`と表示されればOK.

ライブラリのプロジェクトにR.swiftを追加する。Podfileに`pod 'R.swift', '~>3.2.0'`を追加し、`pod install`する。

## ビルドの設定

[ビルド時のR.swiftの適用](http://qiita.com/hayashier/items/0d4fd0b59103f3f3680d#%E3%83%93%E3%83%AB%E3%83%89%E6%99%82%E3%81%AErswift%E3%81%AE%E9%81%A9%E7%94%A8)と同様にBuild Phasesの設定を行う。
`Build R.swift`でのスクリプトは`--accessLevel public`を追加して、以下のようにしないとライブラリ中のリソースを認識できない。

```
$PODS_ROOT/R.swift/rswift --accessLevel public $SRCROOT/＜プロジェクト名＞
```

先程の本体の`Color Converter`と`Build R.swift`は消去しておく。

# アプリごとに異なる部分の切り出し
## 切り出し作業
アプリの切り替えはTargetで行う。

引き続き、CocoaPodsの自作ライブラリのプロジェクトで作業をする。
先程共通部分のアプリで作成したstringsファイルやcolorsファイルを新しく作成したCocoaPodsの自作ライブラリのプロジェクトに移動して追加しておく。
リソースファイルの追加後ビルドを行い全てgit管理下におく。clrファイルやR.generated.swiftファイルもgit管理下にするので注意。
ビルドが通ることを確認しておく。確認ができたら、自作ライブラリのレポジトリをGitHubにpushする。

```
$ git init .
$ git remote add origin https://github.com/＜GitHubユーザ名＞/PackageLibSample1.git
$ git add ./
$ git commit -m "add podspec file"
$ git push origin master 
$ git tag 0.0.1
$ git push --tag
```

次に共通部分のプロジェクトで作業をする。

`Podfile`に以下を追加する。

```
pod 'PackageLib', :git => 'https://github.com/xxxxx/PackageLibSample1.git'
``` 

以下のコマンドで`PackageLib`をインストールする。
ただし、ライブラリのレポジトリはプライベートレポジトリでも良いが取り組みには、[Settings]の[Collatorators]からメンバーで追加されている必要があるので注意。

```
$ pod install
```

<!--
GitHubに古いタグが残っているとバージョン指定してもそちらが優先されるので、古いタグは以下のようにする

```
git push origin :<Old version>
```
-->


ここで、`import PackageLib`のように`PackageLibSample1`のモジュールを読み込んで、`PackageLib.R.string.sample.sampleTest1()`のように入力すると予測変換で候補が出るなど読み込まれることを確認しておきます。

## Podfile中のTarget毎の定義
アプリごとのTargetの作成を行う。元のプロジェクト名を`sample_app`とします。
Targets中のアプリ本体(`sample_app`)を右クリックして[duplicate]を選択。
名前を変更して、`sample_app2`とします。

### Podfileの設定
次にPodfile中におけるTarget毎に読み込むパッケージの定義を行います。
共通に読み込むライブラリは関数を定義しておき、そこに定義します。
Target毎のパッケージは先程定義した関数の宣言に加えて独自に読み込むパッケージを宣言します。

```
def shared_module
    pod 'xxxxxxxxxxxxxxxxx', '~>xxx.xxx.xxx'
    pod 'xxxxxxxxxxxxxxxxx', '~>xxx.xxx.xxx'
    pod 'xxxxxxxxxxxxxxxxx', '~>xxx.xxx.xxx'
end

target "sample_app" do
    shared_module
    pod 'PackageSampleLib1',:git => 'https://github.com/xxxxx/PackageSampleLib1.git', :tag => '0.0.1'
end

target "sample_app2" do
    shared_module
    #pod 'PackageLibSample2',:git => 'https://github.com/xxxxx/PackageLibSample2.git', :tag => '0.0.1'
end
```

### Targets毎の定数の設定

Targets毎に定数の設定をします。
[Build Settings]の`Swift Compiler - Custom Flags`の`Other Swift Flags`の`Debug`と`Release`に例えば、`PackageSampleLib1`というTargetであれば、`-D PackageSampleLib1`のように追加します。

### ビルド時に読み込むファイルの切り替えの設定
AppTargets.swiftのようなファイルを作成し、以下のように定義します。

```swift
#if PackageSampleLib1
    import PackageSampleLib1
    let COLOR = PackageSampleLib1.R.color.default.self
    let STRING = PackageSampleLib1.R.string.self
    let SUtil = PackageSampleLib1.R.string.utility.self
#else
    import PackageSampleLib2
    let COLOR = PackageSampleLib2.R.color.default.self
    let STRING = PackageSampleLib2.R.string.self
    let SUtil = PackageSampleLib12.R.string.utility.self
#endif
```

## ビルド
Target毎のビルドはXcode画面上部のStopボタンとDestinationの指定するところの間にTargetを選択できるのでそちらを選択した状態でビルドします。

<img width="1277" alt="スクリーンショット 2017-02-07 14.03.22.png" src="https://qiita-image-store.s3.amazonaws.com/0/81083/31de9447-1541-68c7-86d6-0a5b13cb9d5b.png">

# 参考
[CocoaPodsによる、外部ライブラリの利用と作成](http://dev.classmethod.jp/smartphone/iphone/cocoapods/)
[CocoaPods Private Specリポジトリの作成と利用方法](http://qiita.com/makoto_kw/items/701b1060a6809fd1bb60)
[【iOS Apple Watch,watchOSプログラミング入門 #009】CocoaPodsで複数のターゲットで共通するライブラリがある場合のPodfileの定義方法](http://swift.swift-studying.com/entry/2015/12/20/161918)
[Swift Package Managerの使い方](http://qiita.com/_tid_/items/c26fcd1b95eaed2dedb2)
[R.swiftで色を管理する](http://qiita.com/noppefoxwolf/items/5132d173b7d82579932a)