くま's Tech系Blog

基本的には技術で学んだことを書き留めようと思います。雑談もやるかもね!

iOSDC Japan 2021 に参加した

iOSDC Japan 2021 に参加してきたので、ブログにしようと思います

iosdc.jp

iOSDC Japanとは?

iOSDC Japan 2021はiOS関連技術をコアのテーマとしたソフトウェア技術者のためのオンラインカンファレンスです。 日本中、世界中から公募された100本を超える知的好奇心を刺激するトークの他にも、パンフレットに掲載された技術記事、参加者であれば誰でも作れる即興のトーク・アンカンファレンスなど、初心者から上級者まで楽しめるコンテンツがみなさんを待っています!

今年、iOS15が発表されたり、Swift5.5が発表されたりしたので最新情報をまとめたものを発表したり、自分の会社の説明やこういう実装の仕方をしていますというのを共有したものが多かった印象です

自分にとっては自社以外の開発が知れたり他の人の考え方が知れて驚きが多く、モチベーションが上がる機会でした

また、オンラインで開催されるのが個人的にはよかったと思っています

2〜3日前に今年初めて都内に行ったくらいなので、会場に行くより自宅で参加できるのは大きかったです。オフラインで開催される流れになってもオンラインは残ってほしいと思いました

ask the speakerというシステムがあり、スピーカーに質問できることやDiscordで交流ができるのでオンラインでも皆で楽しめるのでオフライン寄りのオンラインイベントだと感じました

そして、オンライン開催ならではなのが、見逃たものを後で見れることです

ニコニコ生放送で開催されたのですが、見逃したり見たいものが同じ時間に開催されても後から視聴可能です。仕事で見れないセッションがあったり、見たいものがかぶったりしたのでありがたいです

ここからは個人的に今年の良かったセッションを少しまとめてみようと思います

noteのiOSアプリで実装したアクセシビリティの全て

fortee.jp

noteでのアクセシビリティ向上のために実施してきた内容を紹介するセッションでした

アクセシビリティに力を入れるのはなかなか難しいものがある中で、単純にすごいと思いました

ただ、セッションを聞いていく中でアクセシビリティが良くないのははバグなんだと思うようになり、アクセシビリティに力を入れるのはハードルが高い対応ではなく、当たり前の対応なんだと思えるようになりました

バックグラウンドでアプリがキルされても怖くない!アプリの状態を元に戻すリストア機能の全て

fortee.jp

アプリがキルした後にリストアする方法を紹介するセッションでした

リストア対応を以前行ったときにはすごく大変だったイメージがあったのですが、今回のセッションでとてもシンプルに対応できるなあと感じました

正直、もっと前に知りたかったです

ケースに応じたUICollectionViewのレイアウト実装パターン

fortee.jp

UICollectionViewのレイアウト実装をパターン別に紹介するセッションでした

UICollectionViewはほとんどのアプリで使われる、なおかつFlowLayoutやCompotisionLayoutなどいろんな方法がありどの方法がいいのか悩むことがあったので、今回のセッションである程度使い分けができそうな印象でした

ちょうど、UICollectionViewのレイアウト実装で色々調べていたというのもあり、理解が深まりました

最後に

主催者・スポンサー・発表者・参加者などいろんな人が1つのカンファレンスを楽しめるイベントだったと思いました

来年も開催されると思っているので、来年はよりカンファレンスに参加できたらいいと思いました

Callkitで着信画面を表示させる

今回はCallkitについてです

Callkitは下記のようなiPhone標準の着信画面を表示させる機能です。画面がOFFの状態であれば、スライドして応答する画面が表示されます。また、着信履歴にもデータが残ります

SkypeやLINEなどで電話をかけたときに、着信時に同じ画面が表示されていると思います。それはCallKitというiOSの公式フレームワークを使っています。

着信側の端末はPushKitと呼ばれるライブラリによってPush通知を受信し、これをトリガーにCallKitと呼ばれるライブラリを利用して着信画面を表示します。

