酢ろぐ!

カレーが嫌いなスマートフォンアプリプログラマのブログ。

App Extension(Widget)を含んだiOSアプリをCocoaPods 1.0にバージョンアップさせる

記事自体は2ヶ月前に書いていたのですがちょうどiTunes Connectがトラブっていた時期だったので公開できずじまいでした。そのため一部記載しているバージョンが古い部分があります。


大半のプロジェクトでCocoaPods 1.0.0へ移行し終えてノウハウも溜まってきました。

ビルドサーバーを含め環境要因のトラブルが多く、CocoaPods 1.0.0への移行は一筋縄でいきませんでした。原因が環境要因なので一度わかってしまえば対策は簡単なのですが……

本記事では、CocoaPods 0.39.0で使用していたPodfileをCocoaPods 1.0.0で使えるようにどのようにマイグレーションさせたかを紹介したいと思います。

マイグレーション対象のアプリについて

仕事で関わっているプロジェクトで使用しているPodfileを晒すのは問題がありそうなので、「ikatomo」のケースを例にとって説明していきます。

https://itunes.apple.com/jp/app/ikatomo-for-splatoon/id1066729147?mt=8&uo=4&at=10l8JW&ct=hatenablog

ikatomoの売りのひとつは通知センターに表示されるステージ情報とフレンドのオンライン状態を閲覧できるWidget(App Extension)です。

ひょっとしたら僕のPodfileの書き方が悪いだけなのかもしれませんが…と前書きしておきますが、App Extensionを含んだプロジェクトの場合CocoaPodsとは少し相性がよくありません

解決方法については後ほど紹介します。

CocoaPods 0.39.0までのPodfile

CocoaPods 0.39.0までのPodfileです。

App ExtensionとCocoaPodsとの相性の悪さに四苦八苦していた時期に書いてそのままにしています。若干複雑になってしまっているところがあるかもしれません。

platform :ios, '9.0'
use_frameworks!

link_with 'TodayExtension'

pod 'RealmSwift'
pod 'SDWebImage'
pod 'Alamofire', '~> 3.0'
pod 'SwiftyJSON'
pod 'BrightFutures'

target :ikatomo do
    pod 'MaterialControls', '~> 1.0.2'
    pod 'BrightFutures'
    pod 'KeychainAccess'
    pod 'UIDeviceIdentifier', :git => 'https://github.com/squarefrog/UIDeviceIdentifier.git'
    pod 'MRProgress'
    pod 'Ji'
    pod 'SVGKit', :git => 'https://github.com/SVGKit/SVGKit.git'
end

post_install do |installer|
  installer.pods_project.targets.each do |target|

    target.build_configurations.each do |config|
      config.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = ""
      config.build_settings['CODE_SIGNING_REQUIRED'] = "NO"
      config.build_settings['CODE_SIGNING_ALLOWED'] = "NO"
    end

  end
end

さて、このPodfileをv1.0.0向けに書き換えてみましょう。

CocoaPods 1.0.0に対応したPodfile

CocoaPods 1.0.0用に書き換えたPodfileです。前述のCocoaPods 0.39.0で使っていたPodfileと比較して行数は増えましたが見やすくなりましたね。

platform :ios, '9.0'
use_frameworks!

def shared_pods
    pod 'RealmSwift'
    pod 'SDWebImage'
    pod 'Alamofire', '~> 3.0'
    pod 'SwiftyJSON'
    pod 'BrightFutures'
end

target "ikatomo" do
    shared_pods

    pod 'MaterialControls', '~> 1.0.2'
    pod 'BrightFutures'
    pod 'KeychainAccess'
    pod 'UIDeviceIdentifier', :git => 'https://github.com/squarefrog/UIDeviceIdentifier.git'
    pod 'MRProgress'
    pod 'Ji'
    pod 'SVGKit', :git => 'https://github.com/SVGKit/SVGKit.git'
end

target "TodayExtension" do
  shared_pods
end

target "FriendsWidget" do
  shared_pods
end

post_install do |installer|
  installer.pods_project.targets.each do |target|

    target.build_configurations.each do |config|
      config.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = ""
      config.build_settings['CODE_SIGNING_REQUIRED'] = "NO"
      config.build_settings['CODE_SIGNING_ALLOWED'] = "NO"
    end

  end
