iOS/Android アプリ転送ツール TranspoterPad をリリースしました

iOS/Android アプリを簡単かつ複数の端末にインストールできる Mac アプリ、TranspoterPad をリリースしました。

f:id:iseebi:20171010200748p:plain

github.com

iOS/Android アプリの実機転送は公式の TestFlight などや、DeployGate といった OTA 配信の仕組みが使われることが多くなっています。しかし、そのような環境を構築できない場合もあります。そのときは USB をつないでの転送をすることになりますが、この方法だとエンジニア以外にはハードルが高く、デザイナーやマネージャーが新しいビルドを試しづらい環境だと思います。

また、テスターが複数の実機を横に並べてテストする際、そのインストールは大変な手間となってしまいます。

TransporterPad は、そんな非エンジニアの USB 経由アプリインストールを強力にサポートします。

使い方

使い方はとっても簡単。XcodeAndroid Studioのインストールも不要です。

続きを読む

コミックマーケット92にサークル参加します!

サークル 伊勢的新常識はコミックマーケット92に参加します。スペースは1日目 東7ホールそ-33bです。

今回はなんと2冊ご用意しています。

AzureaとAristeaの百合本

ツイッタークライアント百合本です。id:tmyt 開発のTwitterクライアントAzurea,Aristea中心です。おまけでAzureaやAristeaの様々な情報が載っています。

しかも弊サークルとしては初のオフセット本です!

だいたい 1000 円くらいまでの価格設定になる予定です。

伊勢的青魔術読本

いつもの感じのblogには書きづらいネタをまとめた黒魔術読本シリーズ、今回は「青魔術」と称して、某青い航空会社をあれこれしちゃいます。おまけで修行記録なんかもあります。

だいたい 500 円くらいまでの価格設定になる予定です。

サークルページも用意しています

以前から「詳細な告知がないからわかりづらい」というご指摘もありまして、サークルページもご用意しています。

https://c.iseteki.net/ では既刊情報も合わせてご紹介していますのでご参考ください。


1日目 東7そ-33b でお待ちしておりますので、何かのついでにお立ち寄りいただければ幸いです。

Jenkins の認証に Azure Active Directory を使った

昨年の冬に MacBook Pro を買い換えたことで 1 台サーバーとしておいておける Mac ができたので、iOS アプリのビルド用に Jenkins 2 を構築して設置しました。

インターネットに公開するものの、さすがに認証がないとつらい、だけど BASIC 認証は Firefox がうまく覚えてくれなくなってるっぽいので避けたいと思い、別の認証を試すことにしました。

ちょうど、Azure AD の存在を思い出したので、連携させて認証できるようにしてみました。

続きを読む

macOS で USBデバイスの抜き差しを検出する

macOSiOS/Android(ADB) デバイスの接続・切断を検出したくて、この数日ずっと調べていてようやく形にできた。

github.com

備忘録的にどうやっているのかを簡単に書いておこうと思う。

続きを読む

Xamarin.iOS の Swift バインディングライブラリを作る

Xamarin Advent Calendar 2016 17日目です。

以前から僕はネイティブバインディング関連の記事をいろいろ書いてきました。

今回もネイティブバインディングです!

Swift が使われるようになってしばらくたち、CocoaControlsに新しく出てきている今風の UI ライブラリはほとんど Swift 製になりつつあります。 Swift を使う場合、Swift のサポートライブラリ(ランタイム?)が必要となり、Swift で作ったアプリはデバッグ実行やアドホック配布はできるけどストアに上がらない…!というのはネイティブでもよくあります。

Xamarin でのネイティブバインディングObjective-C と異なる手順が必要となります。どのようにすればいいかをご紹介します。

今回は Objective Sharpie で TTSegmentedControl (CocoaControls, GitHub) のバインディングをやってみましょう。

Simulator Screen Shot 2016.12.17 18.27.52.png

Swift と Xcode のバージョンを確認する

使用したいライブラリが決まったら、そのライブラリが対象としている Swift のバージョンを確認しましょう。これにより、この後使用する SwiftSupport の NuGet も変わってきます。