iOS13以降からVoIP Push (PushKit)は着信通知専用に仕様変更されたことにより、PushKitとCallKit はセットで使うことが必須となりました。

ただ、単純に該当のメソッドを呼び出せばいいわけではありません

正しい順番を守って呼び出さないと、システム側がPushKitの呼び出しに失敗したと判断され、着信が来なくなることがあります

着信画面を表示させるためには以下の手順を踏む必要があります

  1. PushKitからVoIPのPushを受け取る
  2. CallKit 着信画面を表示
  3. reportNewIncomingCallのcompletionHandler内部でPushKit のcompletionHandlerを呼び出す

PushKitの設定

まずはPushKitで通知を送る設定が必要になります

VoIPのPushを受け取るためには、基本的にはAPNS通知を行うときと同じですが、Apple DeveloperでCertificateを作るときにVoIP用のチェックボックスがあるので、チェックして証明書を作成します

そして、Xcodeの設定で、Signinig&Capabilityの下記にチェックを入れましょう

また、ペイロードの設定などに注意しましょう。下記に記載されていますが、Sandboxの場合に届かなかったりするので注意が必要です。

developer.apple.com

ここまで設定するとPushKitの通知が送れるはずです。

PushKitの通知を受け取る処理は下記になります

extension AppDelegate {
    
    private func initVoIP() {
        var voipRegistry = PKPushRegistry(queue: .main)
        voipRegistry.delegate = self
        voipRegistry.desiredPushTypes = [.voIP]
    }
    
}

extension AppDelegate: PKPushRegistryDelegate {

    // PushKitからVoIPPush用のトークンを受け取る
    func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
    }
    
    func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type{
}

    // VoIPPushを受け取ったときの処理
    func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
        // 通知の形式が .voIP の場合に後続の処理を行う
        guard type == .voIP else {
            completion()
            return
        }
        
        let userInfo = payload.dictionaryPayload
        
        // Callkitを表示させる処理を行う
    }
}

PKPushRegistryDelegateでPushKitからVoIPPushを受け取り、Callkitを表示させる処理を行います

基本的に上記の処理はAppDelegateで行います

ここまでで着信のPush通知が届くので、次に着信画面を表示させる流れになります

Callkitの設定

次に着信画面を表示させるCallkitの処理を行います

まずはCallkitの初期化を行います。今回はシングルトンで作成しています

import CallKit

final class CallKitManager {
    
    private(set) var callKitProvider: CXProvider!
    private(set) var callKitController = CXCallController()
    var uuid = UUID()
    
    static let shared = CallKitManager()
    
    private init() {
        let configuration = CXProviderConfiguration(localizedName: "TEST".localize())
        configuration.maximumCallGroups = 1
        // 最大通話人数
        configuration.maximumCallsPerCallGroup = 1
        // 扱うタイプ
        providerConfiguration.supportedHandleTypes = [.phoneNumber] 
        // 着信音の設定
        configuration.ringtoneSound = "〇〇.mp3"
        callKitProvider = CXProvider(configuration: configuration)
    }
}

CXCallControllerは通話の管理インタフェースを提供しているクラスです。

developer.apple.com

CXProviderConfigurationは挙動や表示を制御するためのプロパティです

developer.apple.com

localizedNameは着信で表示されるアプリ名を設定します

次に着信処理を行います。

// 通話相手の名前設定
let callHandle = CXHandle(type: .generic, value: "TEST君")

let callUpdate = CXCallUpdate()
callUpdate.remoteHandle = callHandle
// 他の着信が来たら受け取るかどうか
callUpdate.supportsHolding = false
// グループ通話を行うかどうか
callUpdate.supportsGrouping = false
// ビデオ通話を行うかどうか
callUpdate.hasVideo = false

provider.setDelegate(self, queue: .main)
 provider.reportNewIncomingCall(with: CallKitManager.shared.uuid, update: callUpdate, completion: {_ in})