end

App Extensionを含んだプロジェクトの場合CocoaPodsを使うと自動リジェクト対象になるので対策する

CocoaPods 1.0.0を使ってもITMS-90205、ITMS-90206の指摘がありました。pod installや実機デバッグ時には問題になりませんが、iTunes Connectにバイナリを送信する段階で問題が発生してしまいます。CocoaPods 0.38.0、0.39.0だけでなく、CocoaPods 1.0.0でも同様の問題が発生します。

これはApp Extensionパッケージ内に余計なものが含まれているため、アップロード時に機械的にリジェクトされてしまう問題です。詳しくは下記の記事をご参照ください。

App Extensionのパッケージ(*.appex)の中にFrameworksというディレクトリがあって、このFrameworksディレクトリが存在しているのでリジェクトされてしまうのです。

不必要に追加されるEmbed Pods Frameworksを手動で消さないとビルドはできてもiTunes Connectに送信すると、前述の通り「ITMS-90205」「ITMS-90206」を理由に機械的に自動リジェクトされてしまいます。

今までは手動で対策をしていました。さすがにpod installpod updateする度に、手動でプロジェクト設定を弄らないと自動リジェクトされてしまうのがとても面倒なので、この問題に関してもきっちり解決しようと思います。

ikatomoではステージ情報Widget(TodayExtension)とフレンド情報Widget(FriendsWidget)を含んでいるので、下記のようにアプリ側のBuild PhasesでRun Scriptを追加します。

f:id:ch3cooh393:20160603094446p:plain

画像だとコピペしにくいのでスクリプト部分を抽出しておきます。ご使用の際には***.appexの部分を適切なアプリパッケージ名に修正してください。

EMBEDDED_EXTENSION_PLUGIN_PATH="$TARGET_BUILD_DIR/$PLUGINS_FOLDER_PATH/TodayExtension.appex/Frameworks"
if [[ -d "$EMBEDDED_EXTENSION_PLUGIN_PATH" ]]; then
rm -fr "$EMBEDDED_EXTENSION_PLUGIN_PATH"
fi

EMBEDDED_EXTENSION_PLUGIN_PATH="$TARGET_BUILD_DIR/$PLUGINS_FOLDER_PATH/FriendsWidget.appex/Frameworks"
if [[ -d "$EMBEDDED_EXTENSION_PLUGIN_PATH" ]]; then
rm -fr "$EMBEDDED_EXTENSION_PLUGIN_PATH"
fi

以上です。

iTunes ConnectにバイナリをアップロードするとITMS-90080のエラーが返ってくるようになった

CocoaPodsをバージョン1.0.0に上げたのが原因でしょうか、App extensionを含むバイナリなのが問題なのでしょうか。

今日の夕方あたりからiTunes Connectにバイナリをアップロードするとワーニングが返ってくるようになりました。

お昼頃にはバイナリをアップロードしても何も起こりませんでした。少し時間をおいて夕方頃にアプリのバージョンだけを変えたバイナリをアップロードすると下図のようにITMS-90080のワーニングが出ました。2016/6/5 1:10になっても同じようにワーニングが出ていました。

f:id:ch3cooh393:20160604001325p:plain

小一時間後、TestFlightでテストができるようになったと通知が来ました。ここまではいつも通りです、実際にデバイスにインストールしてテストをしていました。

しばらくするとAppleからワーニングメールが届いた

さらに時間をおいてAppleから下記のようなメールが届きました。

Dear developer,

We have discovered one or more issues with your recent delivery for "SplatFriends for Splatoon". Your delivery was successful, but you may wish to correct the following issues in your next delivery:

Non-PIE Binary - The executable 'Payload/ikatomo.app/Frameworks/Alamofire.framework' is not a Position Independent Executable. Please ensure that your build settings are configured to create PIE executables. For more information, refer to Technical Q&A QA1788 - Building a Position Independent Executable

前述したようにTestFlightで問題なく受理されて、テスターへ配布できるようになっています。ワーニングだから致命的ではないのかもしれません。何が問題なのかよくわかりません……

同じような現象が発生している方が少なからずいるようで、CocoaPodsのIssuesが賑わっていました。Appleのサーバーのバグだろうとのことで該当Issueはクローズされています。