また、異なるバージョンの Swift で書かれたライブラリの混在はできない(ネイティブでもできない)ので、注意しましょう。

Swift バージョン Xcode バージョン iOS SDK のバージョン SwiftSupport の NuGet
Swift 2.2 Xcode 7.3.1 iphoneos9.3 Xamarin.SwiftSupport
Swift 2.3 Xcode 8.x iphoneos10.x Xamarin.Swift23.Support
Swift 3.0 Xcode 8.x iphoneos10.x (現段階で存在せず)

もし、対応する Xcode を持っていない場合、Downloads for Apple Developers というページからダウンロードしましょう。 このページには過去の Xcodeアーカイブされています。

複数の Xcode を環境中に共存することもできます。Xcodeの複数バージョンを共存をご覧ください。

TTSegmentedControl は GitHub の README.md にもあるとおり、Swift 3.0 と 2.x のどちらでも使えますが、2.x を使う場合は 0.1.1 を指定する必要があります。 今回はこれを Swift 2.3 で使うこととし、Xcode 8.2 を使って作業していきます。

Objective Sharpie でネイティブライブラリを作る

使った Objective Sharpie のバージョンは以下の通りです。

$ sharpie -v
3.3.0p 3d8547f 3d8547fcf1f7c2c2337b122c8d1fe893eb6a1ad3 2016-08-03 04:49:53

まず、sharpie pod init でソースコードをダウンロードします。

$ sharpie pod init iphoneos10.2 TTSegmentedControl

プロジェクトが作成され、ライブラリのソースコードがダウンロードされますが、バージョン指定して Swift 2.x で使えるバージョンをダウンロードし直します。 できた Podfile を編集して、0.1.1 指定にします。

platform :ios, '10.2'
install! 'cocoapods', :integrate_targets => false
target 'ObjectiveSharpieIntegration' do
  use_frameworks!
end
pod 'TTSegmentedControl', '0.1.1'

Podfile を編集したら、pod install で反映します。

$ pod install

次に、できた Pods/Pods.xcodeproj を確認します。

目的とするライブラリのターゲットの Enable Bitcode を No にします。(これをしておかないと、ストア申請時に Invalid Bitcode エラーとなります)

スクリーンショット 2016-12-17 13.46.34.png

Xcode 8.x で Swift 2.3 を使うときは Swift Use Legacy Swift Version を明示指定する必要があります。

スクリーンショット 2016-12-17 13.49.39.png

Deployment Target も必要とするバージョンまで下げておきましょう。

スクリーンショット 2016-12-17 13.48.10.png

あとは、sharpie pod bind でビルドします。

$ sharpie pod bind

Mac インストール直後で requires Xcode といわれてしまう場合は、xcode-select コマンドを実行してコマンドラインで使用する Xcode を指定してやりましょう。

$ sudo xcode-select --switch /Applications/Xcode.app

できた Binding フォルダの中にある .framework と .cs が必要になります。

スクリーンショット 2016-12-17 13.59.48.png

が、これだけではシミュレーターで実行できない状態となります。 Objective Sharpie 実行時点では実機用のバイナリしか生成されないため、シミュレーター向けのビルドは別途手動で実行し、実機向けのビルドと合成させる必要があります。

まずは、シミュレーター向けの .framework を作ります。Pods フォルダに移動して、xcodebuild コマンドでビルドしましょう。 最後の codesign の結果で、どのパスに生成されたかがわかります。

$ cd Pods
$ xcodebuild -project Pods.xcodeproj -target TTSegmentedControl -sdk iphonesimulator10.2 -configuration Release build
(中略)
Signing Identity:     "-"

    /usr/bin/codesign --force --sign - --timestamp=none /Users/***/Projects/TTSegmentedControlSample/Native/build/Release-iphonesimulator/TTSegmentedControl/TTSegmentedControl.framework

