xcode-install を利用した Xcode のバージョン管理をやってみた

Xcode のバージョン管理するが面倒くさい!そう思ったことはありませんか?

App Store から DL しても途中で失敗したりなかなかうまいこといきません。 仕方がないので Developer Center から直接 DL してきます。 そして古い Xcode.app をリネームして残しておき、新しいのと入れ替える。

そう、めんどくさいんです。。。

そんな時に neonichu/xcode-install と出会いました。 作者は try! Swift でも登壇されていた Boris Bügling さんです。

これは rbenv のような **env 系と似たような interface を持つ CLIXcode のバージョンを管理するのに特化しています。

これがとても便利だったので紹介します。

xcode-install の使い方

Installation

gem install xcode-install

Xcode を DL する際に Developer Center にログインする必要があります。

毎回ログイン情報を入力するのもめんどくさいので、XCODE_INSTALL_USER, XCODE_INSTALL_PASSWORD環境変数を指定しておくと楽になります。

password はいやだって人は ID だけでも設定しておくと、一度パスワードを入力すると次回からは keychain から取得してくれるのでそれでもいいかなと思います。

ここの部分は fastlane/credentials_manager を使っているようです

Usage

$ xcversion update
$ xcversion install 7.3.1

仕組みとしては簡単で Xcode_x.x.x.dmg を cache ディレクトリに保存し、それを /Application/Xcode-x.x.x.app にインストール後、xcode-select を実行してバージョンを切り替えてくれます

以下、ざっと使うコマンドを紹介していきます

インストール可能なバージョン一覧を表示

$ xcversion list
7
7.0.1
7.1
7.1.1
7.2
7.2.1 (installed)
7.3 (installed)
7.3.1 (installed)

--all をつけると、さらに古いバージョンも表示されます

インストール可能なバージョン一覧を更新

$ xcversion update

インストール済みのバージョン一覧を表示

$ xcversion installed
6.4     (/Applications/Xcode-6.4.app)
7.2.1   (/Applications/Xcode-7.2.1.app)
7.3     (/Applications/Xcode-7.3.app) 
7.3.1   (/Applications/Xcode-7.3.1.app)

DVTPlugInCompatibilityUUID を一覧で表示するオプションを追加した PR がマージされたので 1.3.0 の次のバージョンから使えるようになるでしょう。 Xcode Plugin を作ってる(使ってる)人には大助かりですね。

はやく使いたい方は master からインストールしてもらえれば使えます

$ xcversion installed --uuid
6.4     (/Applications/Xcode-6.4.app)     7FDF5C7A-131F-4ABB-9EDC-8C5F8F0B8A90
7.2.1   (/Applications/Xcode-7.2.1.app)   F41BD31E-2683-44B8-AE7F-5F09E919790E
7.3     (/Applications/Xcode-7.3.app)     ACA8656B-FEA8-4B6D-8E4A-93F4C95C362C
7.3.1   (/Applications/Xcode-7.3.1.app)   ACA8656B-FEA8-4B6D-8E4A-93F4C95C362C

利用する Xcode を変更

$ xcversion select 7.3.1

Simulator の install 状況を確認

$ xcversion simulators
Xcode 7.3.1 (/Applications/Xcode-7.3.1.app)
iOS 8.1 Simulator (installed)
iOS 8.2 Simulator (installed)
iOS 8.3 Simulator (installed)
iOS 8.4 Simulator (installed)
iOS 9.0 Simulator (installed)
iOS 9.1 Simulator (not installed)
iOS 9.2 Simulator (installed)
tvOS 9.0 Simulator (installed)
tvOS 9.1 Simulator (installed)
watchOS 2.0 Simulator (installed)
watchOS 2.1 Simulator (installed)

Simulator を install

$ xcversion simulators --install='iOS 9.1 Simulator'

Xcode_x.x.x.dmg を削除

一度 DL した Xcode_x.x.x.dmg は保存されたままになります。 ディスク容量が気になる方は cleanup を実行して削除してください

$ xcversion cleanup

はまりどこ

Developer Center にログインすると利用規約への同意画面が表示されるような状態のアカウントを使うと Xcode の DL に失敗(タイムアウト)します。 普段使っていないアカウントを使う場合は一度ログインして確認しておきましょう。

まとめ

xcode-install を使うことによって Xcode のアップデートが以下のような手順になります。

$ xcversion update             // 一覧更新
$ xcversion list               // 入れたいバージョンを確認
$ xcversion install x.x.x      // インストール

これだけで最新の Xcode が使えるようになり、バージョン管理も行ってくれます。 これで Xcode のヘッダーの diff をとってニヤニヤする作業もしやすくなりますね。

UIWebView で input[type=file] が正常に動かない対応について -決定版-

基本的なことは「iOSアプリのwebviewでinput[type=file]が正常に動作しない時」見てもらえれば分かるかなと思います。

簡単に説明すると画像を選択後 Image Picker を閉じようとした際に WebView をモーダルで表示していた場合、モーダルごと閉じられてしまうということです。

どういう view 構造にするかにもよりますがこのような構造でモーダルを表示していたとします。

[modal の view 構造]
UINavigationController
  └── UIViewController
      └── UIWebView

この場合 UINavigationController に対して解決方法を適応してやればいいわけです。

extension UINavigationController {

    override public func dismissViewControllerAnimated(flag: Bool, completion: (() -> Void)?) {
        if presentedViewController != nil {
            super.dismissViewControllerAnimated(flag, completion: completion)
        }
    }
    
}

