20210303のiOSに関する記事は7件です。

error Runner.app/Info.plist does not exist. The Flutter "Thin Binary" build phase must run after "Copy Bundle Resources".が出たら

ここを参考にした
ちなみに自分はAndroid StudioでClean Build Folderだけでなおった

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

【SwiftUI】よく使うビューをテンプレートとして作る方法

プログラミングの基本的な考え方として、重複するコードは何度も書かず、テンプレートを作成して使い回すというものがあるかと思います。SwiftUIでも同様に、同じようなビューをテンプレートとして用意し、記述をより簡単にすることができます。

テンプレを作らず書くとどうなるか

次のような、ボタンが4つあるアプリを考えます。

スクリーンショット 2021-03-03 21.28.51.png

それぞれのボタンを押すと、それに対応したキャラクターが画面上部に表示される仕組みです。この機能を普通に書くとこのようになります。

ContentView.swift
import SwiftUI

let character = ["ルフィ", "ゾロ", "ナミ", "サンジ"]

struct ContentView: View {
    @State var id = 0

    var body: some View {
        VStack{
            Image(character[id])
                .resizable()
                .aspectRatio(contentMode: .fit)
            Text("Choose Your Character!!")
                .font(.title)
                .fontWeight(.bold)
            // ボタンを4つ並べる
            HStack{
                Button(action: {
                    id = 0
                }, label: {
                    Text("ルフィ")
                        .font(.title)
                        .foregroundColor(.white)
                        .frame(width: 130, height: 130)
                        .background(Color.red)
                        .cornerRadius(20)
                })
                .padding()
                Button(action: {
                    id = 1
                }, label: {
                    Text("ゾロ")
                        .font(.title)
                        .foregroundColor(.white)
                        .frame(width: 130, height: 130)
                        .background(Color.green)
                        .cornerRadius(20)
                })
                .padding()
            }
            HStack{
                Button(action: {
                    id = 2
                }, label: {
                    Text("ナミ")
                        .font(.title)
                        .foregroundColor(.white)
                        .frame(width: 130, height: 130)
                        .background(Color.orange)
                        .cornerRadius(20)
                })
                .padding()
                Button(action: {
                    id = 3
                }, label: {
                    Text("サンジ")
                        .font(.title)
                        .foregroundColor(.white)
                        .frame(width: 130, height: 130)
                        .background(Color.blue)
                        .cornerRadius(20)
                })
                .padding()
            }
        }
    }
}

長い。

コードをよく読むと、この部分が繰り返し使われていることがわかります。

重複しているコード
                Button(action: {
                    id = 0
                }, label: {
                    Text("ルフィ")
                        .font(.title)
                        .foregroundColor(.white)
                        .frame(width: 130, height: 130)
                        .background(Color.red)
                        .cornerRadius(20)
                })
                .padding()

ボタンに書かれている文字や色、機能によって少しコードに違いは出ますが、だいたいは同じことが書かれています。この部分をテンプレートとして準備すれば良さそうです。

ビューの基本的な書き方

ビューの書き方
struct hoge: View {

    // プロパティなど

    var body: some View {

        // ここにビューを書く

    }
}

まず、ビューの基本的な書き方は上のようになります。ここで気づく方もいると思いますが、これはSwiftUIで新しくファイルを作ったときに書かれる内容とまったく同じものです。このbodyの中にビューを書くことでテンプレートを作ることができます。では、実際に書いてみましょう。

テンプレートを作って書き換えよう

テンプレート
struct CharacterButton: View {
    // 違う部分はプロパティにしてそれぞれ変えられるようにする
    @Binding var id: Int
    let number: Int
    let buttonColor: Color

    var body: some View {
        // 重複しているコードを書き出す
        Button(action: {
            id = number
        }, label: {
            Text(character[number])
                .font(.title)
                .foregroundColor(.white)
                .frame(width: 130, height: 130)
                .background(buttonColor)
                .cornerRadius(20)
        })
        .padding()
    }
}

このようになります。重複しているコードをそのまま書いてしまうと違いが出せないため、違う部分はプロパティにし、インスタンス生成時に情報を書けるようにします。

このテンプレートを呼び出すときは次のように書きます。

テンプレートの呼び出し
CharacterButton(id: , number: , buttonColor: )

引数にそれぞれのボタンが持つ特性を書き込めば、インスタンスを生成できます。
このテンプレートを使用し、ContentView.swiftを書き換えたいと思います。

ContentView.swift
import SwiftUI

let character = ["ルフィ", "ゾロ", "ナミ", "サンジ"]

struct ContentView: View {
    @State var id = 0

    var body: some View {
        VStack{
            Image(character[id])
                .resizable()
                .aspectRatio(contentMode: .fit)
            Text("Choose Your Character!!")
                .font(.title)
                .fontWeight(.bold)
            // ボタンを4つ並べる
            HStack{
                CharacterButton(id: $id, number: 0, buttonColor: Color.red)
                CharacterButton(id: $id, number: 1, buttonColor: Color.green)
            }
            HStack{
                CharacterButton(id: $id, number: 2, buttonColor: Color.orange)
                CharacterButton(id: $id, number: 3, buttonColor: Color.blue)
            }
        }
    }
}

// テンプレート
struct CharacterButton: View {
    @Binding var id: Int
    let number: Int
    let buttonColor: Color

    var body: some View {
        Button(action: {
            id = number
        }, label: {
            Text(character[number])
                .font(.title)
                .foregroundColor(.white)
                .frame(width: 130, height: 130)
                .background(buttonColor)
                .cornerRadius(20)
        })
        .padding()
    }
}

スッキリして読みやすくなりましたね。それだけでなく、もしボタンのUIを一括に変更したい、ということがあった場合、テンプレートひとつをいじれば良いのでメンテナンス性も向上しています。

まとめ

スッキリとした読みやすいコードが書けると、アップデートなどで後々自分が楽になります。テンプレートを駆使して読みやすいコードが書けるよう、私も考えながら書きたいと思います。

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

ノンデザイナーでもできる!AppStoreのスクショをサクッと用意する方法!

AppStoreのスクショ問題

こんちは、個人アプリ開発者のまつじです。
個人開発をしていると立ちはだかる壁の一つにAppStoreのスクリーンショットがあります。
あれって地味に大変じゃないですか?
あのスクショを作成する段階まできてれば、アプリ自体は完成に近い状態だと思います。

「リリース目前なのに、スクショの準備が必要...。だけどデザイナーでもないし、そんなに良いものを簡単に用意できない...。でも中途半端なものにするとインストールされないかもしれない...。」

そんな思いが駆け巡りますよね。笑
この記事では、ノンデザイナーな個人アプリ開発者が、サクッとそれっぽいスクショを用意する方法を説明します。