できたシミュレータ用の Framework のバイナリと、Objective Sharpie が生成したネイティブのライブラリを合成します。 lipo コマンドを使い合成した結果で Objective Sharpie の出力を置き換えます。

  • lipo コマンドの引数
    • lipo -create [合成するバイナリ] [合成するバイナリ] -output [出力先のバイナリ]
    • lipo -info [確認するバイナリ]
$ cd ..
$ ls
Binding     Podfile     Podfile.lock    Pods        build
$ lipo -create Binding/TTSegmentedControl.framework/TTSegmentedControl /Users/***/Projects/TTSegmentedControlSample/Native/build/Release-iphonesimulator/TTSegmentedControl/TTSegmentedControl.framework/TTSegmentedControl -output TTSegmentedControl
$ ls
Binding         Podfile         Podfile.lock        Pods            TTSegmentedControl  build
$ lipo -info TTSegmentedControl
Architectures in the fat file: TTSegmentedControl are: i386 x86_64 armv7 arm64 
$ mv TTSegmentedControl Binding/TTSegmentedControl.framework/TTSegmentedControl

また、i386x86_64 の SwiftModule も必要となるので、あわせてコピーします。

$ cp /Users/***/Projects/TTSegmentedControlSample/Native/build/Release-iphonesimulator/TTSegmentedControl/TTSegmentedControl.framework/Modules/TTSegmentedControl.swiftmodule/* Binding/TTSegmentedControl.framework/Modules/TTSegmentedControl.swiftmodule/
$ ls Binding/TTSegmentedControl.framework/Modules/TTSegmentedControl.swiftmodule/
arm.swiftdoc        arm.swiftmodule     arm64.swiftdoc      arm64.swiftmodule   i386.swiftdoc       i386.swiftmodule    x86_64.swiftdoc     x86_64.swiftmodule

バインディングライブラリを作る

あとは、新しくなったObjective SharpieでCocoaPodsのバインディングライブラリを作る とほとんど手順は同じなのですが、1つだけ異なる点があります。

.framework のライブラリは「ネイティブ参照」のところに追加する必要があります。右クリックして、Add Native Reference を選択します。

スクリーンショット 2016-12-17 14.01.50.png

.framework を選択すると、中身が見えてしまいますが、.framework がアクティブになった状態のまま Open をクリックして追加します。

スクリーンショット 2016-12-17 14.06.35.png

プロパティを開いて Force Load のチェックを入れ、使用する Framework を指定しておきます。(Dark スキームだとちょっと見づらいです)

スクリーンショット 2016-12-17 18.32.47.png

次は ApiDefinition の作成です。まず、Swift のクラスはネイティブライブラリ内でも名前が変わってしまうので、別名指定を BaseTypeAttribute に追加する必要があります。 .framework の中にできた Headers/*-Swift.h の中にクラスの名前が SWIFT_CLASS マクロで書かれています。

SWIFT_CLASS("_TtC18TTSegmentedControl18TTSegmentedControl")
@interface TTSegmentedControl : UIView

これを、BaseType の Name に追加してやります。

[BaseType(typeof(UIView), Name = "_TtC18TTSegmentedControl18TTSegmentedControl")]
interface TTSegmentedControl

また、Swift 側でプライベートのエクステンションを多く使っている場合は空のインターフェイスが大量に生成されるのと、UIKit の override など、BaseType が持っているようなメソッドの定義は削除しておきましょう。

今回の ApiDefinition.cs は以下のようになりました。

using System;

using UIKit;
using Foundation;
using ObjCRuntime;
using CoreGraphics;

namespace TTSegmentedControl
{
    // @interface TTSegmentedControl : UIView
    [BaseType(typeof(UIView), Name = "_TtC18TTSegmentedControl18TTSegmentedControl")]
    interface TTSegmentedControl
    {
        // @property (nonatomic, strong) UIFont * _Nonnull defaultTextFont;
        [Export("defaultTextFont", ArgumentSemantic.Strong)]
        UIFont DefaultTextFont { get; set; }

        // @property (nonatomic, strong) UIFont * _Nonnull selectedTextFont;
        [Export("selectedTextFont", ArgumentSemantic.Strong)]
        UIFont SelectedTextFont { get; set; }

        // @property (nonatomic, strong) UIColor * _Nonnull defaultTextColor;
        [Export("defaultTextColor", ArgumentSemantic.Strong)]
        UIColor DefaultTextColor { get; set; }

        // @property (nonatomic, strong) UIColor * _Nonnull selectedTextColor;
        [Export("selectedTextColor", ArgumentSemantic.Strong)]
        UIColor SelectedTextColor { get; set; }

        // @property (nonatomic) BOOL useGradient;
        [Export("useGradient")]
        bool UseGradient { get; set; }

        // @property (nonatomic, strong) UIColor * _Nonnull containerBackgroundColor;
        [Export("containerBackgroundColor", ArgumentSemantic.Strong)]
        UIColor ContainerBackgroundColor { get; set; }

        // @property (nonatomic, strong) UIColor * _Nonnull thumbColor;
        [Export("thumbColor", ArgumentSemantic.Strong)]
        UIColor ThumbColor { get; set; }

        // @property (copy, nonatomic) NSArray<UIColor *> * _Nullable thumbGradientColors;
        [NullAllowed, Export("thumbGradientColors", ArgumentSemantic.Copy)]
        UIColor[] ThumbGradientColors { get; set; }

        // @property (nonatomic, strong) UIColor * _Nonnull thumbShadowColor;
        [Export("thumbShadowColor", ArgumentSemantic.Strong)]
        UIColor ThumbShadowColor { get; set; }

        // @property (nonatomic) BOOL useShadow;
        [Export("useShadow")]
        bool UseShadow { get; set; }

        // @property (nonatomic) CGSize padding;
        [Export("padding", ArgumentSemantic.Assign)]
        CGSize Padding { get; set; }

        // @property (nonatomic) CGFloat cornerRadius;
        [Export("cornerRadius")]
        nfloat CornerRadius { get; set; }

        // @property (copy, nonatomic) NSArray<NSString *> * _Nonnull itemTitles;
        [Export("itemTitles", ArgumentSemantic.Copy)]
        string[] ItemTitles { get; set; }

        // @property (copy, nonatomic) void (^ _Nullable)(NSInteger, NSString * _Nullable) didSelectItemWith;
        [NullAllowed, Export("didSelectItemWith", ArgumentSemantic.Copy)]
        Action<nint, NSString> DidSelectItemWith { get; set; }

        // @property (nonatomic) BOOL allowDrag;
        [Export("allowDrag")]
        bool AllowDrag { get; set; }

        // @property (nonatomic) BOOL allowChangeThumbWidth;
        [Export("allowChangeThumbWidth")]
        bool AllowChangeThumbWidth { get; set; }
    }


    // @interface TTSegmentedControl_Swift_157 (TTSegmentedControl)
    [Category]
    [BaseType(typeof(TTSegmentedControl))]
    interface TTSegmentedControl_TTSegmentedControl_Swift_157
    {
        // -(void)selectItemAtIndex:(NSInteger)index animated:(BOOL)animated;
        [Export("selectItemAtIndex:animated:")]
        void SelectItemAtIndex(nint index, bool animated);
    }
}

プロジェクトに Swift ライブラリを追加する

今回は同一ソリューションに iOS の Single View アプリケーションを追加したので、プロジェクト参照でバインディングライブラリを参照しました。

また、Swift を使ったライブラリを使う場合、SwiftSupport の NuGet ライブラリが必要となりますので、パッケージを追加しておきます。

スクリーンショット 2016-12-17 14.09.42.png

実機で署名が合わないとき

さて、このまま AdHoc ビルドをすると署名が合わずインストールができなくなる場合があります。これは、SwiftSupport のパッケージがビルドの終盤でシミュレータ用のバイナリも一度まとめてコピーして、シミュレータじゃなかった場合は消すという動きをするので、署名が微妙に合わなくなるのです。

このようなときはシミュレータ向けのバイナリを消すタイミングを変更することで回避できます。.csproj を開き、以下の Target を追加してください。

  <Target Name="RemoveSimulatorSymbols" AfterTargets="_CopyResourcesToBundle" Condition="'$(Platform)' == 'iPhone'">
    <Exec Command="rm -rf $(OutputPath)/$(AssemblyName).app/SwiftFrameworksSimulator" />
  </Target>

ストアに申請する ipa を作る

さて、いよいよストアに申請という段になっても、また別の作業が必要です。Swift を使うアプリの場合、ipa の中に SwiftSupport というものを持っておく必要があります。 Xcode の中に入っている Swift のライブラリを追加するという作業になるのですが、この作業をするためのスクリプト auto-ipa-packager が公開されています。

Xamarin Studio から出力されたストアビルドに対して、このスクリプトを実行することで、ストア申請用の SwiftSupport を追加することができます。

このコマンドを使うとき、使用している Swift ライブラリにあった Xcode の中にある Developer ディレクトリを DEVELOPER_DIR 環境変数として指定する必要があります。(Xcode とライブラリの Swift バージョンが一致しない場合は Invalid Swift Support となってしまいます)

$ DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer sh auto_package_ipa.sh Hoge.ipa

また、この作業でもパッケージの署名がおかしくなる場合があります。このような場合は、fastlane の sigh コマンドを使って再署名してやります。

$ sigh resign ./Hoge.ipa --signing_identity 'iPhone Distribution: Nobuhiro Ito (XXXXXXXXXX)' -p net.iseteki.hoge=EbiSoft_Hoge_for_AppStore.mobileprovison

Swift のライブラリを使うのはまだまだ大変ですが、今のおしゃれな UI コンポーネントは Swift が多いので、ぜひ覚えておいて損はないかと思います。

ただ、こんな手のかかることやりたくないというのも事実ですので、Xamarin 自体の Swift サポートが強化されるのを願いたいと思います。

マウントできなくなったTimeMachineからデータを救出した話

先日、MacBook Pro が起動しなくなりました。救出ボリュームから起動したらSSDが認識されていません。そうMacBook Pro 13-inch (Early 2011) に Fusion Drive を搭載したら BootCamp 作れなくなった話 - backyard of 伊勢的新常識でも書いた通り、Fusion Drive にしていたので、SSDとHDDのどちらか片方が死んだら起動できなくなるのです。

ですので、TimeMachineを設定していたのですが、よくないことは続くもので、数日前から TimeMachine をマウントできておらず、この週末に直そうと思っていたところなのです。

最近は多くのデータをDropboxやOneDriveといったクラウド上に置いていたので、作業中だった夏コミの原稿を含めて無事だったのですが、問題は手元にしかないiTunesライブラリです。かれこれ15年近く育ててきたライブラリが一瞬で終了したわけです。

このままあきらめるわけにはいかない。SSDはもうマウントすらできなくなったFusion Driveはもう戻ってこないので諦めて、HDDだけの構成にしてMacBookを仮復旧。TimeMachine を救出することを考えて、なんとかデータの吸出しがはじまりました。

まだコピー中ですが、とりあえず手順のメモをかねて。

ディスクユーティリティを使う

続きを読む

コミックマーケット90にサークル参加します!

伊勢的新常識はコミックマーケット90に参加します。スペースは3日目 西f-04bです。

また今回も、id:tmytスマートフォンアプリを黒い方面になんやかんやする内容のコピー本を出します。今のところ、こういうネタ予定してます。やる気があれば小ネタもう1つくらい足すかも。ページ数決まってないのでなんともですが、500円以下にはなりそうです。

  • 手軽に始める Bluetooth Low Energy 機器解析
  • Aristeaの魔窟 ~ future of Aristea かっこかり

もう夏しか参加できないのに、夏2連敗してたので、すごい久しぶりです。活動の空きが年単位になりますね。

3日目 西f-04b でお待ちしておりますので、何かのついでにお立ち寄りいただければ幸いです。(今回はちゃんと本人います!)