github.com

stackoverflow.com

stackoverflowにも「Appleのサーバーサイドで問題が発生しているのではなかろうか」と回答が載っていました。

サーバーサイドの問題であれば週明け1,2日もすれば解決すると思います。ただ、ワーニングが出ている状態でアプリアップデートの申請を出すのは怖いのでしばらく様子見しようと思います。

(追記)申請を出したら普通に通りました。

CocoaPods経由でRealm 0.98.3をインストールすると'impl/async_query.hpp'が見つからないとエラーが出てしまう

今までとは別のプロジェクトにアサインされたので、いつものように開発環境を整えようとpod installをしてRealmをインストールしようとしたところ、謎のエラーが出てビルドができなくなりました。

謎のエラーとは'impl/async_query.hpp' file not find.というコンパイルエラーです。

実際にPodsディレクトリのソースファイルをみてみると*.hppがありませんでした。

さすがにRealmチームがビルドの通らないモノにバージョンをつけて配布したりしないだろうと思い検索したところ同じ現象が発生している方を何人か見つけました。

解決方法

解決したので解決方法を上の方に書いておきます。

直接的な原因はわかりませんがここ最近CocoaPodsの調子が悪くインストールに物凄く遅かったので、CocoaPods内部のキャッシュが異常な状態になっていたのではないかと思います。

CocoaPods自体のキャッシュを削除して、プロジェクト直下のPodsディレクトリを削除して、再度pod installをおこないました。

$ rm -rf ~/Library/Caches/CocoaPods/
$ rm -rf Pods
$ pod install

僕の環境では、この対応でRealmがエラーを出すことがなくなりました。

以下、エラーが出ている間に試したことを順番に書いています(あまり有益な情報はありません)。

経緯

Realmでコンパイルエラーが発生した時点でのPodfile.lockは下記のようになっていました。

PODS:
  - Realm (0.98.3):
    - Realm/Headers (= 0.98.3)
  - Realm/Headers (0.98.3)
  - RealmSwift (0.98.3):
    - Realm (= 0.98.3)
  - Result (1.0.2)

SPEC CHECKSUMS:
  Realm: c6a7d00221a2d5177810a674aebd40c5e0b70b0f
  RealmSwift: d6ecad6e4c2a645d6ac956c2c7e9efb375f39c31
  Result: dd3dd71af3fa2e262f1a999e14fba2c25ec14f16

こんなツイートが話題になっていました。

そういえば前回pod installした時にものすごく時間が掛かったので影響を受けているような気がします。念のため、CocoaPodsのキャッシュを削除しました。

$ rm -rf ~/Library/Caches/CocoaPods/
$ rm -rf Pods

この状態でログ出力させてpod install --verboseをしてみました。

-> Installing Realm (0.98.3)
 > Git download

(中略)

 > Running prepare command
   $ /bin/bash -c  set -e sh build.sh cocoapods-setup
   core is not a symlink. Deleting...
   Downloading dependency: core 0.96.2

ここまで出て15分くらい止まったままになっています。

(追記)そこから10分くらい待っていたらインストールが終わりました。普通にビルドができるようになりました。

Jenkinsを使ってToday Extension(Widget)を含むiOSアプリをビルドする

過去にxcodebuildを使ってビルドを自動化する方法を紹介しました。

本記事ではToday Extension(Widget)を含むiOSアプリをJenkinsでビルドする方法を紹介します。アプリ側とToday Extension側とで使用するプロビジョニングプロファイルが異なります。

ビルド時に適切なプロビジョニングプロファイルのIDを指定する必要があります。.xcodeprojファイルを編集して、コマンドラインからプロビジョニングプロファイルのIDを指定できるようにしましょう。

アプリ側に$(APP_PROFILE)を指定します。

App Extension側には$(EXTENSION_PROFILE)を指定します。

あとはJenkinsで下記のように設定します。

xcodebuild -sdk iphoneos \
  -workspace ${XCWORKSPACE_NAME} -scheme "${SCHEME_NAME}" \
  -configuration Release build \
 CODE_SIGN_IDENTITY=${CODE_SIGN_ID} \
 APP_PROFILE=${PROVISIONING_ID} \
 EXTENSION_PROFILE=${EXTENSION_PROVISIONING_ID}