題材

今回は僕が個人開発したiOSアプリ、Widgetterを題材にしたいと思います。
文字とスクショのみと至ってシンプルな構成ではあるものの、デザイン初心者のわりには綺麗じゃないですか?
こういったスクショを作る方法を今日は説明します。

もし、スクショのことよりアプリのことが気になってきたって人がいたら、ぜひ こちら からインストールできますので、よろしくお願いします!

使うソフト

いくら綺麗なスクショができるからって、結局使うソフトが高いものだったり、学習コストが高いものを使うのには抵抗がありますよね?
安心してください、iOSアプリ開発者であれば全員が持っているソフトを使います。

今回使うのは次のたった2つです。

  • Keynote (PowerPoint可)
  • Simulator

多分皆さんインストール済みですよね?
早速説明していきます。

素材の準備

まずは素材の準備ですが、この記事の一番のポイントはここです。
Simulatorを起動し、スクショの設定を変えます。

メニューバーから、"Preferences"を開き、

"Show device mask in screenshots"を有効化します。

こうすると、Simulator上でスクショを撮ったとき、次の画像のようにiPhoneX以降のデバイスで上のノッチがマスクされるます。
(スクショは「⌘ + s」で撮れます)

これで準備完了です、ここからはKeynoteで実際に画像を準備していきます。

Keynoteでの作業

まずは、AppStoreのスクショのサイズのスライドを用意します。
新規書類を作成した後にスライドのサイズを変えます。
6.5インチの場合は、1242×2688です。

余計なテキストボックスは削除して、先ほど用意したスクショを貼り、位置を調整します。
変な位置にしたりせず、真ん中になるように配置してください。

こんな感じですね。
あとは、これの下に角丸の四角形を配置します。
上の図形から、「角丸四角形」を選択して追加し、最背面に設定します。

もうお分かりですかね?
これを下のスクショのの周りに配置し、角丸とサイズを調整しましょう。
この時、ベゼルが細い方がかっこいい感じになります。

良い感じになってきました。
さらにちょっとしたtipsですが、周りの色は純粋な黒(16進数だと000000)より、少し灰色ぎみだったり、他の色を混ぜた方がおしゃれに見えます。
例えば、周りの色を少し灰色(333333)にするとこんな感じです。

僕は自分のアプリのテーマカラー(141D2C)を使いました。

最後に文字です。
文字を入れるときのポイントは「できるだけ文字数を減らす!」と「改行する時は単語や文節で改行する!」です。

これを踏まえて、文字を追加するとこんな感じです。

文字を左揃えにするか中央揃えにするかは難しい問題です。
個人的には、全てのスクショの文字が2行で収まるなら中央揃え、3行になるなら左揃えです。
4行以上の場合は文言を考え直しましょう。

まだなんか文字が細くて弱々しいですね、文字を太くしましょう。
ここでまたtipsですが、個人的にこだわりがないのであればGoogleとAdobeが共同開発した源ノ角ゴシックがおすすめです。
商用利用可能な上、日本語とアルファベットを両方同じフォントでも違和感がなく、さらにウェイト(文字の太さ)も豊富です。
今回は源ノ角ゴシックのBoldを使いました。(行間も少しいじりました)

だいぶいい感じですね。
最後に、この文字の色がまだ純粋な黒(000000)です。
純粋な黒をそのまま使うと素人感が出ますし、せっかく下の端末の色も自分で決められるわけですから、同じ色(141D2C)に統一しましょう。

微妙な差ではありますが、印象が少しだけ変わったように思います。

これでスクショの一枚が完成です。
画像を使いながら逐一説明したので、長いように感じたかもしれませんが慣れるとサクッとできます。
また、全てのスクショで同じことをする必要はありません。
今回作ったスライドをコピペして新しいスライドを作り、文字と画像だけ置換してあげれば大量生産できます。
(というかそうした方がスクショの位置がずれたりせず、統一したスクショが作れます。)

まとめ

どうでしたか?案外簡単にできたのではないでしょうか?
僕はこんな感じでWidgetterのスクショを作っていきました。
(実際に使ったソフトはSketchですがw)

今日はここまでにしたいと思います。
Widgeterで学んだ技術的なことも別の記事にしていこうと思います。

あと、本当に良いものができたと思うので、ぜひWidgetter をインストールしてみてください!

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

jazzyのエラーが解決できない人向けの記事( could not successfully run `xcodebuild`. Please check the build arguments. Failed to generate documentation)

検索でヒットする記事を見ても解決できなかったので備忘録として、そして同じエラーを繰り返さないために記事を残しておきます。
※間違っていたらご指摘お願い致します。

エラー内容

image.png
スクリーンショット 2021-02-28 11.50.59

エラー原因

色々試した結果アプリがビルド可能な状態でないと、jazzyもドキュメント化できないようです。

参考サイト

・メインで役立った記事 https://dev.classmethod.jp/articles/generate-documentation-using-jazzy/
・ドキュメントの書き方 https://qiita.com/Qiita/items/c686397e4a0f4f11683d
・jazzyのインストール方法 https://qiita.com/satoshi-baba-0823/items/826d38bc72230e4b5f6a
・見ておくと役に立つかもしれない記事 https://qiita.com/uhooi/items/d900c2de03e9d9f39b95

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

[翻訳] 初めてのCore Bluetooth

訳者まえがき

この記事は通信SDKを開発する企業Dittoのサイトに寄稿された、Tim Oliver氏による記事です。
英語の記事はこちらでご覧いただけます。


初めに

スマートフォンとタブレットは世の中を大きく変えてきました。2007年に発売されたiPhoneから様々なデバイスが直感的なタッチインターフェースとネットワーク技術を持つようになりました。

iOSの開発者として、私は自分自身がネットワークに精通しているとは思っていません。最近の全てのアプリには当然のようにデータのエンコードやデコードを処理するREST APIのようなものが存在します。

しかし私がこれまで触れてこなかったスマートフォンのネットワーク技術に、Bluetoothがあります。
ワイヤレスイヤホンを着けながらこの記事を書いている時、手首にApple Watchを着けている時、Nintendo Switchで『どうぶつの森』をワイヤレスProコントローラーでプレイしている時、Bluetoothは当然のものの様に感じてしまいます。
意識せずとも動作している魔法のブラックボックスです。

これまでのiOSエンジニアとしてのキャリアの中で、Appleが提供するBluetooth API使うプロジェクトに関わったことはありませんでしたし、サイドプロジェクトでも必要性を感じたことはありませんでした。
なので友人のMax(訳注: Ditto社の共同創業者)がCore Bluetoothの動作を示すためのアプリを作って、そのことについての記事を書いてくれないかと依頼してきた時、このチャンスに飛びつきました。