CXProviderクラスのreportNewIncomingCallが着信画面を表示するAPIです。

CXHandle のインスタンスに発信者の情報をセットします

電話番号やメールアドレスなどを指定すると、標準の連絡先アプリに登録されている情報を元に発信者名の表示や、画像が登録されていれば着信画面に表示されます

また、reportNewIncomingCallの先頭の引数に通話毎にユニークな uuid を渡しています

着信・発信・拒否・保留等の通話における各アクションの挙動はCXProviderDelegateのメソッドとして定義します(Callkitを表示させるViewcontrollerで行います)

extension CallKitViewController: CXProviderDelegate {
    // リセット時
    func providerDidReset(_ provider: CXProvider) {
        action.fail()
    }
    
    // 通話終了・着信を拒否した時
    func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
        action.fulfill(withDateEnded: Date())
    }
    
    // タイムアウト時
    func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {        
        action.fulfill()
    }
    
    // 発信時
    func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
    }
    
    // 着信を許可した時
    func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
        action.fulfill()
    }
}

action.fulfill()は着信を受け取ったり拒否したりのアクションを終了させる処理です

通話発信や終了などを行う場合

通話発信や終了などを行う場合にはCXTransactionにアクションのインスタンスを登録してリクエストする必要があります

developer.apple.com

let endCallAction = CXEndCallAction(call: CallKitManager.shared.uuid)
 let transaction = CXTransaction(action: endCallAction)

CallKitManager.shared.callKitController.request(transaction) { error in
      if let _ = error {
          CallKitManager.shared.callKitProvider.reportCall(with: CallKitManager.shared.uuid, endedAt: nil, reason: .remoteEnded)
      }
}

上記は通話終了のアクションです

iOSでは標準の電話アプリの通話も含めてCXCallControllerで管理されるため、インスタンスを経由して発信や終話の状態をOSに通知する必要があります

OSに通知するというのはrequest(transaction)で行います

通話を開始する場合には上記のCXEndCallActionCXStartCallActionに変更します

まとめ

最低限しか記載していませんが、通話アプリの着信画面ではこんなことが行われているという大枠は理解できると思います

今回記載したこと以外に、着信履歴からリダイアルなど標準の電話とほとんど同じ機能が実現できるはずなので試してみてください!!

なぜ?突然Githubにpushできなくなった

いつも通りGitHubにpushしようとしたら、突然エラーが発生するようになりました。

fatal: unable to access 'https://github.com/ユーザー名/リポジトリ名.git/': The requested URL returned error: 403

リポジトリ名は合っているし、なんだろう?

ssh接続しているので、GitHubにアップしている公開鍵が何らかの問題を起こしているのかと思い、見てみたのですが、特に問題なし

えー???と思いながら調べてみるとpush時のエラーに下記のようなメッセージがありました

remote: Please see https://github.blog/2020-07-30-token-authentication-
requirements-for-api-and-git-operations/ for more information.

リンク先を見てみると、今まではローカルでGitHubにアクセスするにパスワード認証を使用していたのですが、脆弱性防止のために個人アクセストークン認証に変更になったそうです

なので、パスワードではなく、個人アクセストークンに変更する必要があります。(メールアドレスとユーザー名はそのまま使います)

個人アクセストークンは下記に作成方法が記載されています。

docs.github.com

個人アクセストークンが表示されたページは再表示はできないのでメモしてください

プッシュ時のパスワードに個人アクセストークンを入力するとpushできるはずです

1度プッシュすると今後は必要ないと思いますが、登録されているかを確認するのはキーチェーンアクセスでできます

キーチェーンアクセスでgithub.comと検索するとgithub.comという名前で表示されるはずです

もしパスワードが更新されていないのであれば、1度登録されている認証情報を削除して登録すれば問題なと思います

SourceTreeを使っている場合には下記記事にある対応も必要になります。すごくわかりやすいので参考になると思います。