xcrun -sdk iphoneos PackageApplication \
 "${SOURCE_PATH}" -o ${APP_PATH} \
  --embed ~/Library/MobileDevice/Provisioning\ Profiles/${PROVISIONING_FILENAME}

関連記事

SwiftでUITextFieldの文字入力を検出する

iOSでは文字入力用のコンポーネントとしてUITextFieldを利用します。文字入力中……1文字入力ごとにバリデーションしたい時があります。たとえば、

  • ログイン画面でユーザーIDとパスワード欄が両方入力された場合にログインボタンを有効にする
  • ユーザー登録中にメールアドレスのフォーマットが正しいのか検査してSubmitボタンを有効にする
  • 検索欄で検索単語のサジェストをおこないたい

などです。

NSNotificationCenterを使って、UITextFieldTextDidChangeNotificationの通知を受け取ることで文字入力したことを検出することができます。

class LoginViewController : UITableViewController {

    @IBOutlet weak var passwordTextField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()

        NSNotificationCenter.defaultCenter().addObserver(self, selector: "textDidChange:", name: UITextFieldTextDidChangeNotification, object: self.passwordTextField)
    }

    func textDidChange(notification: NSNotification) {
        // パスワードが正しく入力されたか調べる
    }
}

上記の場合、文字入力が完了する度に textDidChange が呼ばれます。

関連記事

【2015/8/31まで】Windows Phoneアプリ開発者は無料でXamarinのライセンスがもらえるよ!

C#でiOSやAndroidのアプリを開発できる「Xamarin」のライセンスを無料で入手できるキャンペーンをやっているようです。

Xamarinの代理店エクセルソフトの田淵さんのブログによると……

2015/8/17 以前に公開された Windows Phone アプリ個人開発者に無料の Xamarin Business ライセンスが提供されます! Windows Phone 7 アプリはダメよ。とかは書いていないのでワンチャンあるで!!

とのこと。いままでにWindows Phoneアプリを公開したことのある方にワンチャンみたいですよ。応募に関しての要件に関してはブログを参考にしてください。

ytabuchi.hatenablog.com

Xamarinはもともと無料で利用できるのですが、無料で使えるエディションは(個人的には)サイズの制約が厳しいので、ライセンスをゲットしてXamarinの本領発揮を味わってほしいです

おでコンで早期申請していた方にも朗報!

また、Windows Phoneアプリコンテストの「おでコン」に8/17以前に応募している方は対象になると思いますので、Xamarinライセンスをもらっちゃいましょう。

odecon.azurewebsites.net

JenkinsでCocoaPodsを含んだiOSアプリプロジェクトのビルドで署名に失敗してしまう

(開発的な意味で)チームで働くことはほぼなくて1人でiOSアプリを開発していると、1件1件最新ビルドが欲しいという要望に応えるのが難しかったです。……というか、僕は限界だったのでJenkinsを導入してJenkinsおじさんを酷使していました。

それ以降、Xcodeプラグインを使ったり使わなかったりと試行錯誤の末、コマンドライン直叩きスタイルで落ち着きました。僕が使っているコマンドラインの一覧は一通り「Xcodeでのビルドを自動化するxcodebuildコマンドとIPAファイルを作成してiTunes Connect(Testflight)に投げる方法 - 酢ろぐ!」で書きました。

先日から着手した新しい案件で初めてCocoaPodsを使ってみました。CocoaPodsはVisual StudioでいうところのNuGetのようなものです。

今更かもしれませんが昔からのコードをメンテばかりしてきたので、CocoaPodsを導入できずにいたんですよね。導入してみると、これがかなり便利でPodfileに使いたいライブラリをペペーンって書くとインストールしてくれているという優れもので、 なんで今まで使ってなかったんだろうと思うレベルです。

CocoaPodsを使ったプロジェクトは、iPhoneシミュレータ上で実行している時には特に問題なく動いていました。Jenkinsでもアスタリスク(*)なAd-Hocプロビジョニングプロファイルでビルドしている時にもすんなりとビルドできていたため、今回の問題に気付きませんでした。

今日、In-houseなプロビジョニングプロファイルに差し替えたところJenkinsおじさんがアラートを吐き出すという現象が発生してしまいました。証明書のインストールでミスってるのかな?Build Settingsで諸々の設定を間違っているのか?と色々疑って無駄に時間を消耗してしまいました。