この記事は私と同じような経験をしてきた人に向けて書きました。Appleのプラットフォーム向けの開発をしたことがあったとしてもCore Bluetooth APIを学ぼうと思わなかった人もいると思います。
Core Bluetoothの parent/child アーキテクチャ、簡単なデータを送受信する方法、そして遭遇したいくつかの落とし穴について書いていきます。

Core Bluetooth とは

Core BluetoothはAppleのパブリックフレームワークであり、サードパーティ製アプリがiOSやiPadOS上のアプリにBluetooth機能を組み込むための唯一の公式な方法です。

元々、Core Bluetoothは「Bluetooth Low Energy (BLE)を抽象化したもの」でした(BLEはBluetoothクラシックとは違い低消費電力デバイスでも動作するような省電力な通信をする技術です)。
つまりこれまでのCore Bluetoothは、心拍計やIoTデバイスのような低消費電力で定期的に小さなデータをブロードキャストするような用途に使われていました。

一方でゲームコントローラやワイヤレスヘッドフォンなどのようなBluetoothクラシックを利用しているデバイス(より高出力で常にデータをストリーミングしている)はサードパーティ製のアプリからアクセスすることができませんでした。

しかしiOS13から、AppleはCore BluetoothでもBluetoothクラシックをカバーできる様に拡張しました。
パブリックAPIはまだ変更されていませんが、より様々なデバイスが利用できる様になったということです。
Core Bluetoothを学ぶにはとても良いタイミングだと思います。

Core Bluetoothの基本概念

Core Bluetoothを利用するには、関連する専門用語とその関係性に慣れておく必要があります。

セントラル(Central)とペリフェラル(Peripheral)

BLE は、非常に伝統的なサーバー/クライアント型のモデルで動作します。つまり一つのデバイスが情報を含むサーバーとして機能し、別のデバイスはこの情報をクエリしてローカルで処理/表示するクライアントとして機能します。

いくつかのデバイスが自分のデータをブロードキャストします。これがペリフェラルです。
他のデバイスはそれをスキャンして接続します。これがセントラルです。
接続された後は通常は、ペリフェラルがサーバー、セントラルがクライアントとして動作します。
これがCore Bluetoothの動作です。

一般的な例として、Bluetoothヘッドホンをスマートフォンにペアリングする場合、スマートフォンがセントラルとなり、ヘッドホンがペリフェラルとなります。

image

サービス(Service)

もちろんBLEデバイスの種類によって、どのような機能を持っているかが決まります。
例えば心拍計は心拍数を記録し、温度計は温度を計測します。

ほとんどのアプリはデバイスの特有の機能をサポートするように構築されると思います。
例えば健康をトラッキングするアプリは温度計のような健康に関係のないデバイスとの接続には興味がないでしょう。
ペリフェラル機器の特有の機能を、Bluetoothでは「サービス」と呼んで扱います。

ペリフェラルは、自身のサービスを定義したアドバタイズメントパケットをブロードキャストすることで、セントラルに見つけられるようにします。
セントラルはスキャン中に、探していたサービスをサポートするペリフェラルからのパケットを見つけると、接続を開始します。
最初はメインのサービスのみ送信されますが、一度接続を構築した後は、セントラルはその他のペリフェラルのサービスも照会することができます。

センターがペリフェラルとの互換性を確認するには、ペリフェラルがサポートするIDを知る必要があります。
特殊なアプリやペリフェラル機器の場合、お互いのUUIDを利用してサービスを定義しても良いでしょう。

しかし一般的には、ペリフェラルがBluetoothの世界的な基準に則るのが自然だと思います。
例えば血圧を記録するデバイスは、どの機器で計測されたデータであってもそのデータを処理することが出来るかもしれません。
このような、特定のユースケースを利用したいセントラルとペリフェラルの間で利用できる、標準化されたサービスIDのデータベースが存在します。

キャラクタリスティック(Characteristic)

一つのサービスには、複数の様々なデータが含まれる場合があります。
例えば、心拍計には心拍数の情報とセンサーの配置の情報が含まれます。

この様にサービスは、様々なデータを計測したり関連した機能を実行するなど、複数の特徴(キャラクタリスティック)を含むことがあります。
これらのデータはペリフェラルから送られることもあれば、ペリフェラルに送り返されることもあります。
セントラルは一つのキャラクタリスティックをクエリしたり、キャラクタリスティックが更新するたびに呼び出されるオブザーバーを登録したりすることができます。

Bluetoothデバイスが自身の機能をサービスやキャラクタリスティックとして提供することを、GATT (Generic Attribute Profile) と呼びます。

Core Bluetoothのコンセプトのまとめ

ここまででCore Bluetoothの基本的な部分が理解できていることを願っています。

親デバイスは「センター」と呼ばれ、「ペリフェラル」と呼ばれる子デバイスに接続します。
ペリフェラルは「サービス」として機能を管理し、サービスは「キャラクタリスティック」として機能を管理します。

実践

Core Bluetoothの基本的な概念を説明したところで、早速実践してみましょう。
2つのデバイスを接続する流れを見せるために、Core Bluetoothを中心としたサンプルアプリを作ってみました。
このアプリは簡易的なチャットアプリで、お互いを接続し、メッセージのデータを送受信します。

このアプリはセントラルとしてスキャンする方法と、ペリフェラルとしてアドバタイズする方法を紹介します。
接続されると、アプリはその後、1つのパイプラインを介して上流と下流の両方にメッセージを送信することができます。

まず初めに

まず何よりも最初に、NSBluetoothAlwaysUsageDescriptionキーをアプリのInfo.plistに追加して、Bluetoothを使用する理由を記述する必要があります。
このキーが存在しない場合、アプリは提出時にApp Storeに拒否されるだけでなく、アプリ自体が例外をスローしてCore Bluetooth APIを呼び出そうとします。

これはAppleのセキュリティ要件であり、すべてのアプリはBluetoothを有効にする前にユーザーから明示的な許可を得なければならないからです。
今回は、チャットサービスを有効にするためにBluetoothが必要であることを説明します。

他のデバイスとメッセージをやりとりするためにBluetoothにアクセスします。

これでCore Bluetoothを使い始めることができます。
Swiftでは、このフレームワークを利用する全てのソースファイルに以下のようなインポート文を記述する必要があります。

import CoreBluetooth

セントラルとしてスキャンする

Bluetooth 接続でセントラルの役割を果たす iOS デバイスは、CBCentralManager と呼ばれるオブジェクトで表現されます。

まず、新しいインスタンスを作成してみましょう。

let centralManager = CBCentralManager(delegate: self, queue: nil)