zenn.dev

UIViewのレイアウトライフサイクルについて

今回はUIViewの表示についてまとめようと思います。

AutoLayoutが効かない・Viewの表示位置がおかしい・表示されない・画面表示までが遅いことが実装している中で1度は経験はあると思います。

このような問題は、Viewの表示の流れを理解せずに開発していることが原因かもしれません。

UIViewのレイアウトライフサイクルは下記の順で行われます。

  1. 制約の更新
  2. フレームの更新
  3. レンダリング

それでは、1つずつ見ていきましょう!

制約の更新

以下の条件で、Viewの制約が変更されるとUIViewクラスのupdateConstraints()が呼び出され制約の再計算が行われます。

  • 制約のactiveフラグの有効・無効の切り替え
  • 制約の優先度の変更
  • 制約の追加・削除
  • 制約が与えられたviewの階層の変更

updateConstraints()はシステム側がレイアウトパフォーマンスを考慮したタイミングで呼び出すので、意図的に呼び出してはいけません。

ただし、パフォーマンスを大きく損なうような制約の更新などの場合は updateConstraint() 内で行なった方がいいです。

これは、updateConstraint() がレイアウトののバッチ処理を行いメインスレッドをブロックしないためです。

使い方としては、UIViewのサブクラスでメソッドをoverrideして使用します。

意図的に制約の計算を行いたい場合には、下記のメソッドを使いましょう。

  • updateConstraintsIfNeeded()
  • setNeedsUpdateConstraints()

2つの違いはsetNeedsUpdateConstraints() は指定された ViewとそのSubviews がシステムのタイミングによって updateConstraint() を呼ぶようになり、updateConstraintsIfNeeded() は更新を即座に行います。

ただ、この2つのメソッドは、制約の更新は行われるが次に説明するフレームの更新の実行は保証されているわけではないです。

フレームの更新

制約が更新されるとlayoutSubviews()が呼び出され、フレームが更新されます。

フレームの更新というのはあるViewのサイズに合わせてSubViewやlayerの配置を更新することです。

layoutSubviews()が呼び出されるとupdateConstraintsIfNeeded()も呼び出され、必要であれば制約も更新されます。

ただし、制約の更新と同じようにlayoutSubviews()は意図的に呼び出してはいけません。

意図的に更新したい場合には、下記のメソッドを使います。

  • setNeedsLayout()
  • layoutIfNeeded()

2つの違いはsetNeedsLayout()は 次の再描画が行われる際に更新されるのに対し、layoutIfNeeded()は更新を即座に行います。

setNeedsLayout()は更新リクエストのフラグをONにするイメージです。

なので、明示的にフレームの更新を行った後に何らかの処理を実行する場合はlayoutIfNeeded()を使用し、それ以外はsetNeedsLayout()を使ったほうがパフォーマンスが良いと言われています。

レンダリング

フレーム情報が更新されると画面に表示されるためにレンダリングが行われ、draw(rect:)が呼び出されます。また、以下の条件で、draw(rect:)が呼び出され再描画を行います。

  • Viewの追加・削除
  • 非表示になっていたViewの再表示
  • Viewを画面外までスクロールし、再び画面内に戻す
  • ViewのsetNeedsDisplay(), setNeedsDisplayInRect(_:)の明示的な呼び出し

ここまでUIViewが描画されるまでの流れを書いていきました。もう少し細かいところまで調べて追記していきたいと思います。

参照

developer.apple.com

developer.apple.com

developer.apple.com

developer.apple.com

qiita.com

スコープ関数について

kotlinにはスコープ関数というものがあります。

あるインスタンスに対して、連続して処理をするような場合、スコープ関数を使えばコードを簡潔にすることができます。(Lamba形式で処理をおこないます)

スコープ関数にはletrunwithapplyalsoの5種類あります。

それぞれ説明しようと思います。

let

まずはletについてです。

letは個人的に1番使っているので、最初に説明します。

