20200124のiOSに関する記事は4件です。

意外と簡単、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単体で動くアプリになりますのでご注意ください。
    スクリーンショット 2020-01-24 14.07.04.png

  • 次の設定画面では、Product Name に対してAppleWatchAppの命名を行います。

    (iOSアプリの名称)WatchAppなどが一般的でしょうか?
    OrganizationNameは記憶上使ったことがない気がするので、適当に入力すればいったんOKです。
    project.pbxprojファイルにもこの名称は刻まれていなかったため、本当に不要かも・・・?

    ちなみに下記画像にも現れているComplicationというのは、
    AppleWatchのホーム画面にアプリへのショートカットを配置するためのものです。
    少々設定/作成に難がありますが、あったら便利な機能ではあるのでこの段階でcheckを付けておくのもアリかと。
    スクリーンショット 2020-01-24 14.09.51.png

  • ここまでくると、TargetにWatchAppとWatchAppExtensionの2つのTargetが追加されたかと思います。
    これらは依存関係がありますので、AppleWatchのアプリを動かすのには2個セットが当たり前、と考えておいてもらってOKです。

    プロジェクトの作成自体はこれで完了です!:metal:

ProvisionigProfileの追加

  • 今後AppStoreへ審査提出していく際には、
    今までのiOSアプリに対してこれら2種のTargetが追加配布されるような形となります。
    XCodeの Signing & Capabilitiesの欄をチラ見してしまった方はお気づきかもしれませんが、
    つまり新しいProvisioningProfileも2つ必要ということです。

  • ここは特に難しい点はありませんが、iOSアプリ同様にApple Developer Programより、
    Identifierの登録×2Provisioning 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アプリならではの注意事項

  1. [ 審査提出について ]
    iOSアプリ, WatchApp, WatchAppExtensionのそれぞれのVersion, Buildの値は同一である必要があります。
    これを守らないとArchive後のUploadフローで怒られてArchiveやり直しになるので注意が必要です。
    また当然ですが、AppleWatch用のアイコンを用意したり、AppStoreに掲載するためのスクリーンショットなどを撮る必要もあります。
    初めて提出する際は、実際の利用に際したビデオの作成であったり、アカウント情報を求められることもあるのでご注意を。

    詳しい仕様は下記URLで確認可能です。
    https://help.apple.com/app-store-connect/#/devd274dd925

  2. [ アプリインストールについて ]
    以下のすべての場合において、AppleWatchアプリはiOSアプリをインストールすることにより自動でインストールされます!

    • 実機デバッグ時
    • DeployGateなどのアプリインストールサービスを用いた際
    • TestFlight
    • 実際のAppStoreからのインストール
      ※逆に「AppleWatchからのみアプリアンインストールしたい」みたいなケースでは、
      iPhoneに備わっている[Watch]アプリから削除することが可能です。
  3. [ 審査自体について ]
    AppleWatchのアプリが追加されたからといって特段審査が遅くなる、といったことはないように感じます。

  4. [ Targetの依存関係について ]
    XCodeのGUI上で、プロジェクトのBuild Phasesをみればわかりますが、
    各依存関係は以下のようになっています。
    WatchAppExtension → WatchApp → iOSApp

    少し奇妙ではありますが、Embedという形で逆向きの依存関係も持っています。
    これもBuild PhasesEmbed Watch Content, Embed App Extensionsで確認可能です。

  5. [ Apple Watchへの通知ってどうなるの? ]
    特に意識せずとも、AppleWatchが勝手に制御してくれます。
    iOSアプリへ通知があって、かつApple Watchを装着していれば振動とともに教えてくれます:sparkles:

おわりに

たしかにAppleWatchの(特に日本向け)記事は少ないのですが、
こうまとめてみるとプロジェクトのスタートも、審査に必要なことも比較的明確でそれほど怖いものではありません。
Let's enjoy AppleWatch!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift]アクセスレベル

今回はアクセスレベルについて書いていきたいと思います📝
よく変数や関数の前にPrivateといったのを見たことはありませんか?それの事です😎

対象はSwift5です。
公式ドキュメントはこちら

アクセスレベルとは

簡単に言うと宣言した変数やメソッドなどを何処でも使えるようにするか、又は制限した範囲でのみ使用可能にするか設定することです。

全部で5つあり、それぞれ特徴があります。

アクセスレベル 説明
Private 宣言したコードブロック内でのみアクセス可能(これの中→{})
FilePrivate 宣言されたファイル内でのみアクセス可能
Internal アプリのモジュール内でアクセス可能(デフォルトはこれで、設定しなかったら自動でInternalになる)
Public 他のモジュールへのアクセスが可能になるが、継承やオーバーライドは不可
Open 継承もオーバーライドも可能になる、つまり何でもできる

なぜアクセスレベルを使うのか

アクセスレベルを設定しないとデフォルトでInternalになるので、モジュール内でアクセス可能になってしまいます。
モジュール内で使うことを想定しているのなら問題ないのですが、特定の処理に使うことを想定した変数やメソッドを関係無いところで使われるのは問題です😱
それを防ぐためにアクセスレベルを設定し、意図しない利用を防ぎます!🙅‍♂️

このアクセスレベルはいつでも変更が可能なので、初期段階で利用範囲が決まっていないのなら、基本的にPrivateで設定して置くのが無難です。
利用範囲が大きくなったタイミングで段階的にアクセスレベルを広げていきましょう!🙆‍♂️

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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の移動が必要、ということになる。
間違い等あればコメントをおねがいします!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

mediapipeをiOSアプリに組み込む

mediapipeとは

Googleの開発しているML推定のパイプラインを簡単に設定できるライブラリ。Windowsを初めiOSやAndroidなどマルチプラットフォームに対応している。

https://github.com/google/mediapipe

mediapipeにはプリセットで手や関節の位置推定や顔の位置推定のモデルがバンドルされていてexampleで確認することができます。
https://github.com/google/mediapipe/tree/master/mediapipe/models

sample.gif

iOSアプリにmediapipeを組み込む

iOSアプリにmediapipeで作ったパイプラインを組み込むには、一度フレームワークとしてビルドするかiOSアプリをbazelで管理する必要があります。
ここでは既存のアプリに組み込むことを想定してフレームワークとしてビルドしてみましょう。

また今回の作業は以下のブランチで行っていたものです。
説明を省いている箇所はコミットなどを覗いてみてください。(もしくはqiitaにコメントいただければ補足します)
https://github.com/noppefoxwolf/mediapipe/tree/develop/TrackerFramework

mediapipeをクローンする

git clone git@github.com:google/mediapipe.git

bazelをインストールする

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;
@end

mediapipe/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;
}

@end

BUILDファイルを書く

bazelはBUILDファイルを書くことで、ビルドオプションを設定します。
また、mediapipeはいくつかのビルドオプションが既に設定されているので、mediapipeのディレクトリ以下に次のように配置します。

mediapipe/develop/BUILD
load("@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で入れられるようにしてみたので、面倒な場合はこちらを使ってみてください。

https://github.com/noppefoxwolf/HandTracker

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む