このように、オブジェクトはインスタンス化時にデリゲートとして指定されなければなりません。
このオブジェクトは CBCentralManagerDelegate に準拠していなければなりません。
セントラルマネージャをインスタンス化すると、直ちにBluetoothに必要なアクティビティが開始されます。

この時点では、まだスキャンを開始することはできません。
Bluetoothが「電源が入った(powered on)」状態になるまでかなりの時間がかかります。
そのため、最初のデリゲートコールバックを待つ必要があります。

func centralManagerDidUpdateState(_ central: CBCentralManager) {
    guard central.state == .poweredOn else { return }
    // ペリフェラルのスキャンを開始する
}

centralManagerDidUpdateState は、システム上のBluetoothの状態が変化するたびに呼ばれます。
Bluetoothがリセットされた時や、アクセスが許可されていない場合にも呼ばれます。

本番環境では全ての状態を適切に処理する必要がありますが、ここではBluetoothがオンになった状態(powered on)のみ検知するようにします。
その状態になればスキャンを開始することができます。

ペリフェラルのスキャンはとても簡単です。
scanForPeripherals を呼び出して、利用したいサービスを指定するだけです。

let service = CBUUID(string: "9f37e282-60b6-42b1-a02f-7341da5e2eba")
centralManager.scanForPeripherals(withServices: [service], options: nil)

上述したように、サービスは一意の識別子を持っているので、ペリフェラルやセンターはそれにマッチする可能性があります。
Core Bluetoothでは、これらの識別子は CBUUID オブジェクトを介して処理されます。ここではシンプルな文字列を使用します。

このチュートリアルでは、オンラインのUUIDジェネレーターから生成されたUUID文字列値を使用しています。
値はグローバルに一意である必要がありますが、ペリフェラル側とセントラル側の両方から認識可能です。

この時点では、同じサービスIDを持つペリフェラルをスキャンしています。
好きなタイミングで centralManager.isScanning を呼ぶことで、スキャンしているかどうかを確認することができます。

ペリフェラルとしてアドバタイズする

セントラルがスキャンしているので、スキャンしているサービスと同じものをアドバタイズする別のペリフェラルのデバイスが必要です。

セントラルが CBCentralManager を介して管理されるのと同様に、ペリフェラルは CBPeripheralManager のインスタンスによって管理されます。

let peripheralManager = CBPeripheralManager(delegate: self, queue: nil)

セントラルマネージャと全く同じように、ペリフェラルマネージャも、作成時にデリゲートを必要とします。(今回は CBPeripheralManagerDelegate
そしてデバイス上のBluetoothの状態が「powered on」になるのを待つ必要があります。

func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
  guard peripheral.state == .poweredOn else { return }
  // Start advertising this device as a peripheral
}

ペリフェラルのBluetoothの状態がオンになると、ペリフェラルは自身のアドバタイズを始めることができます。

let characteristicID = CBUUID(string: "890aa912-c414-440d-88a2-c7f66179589b")

// キャラクタリスティックを作成し、設定する
let characteristic = CBMutableCharacteristic(type: characteristicID,
                          properties: [.write, .notify],
                          value: nil,
                          permissions: .writeable)

// サービスを作成し、そこにキャラクタリスティックを追加する
let serviceID = CBUUID(string: "9f37e282-60b6-42b1-a02f-7341da5e2eba")
let service = CBMutableService(type: serviceID, primary: true)
service.characteristics = [characteristic]

// このサービスをペリフェラルマネージャに登録する
peripheralManager.add(service)

// サービスIDによってサービスを指定し、アドバタイズを開始する
peripheralManager.startAdvertising(
            [CBAdvertisementDataServiceUUIDsKey: [service],
             CBAdvertisementDataLocalNameKey: "Device Information"])

複雑に見えますが、一つずつ見ればそれほど複雑ではありません。

  1. キャラクタリスティックを作成し、セントラルが期待する標準化されたUUIDを設定します。そしてそのキャラクタリスティックをwriteable(書き込み可能)にして、セントラルがデータを送り返せるようにする必要があります。
  2. 標準化されたサービスUUIDを持つサービスオブジェクトを生成し、タイプをプライマリに設定して、このペリフェラルの「メイン」サービスとしてアドバタイズされるようにします。
  3. 同じサービスIDでペリフェラルをアドバタイズします。CBAdvertisementDataLocalNameKey は通常、ペリフェラルのデバイス名を保持しますが、センターが使う追加データ(温度計の現在の温度など)を保持するようにすることもできます。

セントラルからペリフェラルを識別する

ここまでで、片方のデバイスがスキャンし、同じサービスIDでもう片方のデバイスがアドバタイズしているので、お互いを見つけられるはずです。

セントラル側では、ペリフェラルが見つかると以下のデリゲートコールバックが呼ばれます。

func centralManager(_ centralManager: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {

  // `advertisementData` をチェックして、これが正しいデバイスかどうかを判断する

  // このデバイスへの接続を試みる
  centralManager.connect(peripheral, options: nil)

  // ペリフェラルを保持する
  self.peripheral = peripheral
}

didDiscoverPeripheral はペリフェラルに関する多くの情報を提供します。
advertisementData には CBAdvertisementDataServiceUUIDsKey に定義された全てのサービスUUIDに加えて、デバイスの名前やメーカー名などのペリフェラルに関する情報が含まれています。
(他にももっとあるかもしれません)

必要であれば advertisementData[CBAdvertisementDataServiceUUIDsKey] を使って、このペリフェラルがセントラルの要求するサービスをサポートするかをチェックすることもできます。
またRSSI(Received Signal Strength Indicator) は、ペリフェラルとの距離を判定するのに役立ちます。
動作に近い距離であることが要求されることがあり、この値はその監視に使用することができます。

もしこのペリフェラルが接続したいものであれば、 centralManager.connect() を呼び接続を開始することができます。

このペリフェラルオブジェクトにデリゲートの外でアクセスする方法が無いため、クラス内のプロパティに保持しておくと良いと思います。

ペリフェラルへの接続

ペリフェラルを検出して centralManager.connect() を呼び出すと、セントラルはそのペリフェラルに接続しようとします。
接続すると、以下のデリゲートメソッドが呼び出されます。

func centralManager(_ centralManager: CBCentralManager,
                        didConnect peripheral: CBPeripheral) {
  // 接続されたため、スキャンを停止する
  centralManager.stopScan()

  // ペリフェラルのデリゲートを設定する
  peripheral.delegate = self

  // コミュニケーションに利用するチャットのキャラクタリスティックをdiscoverする
  let service = CBUUID(string: "9f37e282-60b6-42b1-a02f-7341da5e2eba")
  peripheral.discoverServices([service])
}

この時点でペリフェラルを扱えるようになり、セントラルマネージャではなく、ペリフェラルオブジェクトを直接操作します。
このペリフェラルオブジェクトは CBPeripheral という型で、CBPeripheralManager とは全く別のものです。

そのためまず最初に行うことは、このペリフェラルから直接イベントを受信できるように、自分自身をこのペリフェラルのデリゲートとして割り当てることです(CBPeripheralDelegateに準拠)。
次にペリフェラルの discoverServices を呼び出すことで、そのペリフェラルがサポートしているサービスを discover し、必要なサービスのキャラクタリクティックにアクセスすることができます。

ペリフェラルのサービス内のキャラクタリスティックを確認する

自分自身をペリフェラルのデリゲートに設定し、そのサービスを確認するためのリクエストを行うと、 CBPeripheralDelegate の以下のメソッドが呼び出されます。

func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
  // もしエラーが起きた場合、接続を遮断して最初からやり直せるようにする
  if let error = error {
    print("サービスが見つかりません: \(error.localizedDescription)")
    cleanUp()
    return
  }

  // 必要なキャラクタリスティックを指定する
  let characteristic = CBUUID("890aa912-c414-440d-88a2-c7f66179589b")

  // サービスが複数ある場合があるため、ループして必要なキャラクタリスティックをdiscoverする
  peripheral.services?.forEach { service in
    peripheral.discoverCharacteristics([characteristic], for: service)
  }
}