どんなときに使うかというと、nullチェックで行うことが多いです。

val test = "test"

val str = test.let {
    it.toUpperCase()
}

print(str)

上記のprintlnで表示されるのはstrがnullではない場合のみです。また、itはtestを表すレシーバーです。

Lambaの引数はitでitはスコープ関数で使わないといけません。また、戻り値として利用できます。

run

runはletとほとんど変わらないですが、レシーバーをthisにする必要があります。

val test = "test"

val str = test.run {
    this.toUpperCase()
}

print(str)

ただ、runはExtensionとして使用するパターンとExtensionとして使用しないパターンの両方で使えます。

val hexNumberRegex = run {
    val digits = "0-9"
    val hexDigits = "A-Fa-f"
    val sign = "+-"

    Regex("[$sign]?[$digits$hexDigits]+")
}

for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) {
    println(match.value)
}

上記はT.runのように使わずにrun {}というように使っています。

なので、runはオペレーション(操作)だけでなく、初期化のためにも使えます。

with

withに関して、働きはrunと同じです。そのため、withはあまり見かけることは無いかもしれません。

特徴としては、Extensionとして使用しないパターンです。

withは通常の関数のように呼び出し、2つの引数を取ります。第一引数にレシーバ、第二引数にラムダ式を記述しましょう。レシーバーはthisです。

レシーバーのthisは省略できます。

val test = "test"

val str = with(test) {
    this.toUpperCase()
}

print(str)

apply

ここからはlet・run・withとは考え方は同じですが、用途が異なります。

applyはプロパティを設定するために使用します。

val book = Book().apply {
    price = 827
    author = "AAAAA"
}
println(book.author)

上記のコードはBookインスタンスを生成したらプロパティを設定するコードです。

apply関数のラムダ式の中では、レシーバ(ここではBookインスタンス)をthisで参照することもできますが、このthisは省略することが可能です。

戻り値はプロパティが設定されたレシーバになります。(ここではBookインスタンス)

also

alsoはapplyと似た使い方ですが、明確に違う点があります。

alsoの中のラムダ式で、Bookインスタンスは「it」で参照されていて、省略不可です。

val book = Book().also {
    it.price = 827
    it.author = "AAAAA"
}

上記のitは省略できません。

まとめ

ここまでスコープ関数を説明しましたが、全てを使うことはほとんどないかもしれません。下記にまとめますが、用途に応じてしっくりくるものを使ってみてください。

レシーバー 戻り値 用途
let it 指定可能 オペレーション
run this 指定可能 オペレーション
with this 指定可能 オペレーション
apply this 対象オブジェクト プロパティ指定
also it 対象オブジェクト プロパティ指定

追記

レシーバが何を表しているかをきちんと理解しないとなぜ?と思うことが発生するかもしれません

例えば、Fagementでダイアログを表示する際を例に挙げます

private fun showCustomDialog() {
    CustomDialog().apply {
        arguments = Bundle().apply {
            putString("title", requireContext().getString(R.string.title_notifications))
        }
    }.show(parentFragmentManager, "customDialog")
}

上記の例はFragmentの中でDialogFragmentを継承したCustomDialogを表示させる処理です

この例だとExceptionが発生します。なぜでしょうか?requireContext()でcontextが取得できないからです

requireContext()の処理を参照すると、Context context = this.getContext();と行い、contextを取得していますが、thisはapply関数なので、CustomDialogになります。 そして、CustomDialogはアタッチされていないためcontextがnullになりExceptionが発生します。 なので、次のようにする必要があります(今回はHomeFragmentというフラグメントのクラスで行われている)

private fun showCustomDialog() {
    CustomDialog().apply {
        arguments = Bundle().apply {
            putString("title", this@HomeFragment.getString(R.string.title_notifications))
        }
    }.show(parentFragmentManager, "customDialog")
}

もしくはお勧めではないですが、CustomDialog().applyの部分をCustomDialog().alsoに変えてもクラッシュしません