ただしこれだと問題があります。

それは UIActivityViewController を使って新たに ActivityItem を追加しようとした時です。 追加しようとしたモーダルが閉じれなくなってしまうんですね。。。

f:id:dealforest:20160330013354p:plain

というわけで、その状況にも対応しようとするとこのようになります

extension UINavigationController {

    override public func dismissViewControllerAnimated(flag: Bool, completion: (() -> Void)?) {
        if presentedViewController != nil || presentingViewController is UIActivityViewController {
            super.dismissViewControllerAnimated(flag, completion: completion)
        }
    }
    
}

HTML の方が楽な場合(複雑な投稿画面等)とかもあるのでそういう時では使っていきたいんですが、なかなか一筋縄ではいきませんね。 WKWebView では同じ現象が起きるかどうかは知りませんが起きないことを祈ります。

DerivedData を簡単に消せる Xcode Plugin

懇親会における英会話のプロトコル」を見て try! Swift に行くまでに何か簡単に話せる小ネタがあればなと思って作りました。

Swift を書いているとよくあるのですが、頻繁に中間データが壊れてしまいます。 そのため DerivedData を消すとなおったりします。

消す方法は「Window」->「Project」 を開いて該当するプロジェクトの DerivedData を削除するといったことをしないといけません。 めんどくさいですね。

Clean を実行した時に自動で消してくれたらいいのにと思っていました。

と、いうわけで作りました。

dealforest/Cichlid(シクリッド)

f:id:dealforest:20160301011702p:plain

由来は掃除魚(ベラ・ハゼ・シクリッド・ナマズなど)のベラを採用しました。 Swift で作った Xcode Plugin でコード量も少ないので見れば簡単にわかるかなと思います。

ざっと作った感じですが、意外に便利なのでぜひ使ってみてください。 yidev 第22回勉強会 でこの辺りの話をしようかなと思います。

それでは try! Swift で会いましょう。

isRegisteredForRemoteNotifications が正常に動かなかったときの対処

PUSH 通知が有効/無効をアプリ上で表示(UISwitch等)して有効にしようとした場合にアプリの設定画面に遷移して有効にしてもらう。 といったことを実装することがあるかもしれません。

実装は「[Swift] iOS でプッシュ通知の有効・無効を判定する」を見てもらえれば分かりやすいと思います。

それが意図した通りに動かないケースに遭遇しました。

isRegisteredForRemoteNotifications で有効/無効を確認するのですが、PUSH 通知を許可していないのに true になったり、再インストールした時にダイアログが出ている状態なのに true になったりなどです。

解決方法

registerForRemoteNotifications を呼び出しているしている場所を確認してください。

ドキュメント を見ると application:didRegisterForRemoteNotificationsWithDeviceToken:application:didFailToRegisterForRemoteNotificationsWithError: 以外からの呼び出しは推奨していないようです。

今回はこれが原因でした。 なので 上記の delegate の中から呼び出すようにすればなおりました。

呼び出した場合どうなるか

解決方法で書いたようにちゃんと delegate の中で registerForRemoteNotifications を実行していればこんなことにはならないのですが、何かの役に立つかもしれないので検証した結果を書いておきます。

iOS9.2.1 で下記のコードで検証しました。

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    ...
    
    let types: UIUserNotificationType = [ .Badge, .Sound, .Alert ]
    let settings = UIUserNotificationSettings(forTypes: types, categories: nil)
    application.registerUserNotificationSettings(settings)
    print("before \(application. isRegisteredForRemoteNotifications)")
    application.registerForRemoteNotifications()
    print("after \(application. isRegisteredForRemoteNotifications)")
    
    ...
}

検証した限り初回インストールと、再インストールの場合アンインストールした時の通知設定の状況に左右されます。

初回インストール

isRegisteredForRemoteNotifications が正常な値を返す

再インストール

前回のインストールの状況時の PUSH 通知の許可状況に依存します。

アンインストール時に通知を不許可

isRegisteredForRemoteNotifications が正常な値を返す

isRegisteredForRemoteNotifications
before false
after false

アンインストール時に通知を許可

この時が曲者で、初回起動時のみ挙動が変わります。

registerForRemoteNotifications を呼び出したら isRegisteredForRemoteNotificationstrue になります。 そしてダイアログに出ているのに関わらず application:didRegisterForRemoteNotificationsWithDeviceToken: が呼び出され device token が取得できてしまいます。。。

isRegisteredForRemoteNotifications
before false
after true

次回起動時からは通知ダイアログの選択に依存し正常に動作します。

これのタチの悪い所はアンインストール前の通知の有効/無効に依存することです。 状況が不安定になってしまってデバッグ時に困るので delegate の中で registerForRemoteNotifications を呼び出すようしましょう。 (ドキュメントにはそう書いてありますが。。。)

あまり関係ありませんが、いつの間にかアプリを入れなおすと通知のダイアログが毎回出るようになったんですね。 以前は端末の時計を進めて再起動したりしないといけなかったのでお手軽になりましたね。

移転しました

2015年はフリーランスになり色々とあり、あまりブログや発表などでアウトプットができていなかったなと。

諸事情があり前のブログにログインできなくなったこともあり、2016年からは心機一転、移転してやり直すことにしました。(特に深い意味はないです)

SwiftOSS になったのでその辺りや LLDB 周りも追っていこうかなと思っています

といわけで本年もよろしくお願いします。

前のブログ: http://blog.dealforest.net/