この時点でエラーが発生した場合は、接続を処理して終了させる必要があります。
そうでなければ、peripheral オブジェクトには、サポートしているすべてのサービスが登録されることになります。

サービスが CBUUID オブジェクトを介して識別されるのと同じように、キャラクタリスティックも識別されます。
キャラクタリスティックを購読してそのデータを読み込む前に、サービスの中の一つとして discover する必要があります。

ペリフェラルは複数のサービスを持てるため、ループによって必要なキャラクタリスティックを見つける必要があります。
peripheral.services を検索し、特定のキャラクタリスティックIDを見つけ出します。

キャラクタリスティックを購読する

チャットアプリでは、ペリフェラルからのデータの受動的なストリームには興味がなく、データが来たらすぐに通知されるようにしたいです。
そのため、キャラクタリスティックが更新されればすぐに通知されるように設定します。

サービス内のキャラクタリスティックがdiscoverされると、以下のデリゲートコールバックが呼び出されます。

func peripheral(_ peripheral: CBPeripheral,
      didDiscoverCharacteristicsFor service: CBService, error: Error?) {
  // もしエラーが起きた場合、接続を遮断して最初からやり直せるようにする
  if let error = error {
    print("キャラクタリスティックが見つかりません: \(error.localizedDescription)")
    cleanUp()
    return
  }

  // 必要なキャラクタリスティックを指定する
  let characteristicUUID = CBUUID("890aa912-c414-440d-88a2-c7f66179589b")

  // キャラクタリスティックが複数ある場合があるためループする
  service.characteristics?.forEach { characteristic in
    guard characteristic.uuid == characteristicUUID else { return }

    // キャラクタリスティックを購読し、データが来たら通知されるようにする
    peripheral.setNotifyValue(true, for: characteristic)

    // データを送信するために、キャラクタリスティックの参照を保持する
    self.characteristic = characteristic
  }
}

ここでも、何かエラーが起きた場合は適切なエラー処理をおこないます。

service オブジェクトを受け取りましたが、サービス内に複数のキャラクタリスティックが含まれる可能性があるため、ループして必要なものを探しだします。

必要なキャラクタリスティックを見つけたら、peripheral.setNotifyValue()true にして呼び出し、
その中のデータに変更があったら通知を受け取るようにします。

ペリフェラルから、通知が設定されているかを確認する

次にペリフェラルが、セントラルへのキャラクタリスティックの通知の設定が正しく動作しているかを報告します。
成功したか失敗したかに関わらず、以下のデリゲートコールバックが呼び出されます。

func peripheral(_ peripheral: CBPeripheral,
                    didUpdateNotificationStateFor characteristic: CBCharacteristic,
                    error: Error?) {
  // 適切なエラー処理を行う
  // ここでのエラーでは接続自体を破棄する必要はない
  if let error = error {
    print("キャラクタリスティック更新通知エラー: \(error.localizedDescription)")
    return
  }

  // キャラクタリスティックが指定したものであることを確かめる
  guard characteristic.uuid == characteristicUUID else { return }

  // 通知の設定が成功しているかチェックする
  if characteristic.isNotifying {
    print("キャラクタリスティックの通知が開始されている")
  } else {
    print("キャラクタリスティックの通知が止まっています。接続をキャンセルします。")
    centralManager.cancelPeripheralConnection(peripheral)
  }

  // セントラルからペリフェラルに何か情報を送信する
}

必須ではありませんが、サブスクリプションが失敗した場合に再度購読を試みる(必要に応じてクリーンアップコードを実行する)のも良いと思います。
また、もしセントラルにペリフェラルに送信したいが保留中のデータがある場合は、このタイミングで送信するのが良いでしょう。

ペリフェラルにデータを送る

セントラルマネージャがペリフェラルと必要なキャラクタリスティックを見つけ出すことができたら、
このキャラクたりスティックを通じてセントラルはデータを送ることができます。

一つ注意しなければならないのは、このキャラクタリスティックはセントラルで書き込み可能になるように、ペリフェラル側で設定しておく必要があります。

let data = messageString.data(using: .utf8)!
peripheral.writeValue(data, for: characteristic, type: .withResponse)

type引数によって、データを受信したことをペリフェラルに返信させるかどうかを指定します。
これは特定の順序が必要なデータと、頻繁に繰り返されるデータを区別するのに便利で、ペリフェラルが受信できなくても値が失われることがありません。

セントラルにデータを送信する

反対にペリフェラルからセントラルにデータを送る場合も同様に記述します。

let data = messageString.data(using: .utf8)!
peripheralManager.updateValue(data,
        for: characteristic, onSubscribedCentrals: [central])

ペリフェラルからデータを受け取る

ここまででペリフェラルからデータを受信する準備ができました。
キャラクタリスティックに新しいデータが来た際に、以下のデリゲートを利用して通知を受け取ります。

func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
  // 適切なエラー処理を行う
  if let error = error {
    print("キャラクタリスティックの値の更新に失敗しました: \(error.localizedDescription)")
    return
  }

  // キャラクタリスティックから値を取り出す
  guard let data = characteristic.value else { return }

  // デコード/パース処理を行う
  let message = String(decoding: data, as: UTF8.self)
}

セントラルからデータを受け取る

最後に、ペリフェラルがセントラルからデータを受け取ると、 CBPeripheralManager の以下のメソッドが呼び出されます。