参照

kotlinlang.org

pouhon.net

Androidのスレッドについて

今回はAndroidのプロセスやスレッドについてまとめようと思います。

最近は非同期処理でCoroutine などが出てきたおかげで非同期処理が簡単にできるようになりました。

ただ、Coroutine などを理解する前にプロセスやスレッドについて理解する必要があると思い、簡単にまとめようと思います。

スレッドについて

Androidは、処理を行うためのベルトコンベア(スレッド)が1本しか存在しないシングルスレッドモデルです。

なので、重い処理が走った場合はアプリが止まってしまう可能性があります。

それを回避するために、一時的に別のベルトコンベア(サブスレッド)で処理が出来る仕組みが用意されています。

流れとしては、アプリの起動時にアプリ実行用のスレッドを作成します。

これは、メインスレッド(UI スレッド)と呼ばれていて、イベント(描画イベントを含む)を適切なユーザー インターフェースウィジェットに送信する役割を担うため非常に重要です。

また、これはほとんどの場合、アプリが Android UIツールキットのコンポーネント(ViewやButtonなど)とやり取りをするスレッドでもあります。

つまり、UI更新などやボタンのクリックなどUIの操作はUI スレッドで行う必要があり、重い処理(APIなど)を行う場合にはワーカースレッドで行う必要があります。

また、Android UI ツールキットはスレッドセーフではありません。

したがって、ワーカー スレッド(サブスレッド)からUIを操作するべきではありません。

ユーザー インターフェースに対するすべての操作は、UI スレッドから行う必要があります。そのため、Android のシングル スレッド モデルには下記のルールがあります。

  • UI スレッドをブロックしない
  • UI スレッド以外から Android UIツールキットにアクセスしない

下記がイメージです。ワーカースレッドはUIスレッドとは別の処理のコンベアのようなイメージです。

f:id:kumaskun:20210725195236p:plain

スレッドの作成

次はどうやってスレッドを作成するかについてです。

スレッドの作成については何パターンかあるので、説明したいと思います。

①handler

Handler は Android が提供するライブラリです。

別スレッドからアプリの UI を操作するのにLooperHandlerというしくみを使うことができます。

LooperはUIスレッドにあらかじめ用意されています。また、Looper は処理対象のリスト(キュー)を持っていて、そこに登録されたスレッドを順番に実行します。

Looperのリスト(キュー)への登録など、LooperとのやりとりはHandlerを使って行います。

f:id:kumaskun:20210725212019p:plain

実際のソースコードは下記です。

// Handlerのオブジェクト作成
val handler = Handler(Looper.getMainLooper())

// 別スレッドを作成
val runnable = object : Runnable {
    override fun run() {
        // 別スレッドで行う処理
    }
}

// 別スレッドを実行
handler.post(runnable)

Runnableは別スレッドを作成するインターフェイスで、run()のなかで別スレッドで行う処理を定義します。

そして、別スレッドで処理を行ったら、Handlerを介して、UIスレッドで処理を行います。

val handler = Handler(Looper.getMainLooper())となっているのはLooperはスレッドごとにあり、UIスレッドのLooperに属しているHandlerであることを明示しています。

また、別スレッドの結果をUIスレッドで更新するのは下記runOnUiThreadで行えます。

runOnUiThread {
    // UIスレッドで行う処理
}

②AsyncTask

Handlerとは別にAsyncTaskという非同期処理用クラスがあります。

使い方は下記のようになります。

class TestAsyncTask : AsyncTask<Void, Int, Void>() {

      // 別スレッドの処理を行う前にUIスレッドで処理を行う場合
        override fun onPreExecute() {
        }

      // 別スレッドの処理
        override fun doInBackground(vararg param: Void?): Void? {
        }


      // 別スレッドの処理が終わり、UIスレッドで行う場合
        override fun onPostExecute(result: Void?) {
        }

    }

