- 投稿日:2020-01-24T17:27:02+09:00
意外と簡単、iOSアプリへAppleWatchアプリを繋ぎこみ!!
はじめに
AppleWatchは発売からすでに5年が経過しようとしていますが、
アプリの作成に関する記事、事柄というのはまだあまり世の中に出回っていない認識です。
「今iOS向けに作成しているアプリに、AppleWatchのアプリを紐づけて配信したいけど、どんな手順が必要なんだろう?」
といったことでお困りの方も多いのではないかと多います。本記事はそんな方向けに、既存のiOSアプリへAppleWatchプロジェクトを紐づける際の手順・注意点を記載します。
手順
※以下はすでにiOSアプリが存在する前提で話を進めていきますが、
仮にAppleWatch単体で動くアプリを作りたい!という場合にも、概ね参考にはなるかと思います。AppleWatchプロジェクトの作成
- なにはともあれ、まずはXCodeを開きます。
次に、Xcode上部の
File → New → Target...
を選択します。
下記のようなウィンドウが開くので、[Watch App for iOS App] を選択し、[Next]を押下します。
※ちなみにここで[Watch App]を選択すると、AppleWatch単体で動くアプリになりますのでご注意ください。
次の設定画面では、
Product Name
に対してAppleWatchAppの命名を行います。
(iOSアプリの名称)WatchApp
などが一般的でしょうか?
OrganizationNameは記憶上使ったことがない気がするので、適当に入力すればいったんOKです。
→ project.pbxprojファイルにもこの名称は刻まれていなかったため、本当に不要かも・・・?ちなみに下記画像にも現れている
Complication
というのは、
AppleWatchのホーム画面にアプリへのショートカットを配置するためのものです。
少々設定/作成に難がありますが、あったら便利な機能ではあるのでこの段階でcheckを付けておくのもアリかと。
ここまでくると、TargetにWatchAppとWatchAppExtensionの2つのTargetが追加されたかと思います。
これらは依存関係がありますので、AppleWatchのアプリを動かすのには2個セットが当たり前、と考えておいてもらってOKです。プロジェクトの作成自体はこれで完了です!
ProvisionigProfileの追加
今後AppStoreへ審査提出していく際には、
今までのiOSアプリに対してこれら2種のTargetが追加配布されるような形となります。
XCodeのSigning & Capabilities
の欄をチラ見してしまった方はお気づきかもしれませんが、
つまり新しいProvisioningProfileも2つ必要ということです。ここは特に難しい点はありませんが、iOSアプリ同様にApple Developer Programより、
Identifierの登録×2とProvisioning Profileの登録×2を行なってください。Identifier登録時に必要なCapabilitiesの登録についても、AppleWatchだから、という特別な項目はありませんのでご安心を。
WatchAppの中身を作っていく
- ここは割愛。
最近ですとSwiftUIを用いてのUI作成もできるようなので、今後どんどんアプリ作成の幅が広がることを願います...!!!Buildして実機で動かしてみたい!
通常のiOSアプリと同様です。
まずはAppleWatchとペアリングしたiPhoneを、PCに接続してください。
すると、接続中のDevice一覧にiPhone + AppleWatch
といった形で表示されるかと思います。
TargetはiOSアプリのままでも、Buildすれば勝手にAppleWatchにインストールされます。
ただし、AppleWatch側のデバッグを行いたい場合には、TargetをAppleWatchアプリに設定する必要があります。ちなみにiOS同様、シュミレーターで動かすことも勿論可能です。
操作性はかなり悪いですが。。。(笑)AppleWatchアプリならではの注意事項
[ 審査提出について ]
iOSアプリ, WatchApp, WatchAppExtensionのそれぞれのVersion, Buildの値は同一である必要があります。
これを守らないとArchive後のUploadフローで怒られてArchiveやり直しになるので注意が必要です。
また当然ですが、AppleWatch用のアイコンを用意したり、AppStoreに掲載するためのスクリーンショットなどを撮る必要もあります。
初めて提出する際は、実際の利用に際したビデオの作成であったり、アカウント情報を求められることもあるのでご注意を。詳しい仕様は下記URLで確認可能です。
https://help.apple.com/app-store-connect/#/devd274dd925[ アプリインストールについて ]
以下のすべての場合において、AppleWatchアプリはiOSアプリをインストールすることにより自動でインストールされます!
- 実機デバッグ時
- DeployGateなどのアプリインストールサービスを用いた際
- TestFlight
- 実際のAppStoreからのインストール
※逆に「AppleWatchからのみアプリアンインストールしたい」みたいなケースでは、
iPhoneに備わっている[Watch]アプリから削除することが可能です。[ 審査自体について ]
AppleWatchのアプリが追加されたからといって特段審査が遅くなる、といったことはないように感じます。[ Targetの依存関係について ]
XCodeのGUI上で、プロジェクトのBuild Phases
をみればわかりますが、
各依存関係は以下のようになっています。
WatchAppExtension → WatchApp → iOSApp
少し奇妙ではありますが、
Embed
という形で逆向きの依存関係も持っています。
これもBuild Phases
のEmbed Watch Content
,Embed App Extensions
で確認可能です。[ Apple Watchへの通知ってどうなるの? ]
特に意識せずとも、AppleWatchが勝手に制御してくれます。
iOSアプリへ通知があって、かつApple Watchを装着していれば振動とともに教えてくれますおわりに
たしかにAppleWatchの(特に日本向け)記事は少ないのですが、
こうまとめてみるとプロジェクトのスタートも、審査に必要なことも比較的明確でそれほど怖いものではありません。
Let's enjoy AppleWatch!
- 投稿日:2020-01-24T13:04:11+09:00
[Swift]アクセスレベル
今回はアクセスレベルについて書いていきたいと思います?
よく変数や関数の前にPrivateといったのを見たことはありませんか?それの事です?対象はSwift5です。
公式ドキュメントはこちらアクセスレベルとは
簡単に言うと宣言した変数やメソッドなどを何処でも使えるようにするか、又は制限した範囲でのみ使用可能にするか設定することです。
全部で5つあり、それぞれ特徴があります。
アクセスレベル 説明 Private 宣言したコードブロック内でのみアクセス可能(これの中→{}) FilePrivate 宣言されたファイル内でのみアクセス可能 Internal アプリのモジュール内でアクセス可能(デフォルトはこれで、設定しなかったら自動でInternalになる) Public 他のモジュールへのアクセスが可能になるが、継承やオーバーライドは不可 Open 継承もオーバーライドも可能になる、つまり何でもできる なぜアクセスレベルを使うのか
アクセスレベルを設定しないとデフォルトでInternalになるので、モジュール内でアクセス可能になってしまいます。
モジュール内で使うことを想定しているのなら問題ないのですが、特定の処理に使うことを想定した変数やメソッドを関係無いところで使われるのは問題です?
それを防ぐためにアクセスレベルを設定し、意図しない利用を防ぎます!?♂️このアクセスレベルはいつでも変更が可能なので、初期段階で利用範囲が決まっていないのなら、基本的にPrivateで設定して置くのが無難です。
利用範囲が大きくなったタイミングで段階的にアクセスレベルを広げていきましょう!?♂️
- 投稿日:2020-01-24T10:23:02+09:00
【Swift】touchesMovedが実行される最小移動量について【Tips】
連続的なジェスチャーの検知に使われる
touchesMoved
が実行されるタイミングに関して調査してみたので簡単にまとめる。検証環境
- Swift5.0
- iPhone6s(iOS13.3)
検証方法
以下の様なコードでコンソールに測定した位置を出力することで検証した。
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) { let latestTouch = touches.first let newPoint: CGPoint = (newTouch?.location(in: view))! // previousPointはクラスのプロパティとして保持 let translation = CGPoint(x: previousPoint.x - newPoint.x, y: previousPoint.y - newPoint.y) previousPoint = newPoint print(translation)実行される最小移動量
上記の検証コードを用いて上方向にまっすぐゆっくりとスクロールしていくと以下のようなログが出力される。
(0.0, 0.5) (0.0, 0.5) (0.0, 0.5) (0.0, 0.5) (0.0, 0.5) (0.0, 0.5)つまり、
touchesMoved
が実行されるには最小で0.5ptの移動が必要、ということになる。
間違い等あればコメントをおねがいします!
- 投稿日:2020-01-24T00:02:51+09:00
mediapipeをiOSアプリに組み込む
mediapipeとは
Googleの開発しているML推定のパイプラインを簡単に設定できるライブラリ。Windowsを初めiOSやAndroidなどマルチプラットフォームに対応している。
https://github.com/google/mediapipe
mediapipeにはプリセットで手や関節の位置推定や顔の位置推定のモデルがバンドルされていてexampleで確認することができます。
https://github.com/google/mediapipe/tree/master/mediapipe/modelsiOSアプリにmediapipeを組み込む
iOSアプリにmediapipeで作ったパイプラインを組み込むには、一度フレームワークとしてビルドするかiOSアプリをbazelで管理する必要があります。
ここでは既存のアプリに組み込むことを想定してフレームワークとしてビルドしてみましょう。また今回の作業は以下のブランチで行っていたものです。
説明を省いている箇所はコミットなどを覗いてみてください。(もしくはqiitaにコメントいただければ補足します)
https://github.com/noppefoxwolf/mediapipe/tree/develop/TrackerFrameworkmediapipeをクローンする
git clone git@github.com:google/mediapipe.gitbazelをインストールする
mediapipeはbazel 1.1.0が必要なため、bazelをインストールします。
bazelはgoogle製のビルドツールで、mediapipeはbazelで構築されているため必要になります。
installスクリプトを下記からダウンロードして、実行します。https://github.com/bazelbuild/bazel/releases/download/1.1.0/bazel-1.1.0-installer-darwin-x86_64.sh
$ ./bazel-1.1.0-installer-darwin-x86_64.sh
トラッカーのコードを書く
Swiftから使いやすいように簡単にトラッカーを実装します。
mediapipe/develop/HandTracker.h#import <Foundation/Foundation.h> #import <CoreVideo/CoreVideo.h> @class Landmark; @class HandTracker; @protocol TrackerDelegate <NSObject> @optional - (void)handTracker: (HandTracker*)handTracker didOutputLandmarks: (NSArray<Landmark *> *)landmarks; - (void)handTracker: (HandTracker*)handTracker didOutputPixelBuffer: (CVPixelBufferRef)pixelBuffer; @end @interface HandTracker : NSObject - (instancetype)init; - (void)startGraph; - (void)processVideoFrame:(CVPixelBufferRef)imageBuffer; @property (weak, nonatomic) id <TrackerDelegate> delegate; @end @interface Landmark: NSObject @property(nonatomic, readonly) float x; @property(nonatomic, readonly) float y; @property(nonatomic, readonly) float z; @endmediapipe/develop/HandTracker.mm#import "HandTracker.h" #import "mediapipe/objc/MPPGraph.h" #import "mediapipe/objc/MPPCameraInputSource.h" #import "mediapipe/objc/MPPLayerRenderer.h" #include "mediapipe/framework/formats/landmark.pb.h" static NSString* const kGraphName = @"hand_tracking_mobile_gpu"; static const char* kInputStream = "input_video"; static const char* kOutputStream = "output_video"; static const char* kLandmarksOutputStream = "hand_landmarks"; static const char* kVideoQueueLabel = "com.google.mediapipe.example.videoQueue"; @interface HandTracker() <MPPGraphDelegate> @property(nonatomic) MPPGraph* mediapipeGraph; @end @interface Landmark() - (instancetype)initWithX:(float)x y:(float)y z:(float)z; @end @implementation HandTracker {} #pragma mark - Cleanup methods - (void)dealloc { self.mediapipeGraph.delegate = nil; [self.mediapipeGraph cancel]; // Ignore errors since we're cleaning up. [self.mediapipeGraph closeAllInputStreamsWithError:nil]; [self.mediapipeGraph waitUntilDoneWithError:nil]; } #pragma mark - MediaPipe graph methods + (MPPGraph*)loadGraphFromResource:(NSString*)resource { // Load the graph config resource. NSError* configLoadError = nil; NSBundle* bundle = [NSBundle bundleForClass:[self class]]; if (!resource || resource.length == 0) { return nil; } NSURL* graphURL = [bundle URLForResource:resource withExtension:@"binarypb"]; NSData* data = [NSData dataWithContentsOfURL:graphURL options:0 error:&configLoadError]; if (!data) { NSLog(@"Failed to load MediaPipe graph config: %@", configLoadError); return nil; } // Parse the graph config resource into mediapipe::CalculatorGraphConfig proto object. mediapipe::CalculatorGraphConfig config; config.ParseFromArray(data.bytes, data.length); // Create MediaPipe graph with mediapipe::CalculatorGraphConfig proto object. MPPGraph* newGraph = [[MPPGraph alloc] initWithGraphConfig:config]; [newGraph addFrameOutputStream:kOutputStream outputPacketType:MPPPacketTypePixelBuffer]; [newGraph addFrameOutputStream:kLandmarksOutputStream outputPacketType:MPPPacketTypeRaw]; return newGraph; } - (instancetype)init { self = [super init]; if (self) { self.mediapipeGraph = [[self class] loadGraphFromResource:kGraphName]; self.mediapipeGraph.delegate = self; // Set maxFramesInFlight to a small value to avoid memory contention for real-time processing. self.mediapipeGraph.maxFramesInFlight = 2; } return self; } - (void)startGraph { // Start running self.mediapipeGraph. NSError* error; if (![self.mediapipeGraph startWithError:&error]) { NSLog(@"Failed to start graph: %@", error); } } #pragma mark - MPPGraphDelegate methods // Receives CVPixelBufferRef from the MediaPipe graph. Invoked on a MediaPipe worker thread. - (void)mediapipeGraph:(MPPGraph*)graph didOutputPixelBuffer:(CVPixelBufferRef)pixelBuffer fromStream:(const std::string&)streamName { if (streamName == kOutputStream) { [_delegate handTracker: self didOutputPixelBuffer: pixelBuffer]; } } // Receives a raw packet from the MediaPipe graph. Invoked on a MediaPipe worker thread. - (void)mediapipeGraph:(MPPGraph*)graph didOutputPacket:(const ::mediapipe::Packet&)packet fromStream:(const std::string&)streamName { if (streamName == kLandmarksOutputStream) { if (packet.IsEmpty()) { return; } const auto& landmarks = packet.Get<::mediapipe::NormalizedLandmarkList>(); NSMutableArray<Landmark *> *result = [NSMutableArray array]; for (int i = 0; i < landmarks.landmark_size(); ++i) { Landmark *landmark = [[Landmark alloc] initWithX:landmarks.landmark(i).x() y:landmarks.landmark(i).y() z:landmarks.landmark(i).z()]; [result addObject:landmark]; } [_delegate handTracker: self didOutputLandmarks: result]; } } - (void)processVideoFrame:(CVPixelBufferRef)imageBuffer { [self.mediapipeGraph sendPixelBuffer:imageBuffer intoStream:kInputStream packetType:MPPPacketTypePixelBuffer]; } @end @implementation Landmark - (instancetype)initWithX:(float)x y:(float)y z:(float)z { self = [super init]; if (self) { _x = x; _y = y; _z = z; } return self; } @endBUILDファイルを書く
bazelはBUILDファイルを書くことで、ビルドオプションを設定します。
また、mediapipeはいくつかのビルドオプションが既に設定されているので、mediapipeのディレクトリ以下に次のように配置します。mediapipe/develop/BUILDload("@build_bazel_rules_apple//apple:ios.bzl", "ios_framework") ios_framework( name = "HandTracker", hdrs = [ "HandTracker.h", ], infoplists = ["Info.plist"], bundle_id = "com.noppelab.HandTracker", families = ["iphone", "ipad"], minimum_os_version = "10.0", deps = [ ":HandTrackingGpuAppLibrary", "@ios_opencv//:OpencvFramework", ], ) # To use the 3D model instead of the default 2D model, add "--define 3D=true" to the # bazel build command. config_setting( name = "use_3d_model", define_values = { "3D": "true", }, ) genrule( name = "model", srcs = select({ "//conditions:default": ["//mediapipe/models:hand_landmark.tflite"], ":use_3d_model": ["//mediapipe/models:hand_landmark_3d.tflite"], }), outs = ["hand_landmark.tflite"], cmd = "cp $< $@", ) objc_library( name = "HandTrackingGpuAppLibrary", srcs = [ "HandTracker.mm", ], hdrs = [ "HandTracker.h", ], data = [ ":model", "//mediapipe/graphs/hand_tracking:hand_tracking_mobile_gpu_binary_graph", "//mediapipe/models:palm_detection.tflite", "//mediapipe/models:palm_detection_labelmap.txt", ], sdk_frameworks = [ "AVFoundation", "CoreGraphics", "CoreMedia", "UIKit" ], deps = [ "//mediapipe/objc:mediapipe_framework_ios", "//mediapipe/objc:mediapipe_input_sources_ios", "//mediapipe/objc:mediapipe_layer_renderer", ] + select({ "//mediapipe:ios_i386": [], "//mediapipe:ios_x86_64": [], "//conditions:default": [ "//mediapipe/graphs/hand_tracking:mobile_calculators", "//mediapipe/framework/formats:landmark_cc_proto", ], }), )Info.plistを作る
フレームワークにバンドルするInfo.plistを適当に作ります。
https://github.com/noppefoxwolf/mediapipe/blob/develop/TrackerFramework/mediapipe/develop/Info.plist
を参考にしてください。
フレームワークをビルドする
bazelを使ってフレームワークをビルドします。
$ bazel build --config=ios_fat --define 3D=true mediapipe/develop:HandTrackerビルドが成功すると
bazel-bin/mediapipe/develop/
にzipファイルが生成されているので、それを解凍すると.frameworkファイルが出てきます。
framework search pathやheader search pathを設定すれば、iOSアプリから呼び出すことができます。面倒な場合
とりあえず3D HandTrackerはcarthageで入れられるようにしてみたので、面倒な場合はこちらを使ってみてください。