func peripheralManager(_ peripheral: CBPeripheralManager,
                      didReceiveWrite requests: [CBATTRequest]) {
  guard let request = requests.first, let data = request.value else { return }
  let message = String(decoding: data, as: UTF8.self)
}

特に注意しなければならないことは、キャラクタリスティックの作成時に書き込み可能(writeable)に設定されていなかった場合、
ペリフェラルへのデータ送信は静かに失敗し、このデリゲートは決して呼び出されないということです。

まとめ

ここまでを経て、Core Bluetoothでデータを送受信するには、かなりのステップ数が必要になることが分かります。

ペリフェラル側では、サービスとキャラクたりスティックを設定してアドバタイズし、セントラルが購読/購読中止をした場合に管理する必要があります。
セントラル側では、ペリフェラルをスキャンし、接続し、サービスを検索し、キャラクタリスティックを検索し、そして購読する必要があります。

とはいえ、用語と、それぞれのオブジェクトがどのように連鎖するかを理解すれば、比較的簡単に上手くいきます。

チャットアプリのデザインパターン

上記のCore Bluetooth APIの紹介ではセントラルとペリフェラルを接続するための基本的な手順を示していますが、
チャットアプリを作成する際、「誰がセントラルで誰がペリフェラルなのか」という大きな疑問にぶつかります。

低消費電力のセンサーがiPhoneに接続されているよくあるユースケースでは、セントラルとペリフェラルの役割は明確です。
しかし、2台のiPhoneが接続されている場合、誰がどちらの役割を果たすのかという問題は、突然、はるかに難しくなります。

最初の時点では理解していないかもしれないこととして、Core Bluetoothではデバイスは同時にセントラルにもペリフェラルにもなれるということがあります。
デバイスはスキャンしながらアドバタイズも同時に行うことができます。

アプリが起動してデバイス検索画面が開かれた時、セントラルマネージャとペリフェラルマネージャの両方が作成され、スキャンとアドバタイズを同時に開始します。

範囲内にあるデバイスも同じことをしています。このことによって、検出した他のデバイスをこちらの画面に表示できます。
同様に、他のデバイスもこちらのデバイスを検出して表示します。

デバイス検索画面ではチャット可能な全ての端末を検出しますが、チャット画面に入ると選択したデバイスからのメッセージのみを受信したいと考えます。

このケースではユーザーがデバイスを選択すると、そのデバイスのUUIDが保存され、チャット画面に渡されます。
両方のデバイスで同じチャット画面に入ると、どちらのデバイスも通常のセントラルとしてセットされ、まだ接続は行われません。
デバイス検索画面でアドバタイズしているデバイスを検知しないよう、別のサービスIDをここでの接続に使用します。

どちらかがメッセージを送信するまで、両方の端末はスキャンを続けます。
メッセージが送信すると、その端末はペリフェラルとなり自身をアドバタイズし始めます。
それを検知した他方のデバイスは、セントラルとしてペリフェラルと接続します。
最初にメッセージを送った人がペリフェラルになり、もう一人がセントラルのままで、一つの接続を共有します。
通信の間、デバイスUUIDは接続相手を識別するために使われ、アドバタイズを開始した他のセッションの誰かが誤って入ってこないことを保証します。

このやり方は少し変だと感じるかもしれません。
より実用的には、二つの接続を作成し維持する方が合理的かもしれません。
しかしその場合、途切れる可能性のある接続が2つ存在することになり、安定性が低くなる可能性があります。

技術に関しての考察

ここまでCore Bluetooth APIとそのデザインパターンについて説明してきました。
どのように動かせば良いかを理解するのは難しくないと思います。

それはなぜかというと、Core Bluetoothを動かすための最低限の部分のみ見てきたためです。
これは本番アプリには絶対的に不十分であるということです

Ditto社では、メインのプロダクトでCore Bluetoothを使い、さらにAndroidでのBluetooth Low Energyもサポートしています。
このプロジェクトで私が経験した課題や制限と、Dittoのエンジニアが直面した課題をいくつか紹介します。

メッセージのデータ容量の制限

私が全く理解していなかったことの一つは、キャラクタリスティックを通じて送信できるデータ量はかなり限られており、その制限はデバイスによって違うということです。
元々は20バイトのみでしたが、最近のスマートフォンでは180バイトあります。
メッセージあたりのデータ量の少ないチャットアプリではそれほど気になりませんが、本番アプリでは問題になる可能性があるでしょう。

Core Bluetoothでは各メッセージの許容可能な長さを検出することができます。
それ以上の長さを送信したい場合は、データを分割して複数のメッセージとして送信することになりますが、
その実装は独自で行う必要があります。

速度の制限

GATTを通じた通信の最大速度は、1秒あたり数キロバイトしかありません。
チャットアプリでは問題ありませんが、大きなアプリではこれがボトルネックになる可能性があります。
ユースケースによっては、メッセージのデータ量を最適化する必要が出てくるかもしれません。

安全な送信方法での更なる遅延

.withResponse を指定してペリフェラルが受信したことを保証する場合、この往復の動作が更なる遅延を発生させます。
速度を重視するユースケースでは、この方法を使わずに送信し、独自のエラー修正ロジックを実装するべきです。

プラットフォームごとの制御レベルの違い

Core Bluetoothの独自のポリシーが、Androidなどの他のデバイス上のBLEの実装とは噛み合わない可能性があります。
例として、Core Bluetoothがペリフェラルのアドバタイズメントパケットに含められるデータ量や種類に制限をかけていることです。
そのため、Androidでも同様のアプリを開発しようとしている場合、同じように動作するように注意を払う必要があります。

バックグラウンド起動でのセキュリティ/プライバシーポリシー

通常のBluetoothは画面の操作にかかわらず動作しますが、AppleはBLEを採用しているアプリに対して、厳しいプライバシーポリシーを課しています。
ペリフェラルデバイスのアプリがバックグラウンドになると、アドバタイズは続きますが、"Local Name"プロパティは含まれなくなります。
さらに、バックグラウンドになったセンターは、範囲内のどのペリフェラルからも、継続したアドバタイズメントを受け取らなくなります。

この制限は、新型コロナウイルス接触確認アプリをCore Bluetoothを使って実装しようとしていた組織にとって、大きな争点になりました。

使いこなすにはとても複雑なAPI

慣れれば作業が簡単になるのは確かですが、Core Bluetoothはすぐに使いこなせるほど簡単なフレームワークではありません。
データを送受信し、必要なデータを取れるようになるには、とても長いプロセスを辿る必要があります。