上記のようなAsyncTaskクラスを作成して、インスタンス化して呼び出して、execute()を実行することで行えます。(TestAsyncTask().execute()など)

また、上記のAsyncTask<Void, Int, Void>のVoid, Int, Voidの部分はParamsProgressResultと定義されており、それぞれ下記を意味するので、必要に応じて変更します。

Params→別スレッドの処理に渡すパラメータ(doInBackgroundに渡される型)

Progress→進捗状況を更新する処理に渡される値の型

Result→別スレッドの処理の結果の返り値

AsyncTaskはAPI30からは非推奨となっているので、今後使うことはあまりないと思います。

2つのパターンを例に出しましたが、あまり使うことがないかもしれません。

例えば、APIの処理を行う場合にはokHttpやRetrofitなどはライブラリが勝手にワーカースレッドで処理を行うなどしてくれたり、RxjavaやCoroutineなどあまり意識せず、UIスレッドで処理を行ったりできるためです。

まとめ

AndroidではUIスレッドとワーカースレッドがあり、意識して実装しないと思いもよらない例外が発生してアプリがクラッシュすることがあります。

最近はライブラリが進歩してスレッドをあまり意識せずに開発ができるようになりました。

ただ、想定外のことが起きたときに概念を知っておくのと知らないのでは解決までのプロセスに差ができると思っているので、概念の理解の一助になると幸いです。

参照

developer.android.com

developer.android.com

engineer-club.jp

kommkett.co.jp

Androidでビルドのターゲット分けを行う

今回は、Androidでビルドのターゲット分けを行うようにするにはどうしたらいいのかを書いていきます。

ビルドのターゲット分けというのは開発環境と本番環境など向き先を変えるとうことです。

例えば、開発環境でapkを出力したり、本番環境で出力したりと変更できるようになります。

 ちなみに、iOSで同様なことを行う場合には下記を参照してみてください

kumaskun.hatenablog.com

Androidでターゲットを分ける場合にはプロダクトフレーバーを利用します。

プロダクトフレーバーに関しては、こちらに記載されています。

developer.android.com

では、実際にどうやってプロダクトに追加するか見ていきましょう!

プロダクトフレーバーの設定

プロダクトで設定をする前だとAPKを出力する際には下記のようになっていると思います。

f:id:kumaskun:20210711101839p:plain

実際の設定は下記です。build.gradle(app)で行います。

android {
    flavorDimensions "test"

    defaultConfig {
        applicationId "com.example.myapp"
    }

    productFlavors {
        development {
            dimension "test"
            applicationIdSuffix ".development"
        }

        production {
            dimension "test"
        }
    }
}

公式ドキュメントには下記のように記載があります。

プロダクト フレーバーの作成方法はビルドタイプと同じで、ビルド構成の productFlavorsブロックにプロダクトフレーバーを追加して必要な設定を含めます。defaultConfig が実際には ProductFlavorクラスに属しているため、プロダクト フレーバーでは defaultConfig と同じプロパティがサポートされます。

なので、flavorDimensionsで設定した名前をproductFlavorsdimensionで設定します。

productFlavors配下には開発用と本番用ということでdevelopmentproductionを作成しています。

developmentの方にはapplicationIdSuffixという値が設定されていると思います。

applicationIdSuffixを追加することによってapplicationIdを変更させることができます。

今回の場合だと、開発環境のapplicationIdは「com.example.myapp.development」となります。

プロダクトの指定する内容によって変更してください。

ここまで指定して、ビルドもしくはsyncが成功するとAPKを出力する際には下記のようになると思います。

f:id:kumaskun:20210711103646p:plain

今回追加したdevelopmentとproductionそれぞれで元々あったDebug、Releaseが選択できるようになったと思います。

では、ビルドタイプによって設定値を変えたりする場合にはどうしたらいいでしょうか?

ビルドタイプによって定義を変える

ここでの定義を変えるというのは例えば、APIやWEB画面のURLを変えたりなど、設定値を環境によって変えるということです。