どんなエラーがでていたのか?

下記のようなエラーが発生していました。

CocoaPodsが生成したPod側のプロジェクトのBundle IDが'org.cocoapods.*'になっていたためにプロビジョニングプロファイルが持っているプリフィックスと相違しておりビルドに失敗しています。

=== BUILD TARGET Pods-Realm OF 
PROJECT Pods WITH CONFIGURATION Release ===

Check dependencies
Code Sign error: Provisioning profile does not 
match bundle identifier: The provisioning profile 
specified in your build settings 
(“XXXXXXX In-house Distribution”) 
has an AppID of “jp.ch3cooh.*” which does not match 
your bundle identifier “org.cocoapods.Realm”.
CodeSign error: code signing is required 
for product type 'Framework' in SDK 'iOS 8.3'

pod installを実行すると元々のプロジェクト(ここではhoge.xcodeprojとする)とライブラリ群を抱えたPodsプロジェクトを含むxcworkspaceが生成されます。

━hoge.xcworkspace
 ┃
 ┣━hoge.xcodeproj (アプリが入ってる側)
 ┗━Pods.xcodeproj (ライブラリが入ってる側)

Jenkinsのログに残っていたのは下記のようなエラーでした。アプリのBundle Identifierはjp.ch3cooh.hogeで、Pods側のBundle Identifierはorg.cocoapods.Realmになっていました。

Xcodeを使っていると自動的に解決してくれるのですが、コマンドラインからビルドした場合は自動では解決してくれないので2時間ほど悩んでしまいました。

どんなコマンドで失敗したのか?

Jenkinsでgit cloneしてpod installしてから以下のコマンドでビルドしています。

pod install

# schemeがないので一旦Xcodeを開いて初期化する
# てかここを自動化できていないの問題なのでは(
pgrep -f "/Applications/Xcode.app" | xargs kill
open hoge.xcworkspace
sleep 10
pgrep -f "/Applications/Xcode.app" | xargs kill

# ビルド開始する
xcodebuild -sdk iphoneos \
  -workspace hoge.xcworkspace -scheme hoge \
  -configuration Release clean build \
  CODE_SIGN_IDENTITY='iPhone Distribution: XXXXXXXXX.' \
  PROVISIONING_PROFILE='XXXX-XXXXX-XXXXX'

解決したけど強引かも……

プロビジョニングプロファイルのIDと相違があることが署名できない原因なので、Podfileでなんらかのパラメータを指定すればインストール時のBundle Identifierを指定できるのではないか?と考えましたが見つからず。

最終的に「A post install script for CocoaPods that changes the bundle identifier of all pods to the one specified. · GitHub」を参考に、pod installの完了後にPod側のプロジェクトのInfo.plistを書き換えるという手段に出ました。

post_install do |installer|
  
  installer.project.targets.each do |target|
    target.build_configurations.each do |config|
      if config.name == 'BREnterprise'
        config.build_settings['CODE_SIGN_IDENTITY[sdk=iphoneos*]'] = 'iPhone Distribution: XXXXXXXXX.'
        config.build_settings['PROVISIONING_PROFILE'] = 'XXXX-XXXXX-XXXXX'
      end
    end
  end
  
  # change bundle id of each pod to 'com.bottlerocketapps.*'
  bundle_id = 'jp.ch3cooh'

  directory = installer.config.project_pods_root + 'Target Support Files/'
  Dir.foreach(directory) do |path|

    full_path = directory + path
    if File.directory?(full_path)

      info_plist_path = full_path + 'Info.plist'
      if File.exist?(info_plist_path)

        text = File.read(info_plist_path)
        new_contents = text.gsub('org.cocoapods', bundle_id)
        File.open(info_plist_path, "w") {|file| file.puts new_contents }
      end
    end
  end
end

もっと良い手段があれば知りたいです……というかCocoaPodsを使っている人はみんなここでつまづくと思うんだけどどうやって解決してるんだろう。良い方法があれば教えて下さい。

(追記2016/6/1)CocoaPods 1.0を導入後、ビルド時の署名に失敗してしまう現象が再発しました

blog.ch3cooh.jp

関連記事

blog.ch3cooh.jp