さらに、このステップはコールバックを経由して順番に行われます。
自分のユースケースに必要なプロセスを考え出すのは非常に時間がかかり、高い認知負荷が必要になると思います。

かなりしっかりとしたエラー処理が必要

デリゲートコールバックの処理のどの時点でも、プロセス全体を失敗させることが簡単に起こりえます。
ワイヤレス技術であるBluetoothは干渉に弱く、接続が途切れやすいです。
そのため、処理のどの段階でも発生しうる問題に対処するため、確実なエラー処理が必要になります。

予期したコールバックが発生しなかった場合に備えて、ハートビート(定期的にノード間で送信されるメッセージ)や状態チェックを行う必要がある可能性もあります。

不安定さと変な挙動

Core Bluetoothはすでにかなり古いものですが、たまに発生する変な動作は確実に残っています。

  • 送信キューが容量いっぱいになった場合、クリアしたというコールバックがスキップされることがあります。このため、定期的にキューの状態をテストする必要があります。
  • 特定のデバイス(iPad Mini 4やiPhone 6のような)は、ロックされた後にロックを解除すると、スキャンを誤って停止してしまう可能性があります。

暗号化が不足

Bluetoothで送信するデータの中には個人情報(健康記録など)が含まれる場合があるため、暗号化は常に強く推奨されています。
BLEにも暗号化はありますが、信頼性は高くありません。
そのため、暗号化レイヤーとそれに伴う(エラー修正など)の実装を全て独自で行う必要があるかもしれません。

まとめ

最初にチャットアプリが動作し、タイプした文字が別の端末に表示された時、魔法のようだと感じました。
この記事を書くためにCore Bluetoothを学んだのはとても有意義な時間でした。
読んでいただいた方にとっても有益であれば嬉しいです。

しかしながら、最後に一つはっきりとさせておきたいことがあります。
独自のCore Bluetoothの実装をするのはとても大変です
かなり多くのステップがあり、ユーザーの体験のどの部分においても上手くいかない可能性が大いにあります。

もしあなたがCore Bluetoothを調査しているエンジニアで、ローカル通信を実装したプロダクトを作ろうとしているのであれば、
Dittoの同期技術をチェックしてみることをお勧めします。
Dittoの技術スタックは上記の課題を全て解決しており、アプリに通信を実装することを簡単にしてくれます。

読んでいただきありがとうございました!

サンプルアプリのGithubリポジトリ

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

[iOS 14] XCUITestでアプリを削除する

アプリの初回起動時のみに表示される画面をテストする必要があったので、 XCUITest でアプリを削除する方法について調べました。

WEB上にいくつか参考になる記事がありましたが、iOS 14のアプリ削除フローに対応した実装方法が見当たらなかったので内容をまとめてみました。

iOS 14でのアプリ削除フロー

iOS 14では以下のようにアプリを削除します

アプリアイコン長押し コンテキストメニュー 削除確認アラート1 削除確認アラート2
Simulator Screen Shot - iPhone 11 Pro - 2021-03-03 at 15.05.15.png Simulator Screen Shot - iPhone 11 Pro - 2021-03-03 at 15.05.19.png Simulator Screen Shot - iPhone 11 Pro - 2021-03-03 at 15.05.23.png Simulator Screen Shot - iPhone 11 Pro - 2021-03-03 at 15.05.27.png
ホーム画面上で
削除するアプリのアイコンを長押し
コンテキストメニューで
アプリの削除を選択
ホーム画面からの削除確認アラートで
アプリの削除を選択
アプリ削除確認アラートで
アプリの削除を選択

ポイントは、アプリを削除(アンインストール)するための確認アラート(上記の 削除確認アラート2 )が表示される前に、アプリの情報を保持しつつホーム画面から削除するための確認アラート(上記の 削除確認アラート1 )が表示される点です。

なお、コンテキストメニューが表示されてもアプリアイコンの長押しをしたままにすると、各アプリアイコンの左上に削除を示すマイナス:no_entry:ボタンが表示されますが、このフローだと上手く削除できない場合があることが分かったので、今回はコンテキストメニューでアプリの削除を選択するフローを採用しました。

実装

上記を踏まえて、アプリを削除するプログラムを書いていきましょう。
今回は SpringBoard 1 を使ってアプリの削除フローをSwiftで記述します。

struct Springboard {
    private static let myAppName = "削除するアプリ名"
    private static let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")

    static func deleteApp(name: String = myAppName) {
        XCUIApplication().terminate()

        springboard.activate()

        XCUIDevice.shared.orientation = UIDeviceOrientation.portrait
        Thread.sleep(forTimeInterval: 2.0)

        let appIcon = springboard.icons[name]
        guard appIcon.exists else {
            return
        }
        appIcon.press(forDuration: 1.5)

        let preferredLanguageCode = NSLocale.preferredLanguages[0].prefix(2)

        // コンテキストメニュー
        let firstDeleteButtonText = preferredLanguageCode == "ja" ? "Appを削除" : "Remove App"
        springboard.buttons[firstDeleteButtonText].firstMatch.tap()
        Thread.sleep(forTimeInterval: 1.0)

        // 削除確認アラート1
        let secondDeleteButtonText = preferredLanguageCode == "ja" ? "Appを削除" : "Delete App"
        springboard.buttons[secondDeleteButtonText].firstMatch.tap()
        Thread.sleep(forTimeInterval: 1.0)

        // 削除確認アラート2
        let thirdDeleteButtonText = preferredLanguageCode == "ja" ? "削除" : "Delete"
        springboard.buttons[thirdDeleteButtonText].firstMatch.tap()

        Thread.sleep(forTimeInterval: 0.5)

        XCUIDevice.shared.press(.home)
    }
}

確認アラートの文言などはシミュレーターの言語設定によって変わるので、必要に応じて切り分けます。
英語の RemoveDelete で表記ゆれしているのは地味にハマりどころかもしれません :sweat_smile:

これをUIテストのアプリ起動前に呼び出せば、毎回クリーンな状態でテストを実行できます :tada:

import XCTest

class MyAppUITests: XCTestCase {
    override func setUp() {
        super.setUp()

        // In UI tests it is usually best to stop immediately when a failure occurs.
        continueAfterFailure = false

        // インスタンスメソッド `setUp()` で呼び出せば、各テストメソッドの実行前にアプリが削除される
        Springboard.deleteApp()

        // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
        XCUIApplication().launch()

        // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
    }

...

}

別フロー

コンテキストメニューでアプリの削除を選択せずにアプリアイコンを長押ししつづけていると、アプリアイコンたちがプルプルしはじめ、各アプリアイコンの左上には削除を示すマイナス:no_entry:ボタンが表示されます。

このフローでもアプリを削除できますが、環境やタイミングによっては想定外のアラートなどに邪魔されてしまうことがありました。