まず、Android Studioでファイル構成をプロジェクトに変えましょう。

そしたら、app配下にproductFlavorsで設定した名前でフォルダを作成します。今回の場合はdevelopmentとproductionでフォルダを作成します。

次に作成したフォルダそれぞれにjavaフォルダを作成して、javaフォルダ配下はapplicationIdの値でフォルダを作成します。

f:id:kumaskun:20210711105307p:plain

それぞれ、applicationIdの値でフォルダを作成したらその配下にConstantファイルなど定義を設定したファイルを作成します。

設定したファイルの値を呼び出せるようになると思うので、それで完成です。

結構シンプルにターゲット分けが行えるので試してみてください

Custom URL Schemeでアプリを起動する

今回はCustom URL Schemeでアプリを起動する方法を書いていきたいと思います。

iOSでは、 cm-app:// のような、アプリ固有のCustom URL Schemeを実装することで、 リンクをタップしてアプリを起動することができます。( ディープリンクとも言います)

ディープリンクを使用すると、スキーム(上記の例ではcm-app)でアプリを識別して起動できます。

また、//( スラッシュ)以降のURL部分でアプリに対して情報を受け渡すことや、Webページからだけでなくアプリ間の遷移にも応用できます。

設定

まずはCustom URL Schemeの設定方法からです。下記、2つの方法があります。

  • info.plistから設定する方法
  • URL Typesから設定する方法

どちらで設定しても同じ結果になります。

今回は、cm-testというスキームを追加します。

①info.plistから設定

まずは、info.plistから設定する方法です。

info.plistに下記のように追加します。

URL Schemesには今回追加するcm-testを設定します。(複数指定できます)

URL identifireにはユニークな識別文字列を設定します。今回はBundle Identifierを設定しました。

②URL Typesから設定

次に、URL Typesから設定する方法です。

TARGET > info > URL Types の+で追加します。

設定後は下記のようになります。

これで、アプリを入れている端末のブラウザで cm-test:// と入力するとアプリを起動するか求められると思います。

起動後の流れ

では、アプリを起動するか求められた後に許可した場合にはどういう流れになるのかみていきたいと思います。

起動したらAppDelegate内の open url メソッドが実行されます。

func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
    print(url)
    print(url.scheme)
    print(url.host)
    print(url.path)
    print(url.query)

    return true
}

上記printしているのはURLのデータが渡ってきているかの確認のためです。

下記に詳細を記載します。(URLはcm-test://hoge/huga?userId=1を仮定します)

  • scheme -> cm-test
  • host -> hoge
  • path -> huga
  • query -> userId=1

このように値が渡ってくるため、渡ってきた情報を使って処理を行うことがほとんどだと思います。

さらに、open url メソッド内で画面遷移の処理を行えば、画面遷移できます。(rootViewなどに気を付けてください)

補足

アプリがインストールされているか知りたい場合にはcanOpenURLメソッドを使用します。

if UIApplication.shared.canOpenURL(URL(string: "アプリに設定してあるスキーム://")!) {
    // アプリがインストールされている
} else {
    // アプリがインストールされていない
}

ただし、info.plistのLSApplicationQueriesSchemesスキーマを設定する必要があります。 LSApplicationQueriesSchemesのTypeをArrayにしてvalueにアプリのスキームを設定してください。

また、アプリがインストールされていない場合にアプリの設定画面に遷移するようにすると思いますが、次のように対象のアップルIDを設定することで アップルストアに遷移することができます。

guard let url = URL( string: "itms-apps://itunes.apple.com/app/id{AppleID}" ) else { return }
UIApplication.shared.openURL( url )

そして、対象のアプリを起動したい場合にはカスタムスキーマを設定した状態でopenURLを実行すると起動できます。

guard let url = URL(string: "xxx://") else { return }
UIApplication.shared.openURL(url)

参照

[Swift] カスタムURLスキームによるアプリ間連携

qiita.com