アプリアイコン長押し継続 確認アラート 想定外のアラート
コンテキストメニューが表示されても
無視して押し続ける
削除確認アラートが表示されることもあるが ホーム画面編集についてのお知らせアラートなどに
邪魔されてしまうこともある

もちろんこれらを適切にハンドリングできればテストは可能だと思いますが、今回は採用を見送りました。

参考

XCUITestでアプリを削除する


  1. iOSのホーム画面やそれに付随した機能を管理するためのソフトウェア 

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

[Bitrise+fastlane]2FA必須化をさくっと解決する

2FA(2ファクタ認証)が必須に

すべてのロールで2FA必須

2019年にAccount Holderでの2FAが必須になりましたが、ロールに関係なく2FAが必須になったようです。
https://developer.apple.com/jp/support/authentication/
2021年3月以降、App Store Connectにサインインするすべてのユーザーは、2ファクタ認証または2ステップ確認を行う必要があります。

これまでのBitriseでの対応方法

これまではAccount Holder権限以外のアカウントを使用していて、2FAを無効にしてBitriseに設定していました。
しかし、Account Holder以外も2FA必須化の対象になった影響で、本番リリース用のWorkflowにも影響が発生して、2FAエラーで失敗するようになっていました。

最初に出たエラーはこのようになっていて、非常に分かりづらかったです。

Need to acknowledge to Apple's Apple ID and Privacy statement. Please manually log into https://appleid.apple.com (or https://appstoreconnect.apple.com) to acknowledge the statement.

同じようなエラーで困っている方の参考になれば幸いです。

前提条件

  • Bitriseを使用している。
  • BitriseのWorkflowでfastlaneステップを使用している。
  • fastlaneの本番申請用の設定ではpilotを利用している。(Test Flightにアップロードして、App Store Connectで手動でビルドを選択する方法)
  • fastlane ver 2.176.0

このような前提条件ですが、やり方やCIが違っても設定などは参考になる部分があると思います。
調べた感じだと色々とやり方があるようです。
FASTLANE_SESSIONなどはセッション切れのタイミングで再度対応する必要がある部分があります。
App Store Connect APIを使用したやり方は一度設定してしまえば変更の必要がないので現状(2021年3月現在)だと一番良いかなと感じています。(個人の感想です)

対応内容

実はタイトルのようにサクッと解決したわけではありません。
いろんな記事を見て、激闘の末になんとか解決しています。
ただ、実際の対応内容はそんなに大したことはありません。

App Store Connect APIを利用する

App Store Connect APIを利用すれば2FAを気にする必要はありません。

App Store Connect APIキーを発行する

App Store Connect APIキーを発行済みの方は飛ばしてください。
まず、Admin以上の権限でApp Store Connectにアクセスしてください。
キーを発行するにはAdmin以上の権限が必要になるからです。
「ユーザとアクセス」から「キー」をクリックします。

スクリーンショット 2021-03-03 0.07.24.png

そうすると、こちらの画面が表示されます。
+ボタンを押してApp Store Connect APIキーを発行しましょう。
(初めての場合は「APIキーを生成」ボタンが表示されていると思います。)

モーダルが表示されるので、名前とアクセス権限を設定します。
「名前」は識別するためなので好きな名前をつけてください。
私はBitriseで使用することが分かるような名前にしています。
「アクセス」ではアクセス権限を設定します。
プルダウンからBitriseで実行するアップルディベロッパーアカウントの権限に合わせて設定しましょう。
スクリーンショット 2021-03-03 14.14.52.png

App Store Connect APIキーをダウンロード

キーの発行に成功すると、「APIキーをダウンロード」するボタンがありますのでそこをクリックして、キーファイルをダウンロードします。
このファイルは1度しかダウンロードできません。
ダウンロードしたらきちんと保管してください。

Bitrise側の設定

APIキーを発行し終えたら今度はBitrise側の設定に移ります。
先程のキーを発行した画面にある「Issuer ID」、「キーID」、「ダウンロードしたキーファイル」を使用します。

スクリーンショット 2021-03-03 0.07.24.png

Bitriseの環境変数を設定

Workflowエディタから環境変数を設定します。
私の環境では、下記のように設定しています。
キー名は自分自身の好きなように決めてください。

APPLE_API_KEY_ISSUER_ID => Issuer ID
APPLE_API_KEY => キーID
APPLE_API_KEY_CONTENT => ダウンロードしたキーファイルの中身

スクリーンショット 2021-03-03 14.26.56.png

ダウンロードしたキーファイルの中身は、テキストエディタなどで開いてコピペしてください。
このような内容になっているはずです。

-----BEGIN PRIVATE KEY-----
...........................
-----END PRIVATE KEY-----

Fastfileの編集

次はfastlane側の設定です。

編集前

編集前のFastfileはこのようになっていました。
(公開用に一部情報を編集しています)
pilotというのを利用しています。
pilotはTestFlightにバイナリをアップロードするために使っています。
TestFlightにバイナリをアップロードしたら、手動でビルドを選択してAppleに申請をかけています。

desc "Upload to AppStore"
lane :release_appstore do
  slack(message: ":man-running: start upload to AppStore")
  gym(
    workspace: WORK_SPACE,
    configuration: RELEASE,
    scheme: RELEASE,
    silent: true,
    clean: true,
    output_name: "xxx.ipa",
    export_method: "app-store",
  )

  pilot(
    skip_submission: true,
    skip_waiting_for_build_processing: true
  )

  slack(message: ":tada: successfully uploaded to AppStore")
end 

編集後

api_keyにapp_store_connect_api_keyの設定をしています。
「key_id」、「issuer_id」、「key_content」に先程Bitrise側で設定した環境変数を記載します。

desc "Upload to AppStore"
lane :release_appstore do
  slack(message: ":man-running: start upload to AppStore")
  gym(
    workspace: WORK_SPACE,
    configuration: RELEASE,
    scheme: RELEASE,
    silent: true,
    clean: true,
    output_name: "xxx.ipa",
    export_method: "app-store",
  )

  # App Store Connect APIキーを設定
  api_key = app_store_connect_api_key(
    key_id: ENV['APPLE_API_KEY'],
    issuer_id: ENV['APPLE_API_KEY_ISSUER_ID'],
    key_content: ENV['APPLE_API_KEY_CONTENT'],
  )

  pilot(
    # APIキーを設定
    api_key: api_key,
    skip_submission: true,
    skip_waiting_for_build_processing: true
  )

  slack(message: ":tada: successfully uploaded to AppStore")
end 

Workflow実行

ここまで完了したらあとは本番リリース用のWorkflowを実行するだけです。
私の環境では、これで無事に成功することができました。

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