20201215のiOSに関する記事は19件です。

CoreGraphicsのあれこれ

UIImage+CoreGraphics.swift
import UIKit

extension UIImage {

  //準備
  private func preprare(size: CGSize, _ draw: (() -> Void)? = nil) -> UIImage! {
    UIGraphicsBeginImageContextWithOptions(size, true, 0)

    draw?()

    let newImage = UIGraphicsGetImageFromCurrentImageContext()

    UIGraphicsEndImageContext()

    return newImage
  }

  func resize(ratio: CGFloat) -> UIImage! {
    let size = CGSize(width: self.size.width * ratio, height: self.size.height * ratio)
    return self.resize(size: size)
  }

  func resize(height: CGFloat) -> UIImage! {
    let width = self.size.width * height / self.size.height
    let size = CGSize(width: width, height: height)
    return self.resize(size: size)
  }

  func resize(width: CGFloat) -> UIImage! {
    let height = self.size.height * width / self.size.width
    let size = CGSize(width: width, height: height)
    return self.resize(size: size)
  }

  func resize(size: CGSize) -> UIImage! {
    preprare(size: size) {
      self.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
    }
  }

  func compose(image: UIImage, frame: CGRect) -> UIImage! {
    preprare(size: self.size) {
      self.draw(at: .zero)
      image.draw(in: frame)
    }
  }

  func compose(image: UIImage, atCenter: CGPoint, blendMode: CGBlendMode = .copy, alpha: CGFloat = 1.0) -> UIImage! {
    preprare(size: self.size) {
      self.draw(at: .zero)
      let at = CGPoint(x: atCenter.x - image.size.width/2, y: atCenter.y - image.size.height/2)
      image.draw(at: at, blendMode: blendMode, alpha: alpha)
    }
  }

  func compose(image: UIImage, at: CGPoint, blendMode: CGBlendMode = .copy, alpha: CGFloat = 1.0) -> UIImage! {
    preprare(size: self.size) {
      self.draw(at: .zero)
      image.draw(at: at, blendMode: blendMode, alpha: alpha)
    }
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【楽しい個人開発】FlutterでiOSアプリを作るときに通った全工程

この記事は Flutter #2 Advent Calendar 2020 の17日目の記事です :oden: おでんの美味しい季節ですね!

はじめに

こんにちは、すぎっと (@sugitlab) です。
私は京都にある、とある老舗メーカーのエンジニアとして働きながら、趣味でプログラミングにいそしんでいます。今回は、Flutter製のiOSアプリ『 やるひゃく 』の制作過程で通った全工程をまとめて共有しようと思います。
やるひゃくAppStore
AppStoreはこちらから

やるひゃくは、私が毎年ノートに書いていた『やりたいことリスト100』をアプリにしたものです。これまでは、やりたいことリストを作るときはどうしてもタスク管理のようになってしまい、やったかどうかの振り返りは結局年末までほったらかしになっていました。しかし、やりたいことリストとして自ら掲げた事はもっと大事にした方がいいんじゃないか? とふと考えたことをきっかけに、やりたいことリストをただのタスクとして管理するのではなく大切な思い出として記録できるアプリを作ろうと決意しました。
もうひとつのきっかけは、友人と取り組んでいるエンジニアサークルの活動でFlutterを触ってみて、大層ハマってしまったことにあります。これについては以前、Zennの記事に書いています。愛が溢れております。

さて、このアプリ やるひゃく は、11月5日に作りはじめて、38日かかってAppStoreへの初回申請をしました。Flutter歴も半年もない私ですが、本業と育児の合間を縫いながらでもこの日数で初回リリースができたのは、Flutterの凄さだなぁと感じています。ログをTwitterに毎日あげていたので、よろしければご覧ください。

前置きが長くなってしまい申し訳ありません。このアプリを作る時に通った全工程について紹介します。

アプリ制作の全工程

この記事ではソースコードを実際にお見せしながらの説明はしていません。長くなるからです。ですが、とても参考になる他の記事を参照したり、また後日別の記事にして行こうと思います。

  • 前提:
    • iOSだけ対応します: iPhoneしか持っていないからです:sob:
    • AppleDeveloperProgramに入っておきましょう
    • 英語を頑張って読みましょう(Google、DeepLに頼ってでも英語記事を読みましょう)

1. 作りたいものの大まかなイメージを描く

まずは作りたいもののイメージを固めていきます。お絵描きしてもいいし、AdobeXDやFigmaなどを使って本格的に書いてもいいでしょう。この辺りはFlutterに限らず同じです。

Flutterに関して言えば、まずは公式のページでデザインのサンプルを見ることができます。ここでWidgetでUIを組むという事はどういうことかがなんとなく見えてくると思います。
その次はYouTubeを見ることをお勧めします。Flutterは Widget と呼ばれる部品を適所に組み込んで仕上げていきます。じゃあ、どんなWidgetがあるのかを知らないとなかなか作るイメージが湧かないですよね。料理をするにも材料が目の前に並んでた方がレシピが浮かびやすいのと同じです。ジャガイモと玉ねぎとにんじんがあったらカレーか肉じゃが作りますよね。それと同じです。(??

Widgetoftheweek
Flutter Widget of the Week

これを一通り見てみてください。
こんなWidgetがあるならこんな画面が作れそうだな、というイメージをしながら見てみるといいと思います。説明は英語ですが、日本語字幕もありますし、説明自体あまり難しいことはしゃべっていないので、無音でも十分楽しめます。

2. StatelessWidgetで全てのUIを組んでモックを作ってしまう

完成の絵をコードに落とし込んでいきますが、まず全部StatelessWidgetで組んでしまって大丈夫です。
Flutterの勉強を始めるとまず最初にStatelessWidgetとStatefulWidgetが出てきて、状態がどうのこうのというのがあります。もっと深掘りするとElementの話も出てきたりします。-> この辺りを詳しく勉強しておきたいなら、monoさんのMediumの記事が最高です。

さて、UIのモックを作る上ではStatelessWidgetだけで走り切ることができますと言いましたが、これはなぜかというと、StatelessWidgetに後から状態を乗せてStatefulWidgetにする なんていうことが簡単にできます。Flutterは良くできています。

実際に触れるようになると、ここのボタンはもっと上がいい、アニメーションつけた方がいい、そんな改善がたくさん見えてきます。とにかく 実機にインストールするところまではStatelessWidgetだけで進める のがオススメです。

ここが個人的には一番難しいポイントだと思います。教本や学習サイトなどをみても、自分でアプリを作る時の参考にするにはシンプルすぎるものが使われているか、完成度が高すぎるものが使われているかのどちらかが多いように思います。なんか上手く作れないな〜にハマるか作り込みすぎてしまうかのどちらかに陥りやすいように感じました。

ここで私のつくった やるひゃく の Ver.0.1 を、こんなもんでええんやな〜 と思えるように公開しておきます。

mock01

ここでの目的はUIパーツのコア部品を選定する事だ、という程度にするように心がけてつくりました。あと、ひこにゃんかわいい٩( ᐛ )و

3. これなら使えるなという感覚を得るまでモックを改良する

とにかく実機上で触ります。ここで感じた違和感はこの時点で潰しておいたほうが良いと思います。状態管理やデータ構造にまで手が及ぶと流石のFlutterでも変更は骨が折れます。

UI周りの検討は オブジェクト指向UIデザイン: 通称OOUI本 を参考に進めました。この本ではUIの考え方の大原則を説明してくれていますが、オシャレなものの作り方を説明するものではありません。比較的抽象的な部分、つまりは最高に使いやすいアプリのモックを作るために必要な考え方 を提供してくれていると私は理解しています。

同じように、ソシオメディアさんのヒューマンインターフェースガイドラインを見ておくとたくさんの気づきが得られます。
やるひゃく でその全てを遵守できている自信はありませんが、大事なところは押さえていると思っています。

とにかく触って、身近な友達や家族にも恥ずかしがらずに見てもらいましょう。

4. この辺りでプロジェクトの構成をちゃんとしておく

私はFlutterのプロジェクトを flutter create してからこんな勢いで走り抜けてしまったので整理整頓をすっかり忘れていました。

  • libの下にたくさんファイルが並んでしまっていませんか?
  • ひとつのDartファイルに大量のWidgetが並んでいませんか?
  • ひとつのWidgetに大量のchildがぶら下がっていませんか?

Flutterのフォルダ構成はあまり日本語の良い記事が見つかりませんでした。私はこちらの英語のMedium記事を参考にしました。

ここまでは UIに関するWidgetしか作っていないと想定します。それなら話は早いです。まず、View とか UI みたいな名前のフォルダをlibの下に作ってください。
そして、main.dart 以外をすべて投げ込んでください。

次に、1つのdartファイルが巨大になっている場合、複数のStatelessWidgetに分離できないか チャレンジしてみてください。再利用性とかそういう難しいことは後からでもなんとかなるので、一覧性の良いようにしてみてください。

もし、複数のWidgetから共通して参照されるような dartファイルができていたら、widgets といった名前のフォルダを作成し、その中に投げ込んでください。

/lib
  |- main.dart
  |- /view
      |- cool.dart
      |- fabulous.dart
      |- /widgets
          |- good.dart

あと、なんとなく色々な人のサンプルをみて気づいたのでそうしているのですが、dartファイルには英大文字を入れないようにしています。camelCaseやPascalCaseにはせず、settings_view.dart のようにアンダースコアで繋いでいます。
公式かどこかで案内されているのかなぁ・・・見落としているかもしれません。

5. サンプルのデータをアプリ全体に流す(状態管理を実装する)

まず、アプリで使用するデータを書き出します。書き出したデータの親子関係を考えたり、後の機能追加を考慮して設計します。ここはエンジニアのセンスというか、個性が出るところかなぁと思います。Flutterに限らず必要な工程ですね。

次がポイントとなる 状態管理 です。
アプリにデータを流していくためには状態管理の仕組みを考える必要があります。状態管理を正しく詳しく説明することは自信がないので、大まかに説明します。

Flutterの状態管理は大まかに言えば、おおきなツリー状になっているWidgetの関係において、色々な場所で使用される共通のメモリデータ(状態)を上手く参照できるようにするための仕組みです。巨大なツリーの各ノードに対してバケツリレーでデータを流すよりも、どこかで一元管理して上手い仕組みで特定のノードに流すのが便利だよね、というものです。

これを理解するにはinheritedWidgetから始めるといいと思っています。Widgetツリーとcontextを意識することができるからです。

image.png
Flutter公式から借用

さて、Flutterでは状態管理の仕組みがいくつもあります。私は何かをおすすめできるほど網羅して使った経験がないので、単に私が使ったものを紹介します。状態管理の比較はたくさんの良記事があるのでぜひそちらをご覧ください。私が参考にしたものです。

私はRiverpodを使いました。RiverpodはProviderと呼ばれる状態管理パッケージを開発した方が、もっといいものをという事でまた新しく作られたものです。こんなものをどんどん作れるってほんとすごいと思います。ちなみに、Riverpodの文字を並び替えるとProviderになるのもシャレが効いていていいですね!

加えて、Riverpod公式がおすすめしていたので私は hooks_riverpod を組み合わせて使用しました。本業の方でReact.jsを使っていて、そこでもReact Hooksをよく使うので、手に馴染みがあったからです。

Riverpodの実装方法については、Riverpod公式のGettingStartedとmonoさんの記事がオススメです。monoさんの記事ではHooksを使われていないですが、Riverpodのイメージは同じなのでとても参考になりました。

最初のステップで全てStatelessWidgetで作成していたと思います。RiverpodとHooksをつかえば、データを受け取りたいStatelessWidgetをどんどんHookWidgetに変えていき、useProvider()でデータを受け取るだけです。

6. データを永続化する

データの永続化とはつまり保存です。保存先は大きく分けて2つです。

  • iPhoneの中に保存する
  • インターネットを介してどこかのサーバーに置く

個人開発の場合は後者はFirebaseなどのクラウドサービスを使うことが一般的ですね。自前でサーバーを立てている人には出会ったことがありません。

私は前者を選びました。やるひゃくはSNS的な要素や、ユーザー間のデータのやり取りなどは想定していないからです。もちろん、個人利用でもクラウドを使う事で、スマホを買い換えたり、iPhoneとiPadで連携させたりということができるようになります。この目的であればいつか対応してもいいなぁと考えています。もしクラウドを使うなら Flutter は Firebase がベストな選択肢だと思っています。Firebase公式もそう言っています。まぁ、どちらもGoogle様ですので・・・。

さて、iPhoneに保存する方法にもいくつかあります。Flutterの公式では2つ案内されていますね。

  • sqflite
  • shared_preferences

私はsqfliteを選びました。やるひゃくはやりたいことリストを作るアプリです。たくさんの項目を登録することを想定しています。データベースではクエリを使って簡単にデータの取り出しができるので便利です。

とは言いましたが、世の中のアプリはだいたい何らかのデータベースを使っていると思いますのでそれに習っています。また先ほど書きましたが、いつかはクラウドにデータを載せてもいいと思っているのもひとつの理由です。なにかと移行しやすいですからね。

7. UIをブラッシュアップする

とにかくたくさん触って、試して、親しみを持ってもらえるようなデザイン を目指しました。正直、このデザインに関しては間違いないサクセスケースというものは無いのかなぁと思う一方で、ここを押さえておいたら割といい感じにまとまるぞ、というものもあると思っています。

私はたくさんのアプリをインストールしてみて、これから良いデザインを考えるぞ〜というモチベーションで見ることにしました。普段何気なく触っていては気づかないようなことに気づくことができました。

例えばiOSの最近のアプリのほとんどに共通することがあります。

  • ベタ塗りカラーの直線的なAppBarはあまり使われていない
  • AppBarの色はだいたい背景色と合わせてある
  • elevation(立体感のあるシャドウ)もかなり控えめ

などです。この辺りの私の気づきについては、Zennの方で記事を書いてみましたので参考にしてみてください。

8. アイコンを作る

デザインがまとまったらあとはリリースに向かってまっしぐらです。アイコンを作りましょう。
Flutterでアイコンを作る場合、ひとつのサイズを作っておけば一括で色々なサイズの画像に変換してくれるパッケージ があります。
これ、とっっっってもありがたいですよね。以前Unityで遊んでいた時のアプリは全てのサイズのアイコンを自前で用意していたので本当にありがたい。結構な種類が必要ですから・・・。

そのパッケージがこちらです。

私はアイコンの作成はiPadを使っています。VectornaterとProcreateを活用しました。Procreateでラフ絵を描き、Vectornaterでトレースしてvectorに変換し、出力しています。4〜5年前に趣味でWebページとか作っていたときはAdobeのイラレとペンタブなんかを使っていたのですが、ApplePencilの登場によって不要になりました。もちろん素人レベルのことしかしないからです。ちょっとしたアイコン作りにはイラレは贅沢すぎでしたね。

9. ライセンス情報やポリシーの用意

ライセンス情報ではアプリで使っているパッケージを掲載するのですが、なんとFlutterには 利用したパッケージ情報の一覧ページを自動で作ってくれる機能 があります。それがこちらです。

どちらも同じような機能です。AboutDialogを出した上で、ライセンスページに飛ぶか。ライセンスページをいきなり出すか。それくらいの違いです。

次に、AppStoreに用意しなければならないポリシー情報ですが、Notionを使うと簡単です。ポリシーに限らずちょっとしたWebページの代わりに使えるのでかなり便利です。

私は普段から情報はNotionに集約しているので、Notionは私の第二の脳みそです。超有能な外部ストレージです。

Notionでページを作った後、Shere to Webの機能を使って公開リンクを作成すればそれでおしまいです。なんとまぁ素晴らしいエコシステムですこと。そしてまたこれも無料です。Notionが出た当初は無料枠には保存の上限があったのですが、最近無くなりました。信じられません。私はAPIを使用していろいろ遊んでいるので、引き続きサブスク契約していますが、それでも安く感じます。

ちなみに、やるひゃくのページはこんなふうに作ってあります。
↓ こんな感じ
キャプチャ

ここで使用しているなんだかおしゃれ風な画像ですが、フリーです。商用フリーです。こういうちょっとしたところに使いやすいフリー素材も紹介しておきます。

10. 申請する

申請方法はいろいろな説明があるので割愛します。

ここでは、アプリストアの紹介画像にこだわったぞという紹介をさせていただきます。AppStoreですが、検索すると各アプリの画面キャプチャを含んだ説明画像が並ぶと思います。多くの場合、三枚の縦長画像か、一枚の横長画像が使用されていますね。

  • 縦3枚の例: GoogleMap

googlemap

  • 横1枚の例: Google スプレッドシート

spread

そんな中、いくつかのアプリでは複数枚の縦長画像を組み合わせて1つの画像のように見せるテクニックが使われていることに気づきました。

  • 複数枚を組み合わせた例: Google Chrome

chrome

そこで私も三枚全部は難しかったので、二枚で1つの画像になるようにつくりました。
これも、iPadでVectornaterとProcreateを使いました。本当に便利です。

やるひゃくStore

以上の工程を経て、無事アプリをリリースできました。
今後、Flutterでアプリを作ってみたいな、という方の参考になれば幸いです。

長文にお付き合いいただきありがとうございました。

例として(?)紹介させていただいた個人開発したアプリ やるひゃく を使って、2021年のやりたいことリストの1つに「Flutterアプリを作る」を登録して、チャレンジしてみてはいかがでしょうか?

(最後の最後で宣伝、失礼しました)

ありがとうございました。

すぎっと٩( ᐛ )و

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

ランダム化されたMACアドレスは社内のネットワーク認証においてはどう扱えるのか。[Cisco Identity Services Engineの利用例]

ランダム MAC アドレス

iOS 14 や Android 端末で MAC アドレスのランダム化する機能が出てきました。自宅のルータなんかで MAC アドレスによるアクセス制限をされている方たちの「なぜかつながらなくなった!」「原因がわかるまで大変だった」「ランダム化は切ったわ」というような声がちらほらとネットでも見受けられました。

Use private Wi-Fi addresses in iOS 14, iPadOS 14, and watchOS 7
https://support.apple.com/en-us/HT211227
Android Open Source Project > Develop > Connectivity > Privacy: MAC Randomization
https://source.android.com/devices/tech/connect/wifi-mac-randomization

家庭や公衆 Wi-Fi 等ではプライバシーに配慮した機能として需要がある機能だとは思うのですが、MAC アドレスは端末の識別子として広く使われています。ですので、ご自宅のネットワーク環境だけでなく職場でも同様の苦労されている方がいらっしゃるんではないかと、ちょっと注目しておりました。MAC アドレスでデバイスやユーザを識別する方法というのは一般的なものなので、この機能が端末側で有効になると管理者さん的にはきついじゃないかなあと。

機種ごとに MAC アドレスのランダム化の実装はバリエーションがあるものの、一応このランダム化されたMACアドレスとは言え、フォーマットが IEEE で定められています。よって、それがランダム化された MAC アドレスなのかどうか、は少なくともシステマチックに判断できます。
image.png
この図に描いたように MAC アドレスの先頭3オクテットが OUI で、その8ビットのうちの下2桁のところで判断できます。具体的にはこの b1 が1でかつ b0 が0のアドレスがランダム化されたものとなります。ということは最初の1オクテットが2,6,A,Eのどれかで終わるものがそれです。

ランダム MAC の無効化はエンドユーザにオフロードしたいです…。

こうした MAC アドレスを持ったデバイスを使っているそのエンドユーザは正当なユーザであって、登録したデバイスを利用しているのだとしたら、これを識別して、ネットワークに接続させる際の判断基準に採用できるようになるといいかなと思います。そして組織のネットワークに接続させるときにランダム化されたMACアドレスを持ったものを禁止するとか、制限付きのネットワークに入れるとかいうことも考えられますし、正規の端末を使っているのであればランダム化を切って接続しなおすようにガイドを出すというのが一番な気がしています。
とはいえその作業について管理者がいちいち面倒を見ていては業務が崩壊します。Windows端末にしてもスマホにしても、エンドユーザ側で設定変更ができるのでそうしてもらえるように一度準備しておくのが現実的かつ理想的だと思います。
例えば Windows 10 だと Wi-Fi 設定で一つクリックするだけです。
Windows 10 でのランダム MAC アドレスの設定
image.png
image.png

Cisco Identity Services Engine で ランダム MAC アドレスに対応する

Cisco Identity Services Engine(ISE) という高機能な認証サーバがあるのですが、それを使って認証フローを作るとしたら以下の様なやり方があります。
参考までにこの記事で載せているISEのバージョンは3.0です。
1. 業務用の SSID にランダム MAC を利用した端末で接続してくるとインストラクションにリダイレクト
2. そうでないものは通常の Dot1x 認証へ渡す

まずはランダム MAC を使った端末が仮でストアされる内部 DB グループを作成します。 Administration > Identity Management > Groups
に進み Endpoint Identity Group に新しく入れ物を作ります。
image.png
次に Work CentersGuest Portals を選び Create を選択し新たなポータルを作成します。これがユーザに「ランダム MAC やめなさい」的なインストラクションを見せるポータルになります。

image.png
選ぶのは Hotspot Guest Portal です。
image.png
ポータルの名前を適当に決めてて、そのままスクロールダウンしていきます。
image.png
Endpoint Identity Groupで先ほど作った Random_MAC_Endpoints を選びます。
image.png
そうしたらまた上の方にスクロールアップして戻ります。一旦ここで Save します。
image.png
Portal Page Cusomization というタブを選び、せっかくなので日本語のポータルを編集していく例にしてみましょう。
image.png

Global Page CustomizationsText Elements で適当な文章にバナーを変えます。

image.png

スクロールダウンしAcceptable Use Policy を選択し、ブラウザページタイトルや Content Title を適宜埋めます。
image.png

Instructional Text で Toggle HTML Source を押してから以下のスクリプトを記述し、それができたらまた Toggle HTML Source ボタンを押し戻します。
image.png
そうするとボタンが消えます。

<script>
(function(){
jQuery('.cisco-ise-aup-text').hide();
jQuery('.cisco-ise-aup-controls').hide();
setTimeout(function(){ jQuery('#portal-session-timeout-popup-screen, #portal-session-timeout-popup-popup, #portal-session-timeout-popup').remove(); }, 100);
})();
</script>

同じテキストボックスの中にランダムMACアドレスを無効化する方法を記述してユーザに案内することも併せて行うとよいと思います。
スクロールアップして Save します。

ランダム MAC アドレスで接続してきたデバイスに対して与える認可プロファイルを設定します。
認可プロファイルは単純にポートの開放にあたるような Permit Access であったり、VLAN であったり ACL, そのほか先ほど設定したようなポータルへのリダイレクト等様々なものを与えることができるものです。
ということで、ここで先ほどの Random MAC Detected と名前を付けたポータルへのリダイレクトを選択します。
Policy > Policy Elements > Result > Authorizatioin > Authorization Profiles に進み、+ Add をクリックします。
image.png

image.png
Common Tasks の中でスクロールダウンしていくと Web Redirection というものが選べますので、先ほど作ったポータルを選ぶように選択します。ここで Web Redirection を選択して、次のように設定します。
image.png
image.pngスクロールダウンして Save をお忘れなく。Save ボタンとか Commit ってたまに忘れてアレってなりますよね。

ちなみにこれは無線LANコントローラ Catalyst 9800-CL 側の Redirect ACL です。
image.png

そして最後に 認証認可のポリシーを作成します。

冒頭で記述したように、ランダム MAC アドレスのフォーマットを持った端末をひっかける正規表現を使った MAB の特殊ルールを作り、先ほどのポータルに誘い込めるようにします。Policy > Policy Sets に進み、Authorization Policy に新しくルール作成します。
image.png
image.png

まとめ

これで、Random MAC を無効にする変更をしないと今作ったこっちのルールに引っ掛かってDot1x等の別のルールに進めないようにすることができるでしょう。
ISEの認証・認可で細かなルールを作ってひっかけると結構こまごまといろいろなことができて、例えば認証する SSID 名前だったり、ネットワークデバイスのロケーションだったりを条件として認可が変わる、みたいな使い方はよくお話しとしてあるのですが、今回の件で端末側のMACアドレスで正規表現を組み合わせるというあまり普段考えない方法を試すことができました。
あまり ISE3.0 独自の内容ということはないので、皆さんお手持ちのISEでお試しください。

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

ランダム化されたMACアドレスはネットワーク認証においてどう扱えるのか。[Cisco ISEの利用例]

ランダム MAC アドレス

iOS 14 や Android 端末で MAC アドレスのランダム化する機能が出てきました。自宅のルータなんかで MAC アドレスによるアクセス制限をされている方たちの「なぜかつながらなくなった!」「原因がわかるまで大変だった」「ランダム化は切ったわ」というような声がちらほらとネットでも見受けられました。

Use private Wi-Fi addresses in iOS 14, iPadOS 14, and watchOS 7
https://support.apple.com/en-us/HT211227
Android Open Source Project > Develop > Connectivity > Privacy: MAC Randomization
https://source.android.com/devices/tech/connect/wifi-mac-randomization

家庭や公衆 Wi-Fi 等ではプライバシーに配慮した機能として需要がある機能だとは思うのですが、MAC アドレスは端末の識別子として広く使われています。ですので、ご自宅のネットワーク環境だけでなく職場でも同様の苦労をされている方がいらっしゃるんではないかと、ちょっと注目しておりました。MAC アドレスでデバイスやユーザを識別する方法というのは一般的なものなので、この機能が端末側で有効になると管理者さん的にはきついじゃないかなあと。

機種ごとに MAC アドレスのランダム化の実装はバリエーションがあるものの、このランダム化されたMACアドレスはフォーマットが IEEE で定められています。よって、それがランダム化された MAC アドレスなのかどうか、は少なくともシステマチックに判断できます。
image.png
この図に描いたように MAC アドレスの先頭3オクテットが OUI で、その8ビットのうちの下2桁のところで判断できます。具体的にはこの b1 が1でかつ b0 が0のアドレスがランダム化されたものとなります。ということは最初の1オクテットが2,6,A,Eのどれかで終わるものがそれです。

ランダム MAC の無効化はエンドユーザにオフロードしたいです…。

こうした MAC アドレスを持ったデバイスを使っているそのエンドユーザは正当なユーザであって、登録したデバイスを利用しているのだとしたら、これを識別して、ネットワークに接続させる際の判断基準に採用できるようになるといいかなと思います。そして組織のネットワークに接続させるときにランダム化されたMACアドレスを持ったものを禁止するとか、制限付きのネットワークに入れるとかいうことも考えられますし、正規の端末を使っているのであればランダム化を切って接続しなおすようにガイドを出すというのが一番な気がしています。
とはいえその作業について管理者がいちいち面倒を見ていては業務が崩壊します。Windows端末にしてもスマホにしても、エンドユーザ側で設定変更ができるのでそうしてもらえるように一度準備しておくのが現実的かつ理想的だと思います。
例えば Windows 10 だと Wi-Fi 設定で一つクリックするだけです。
Windows 10 でのランダム MAC アドレスの設定
image.png
image.png

Cisco Identity Services Engine で ランダム MAC アドレスに対応する

Cisco Identity Services Engine(ISE) という高機能な認証サーバがあるのですが、それを使って認証フローを作るとしたら以下の様なやり方があります。
参考までにこの記事で載せているISEのバージョンは3.0です。
1. 業務用の SSID にランダム MAC を利用した端末で接続してくるとインストラクションにリダイレクト
2. そうでないものは通常の Dot1x 認証へ渡す

まずはランダム MAC を使った端末が仮でストアされる内部 DB グループを作成します。 Administration > Identity Management > Groups
に進み Endpoint Identity Group に新しく入れ物を作ります。
image.png
次に Work CentersGuest Portals を選び Create を選択し新たなポータルを作成します。これがユーザに「ランダム MAC やめなさい」的なインストラクションを見せるポータルになります。

image.png
選ぶのは Hotspot Guest Portal です。
image.png
ポータルの名前を適当に決めてて、そのままスクロールダウンしていきます。
image.png
Endpoint Identity Groupで先ほど作った Random_MAC_Endpoints を選びます。
image.png
そうしたらまた上の方にスクロールアップして戻ります。一旦ここで Save します。
image.png
Portal Page Cusomization というタブを選び、せっかくなので日本語のポータルを編集していく例にしてみましょう。
image.png

Global Page CustomizationsText Elements で適当な文章にバナーを変えます。

image.png

スクロールダウンしAcceptable Use Policy を選択し、ブラウザページタイトルや Content Title を適宜埋めます。
image.png

Instructional Text で Toggle HTML Source を押してから以下のスクリプトを記述し、それができたらまた Toggle HTML Source ボタンを押し戻します。
image.png
そうするとボタンが消えます。

<script>
(function(){
jQuery('.cisco-ise-aup-text').hide();
jQuery('.cisco-ise-aup-controls').hide();
setTimeout(function(){ jQuery('#portal-session-timeout-popup-screen, #portal-session-timeout-popup-popup, #portal-session-timeout-popup').remove(); }, 100);
})();
</script>

同じテキストボックスの中にランダムMACアドレスを無効化する方法を記述してユーザに案内することも併せて行うとよいと思います。
スクロールアップして Save します。

ランダム MAC アドレスで接続してきたデバイスに対して与える認可プロファイルを設定します。
認可プロファイルは単純にポートの開放にあたるような Permit Access であったり、VLAN であったり ACL, そのほか先ほど設定したようなポータルへのリダイレクト等様々なものを与えることができるものです。
ということで、ここで先ほどの Random MAC Detected と名前を付けたポータルへのリダイレクトを選択します。
Policy > Policy Elements > Result > Authorizatioin > Authorization Profiles に進み、+ Add をクリックします。
image.png

image.png
Common Tasks の中でスクロールダウンしていくと Web Redirection というものが選べますので、先ほど作ったポータルを選ぶように選択します。ここで Web Redirection を選択して、次のように設定します。
image.png
image.pngスクロールダウンして Save をお忘れなく。Save ボタンとか Commit ってたまに忘れてアレってなりますよね。

ちなみにこれは無線LANコントローラ Catalyst 9800-CL 側の Redirect ACL です。
image.png

そして最後に 認証認可のポリシーを作成します。

冒頭で記述したように、ランダム MAC アドレスのフォーマットを持った端末をひっかける正規表現を使った MAB の特殊ルールを作り、先ほどのポータルに誘い込めるようにします。Policy > Policy Sets に進み、Authorization Policy に新しくルール作成します。
image.png
image.png

まとめ

これで、Random MAC を無効にする変更をしないと今作ったこっちのルールに引っ掛かってDot1x等の別のルールに進めないようにすることができるでしょう。
ISEの認証・認可で細かなルールを作ってひっかけると結構こまごまといろいろなことができて、例えば認証する SSID 名だったり、ネットワークデバイスのロケーションだったりを条件として認可が変わる、みたいな使い方はよくお話しとしてあるのですが、今回の件で端末側のMACアドレスで正規表現を組み合わせるというあまり普段考えない方法を試すことができました。
この正規表現での認可ルールの表現というのは、新しい ISE3.0 独自の内容ということはないので、皆さんお手持ちのISEでお試しください。

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

iPhoneのスクリーンタイムをデータとしてスプレッドシートに記録してみた

はじめに

スマホ、触りすぎていませんか?

私は隙があればついついスマホでYouTubeやSNSを長々と見てしまいます。YouTubeを始めた芸能人も多く、無限にコンテンツを楽しめてしまいますよね。コロナ渦で自宅にいることが増えたこともあってか、スクリーンタイムを見て唖然としてしまう日も少なくありません...。

そんな中、スクリーンタイムをSNSに投稿して勉強のモチベーションにしている受験生を見て、こういう使い方もあるんだなぁと感心しました。
とはいえ、わざわざ設定画面を開いてスクショを撮り、それをトリミングしてSNSに手動で投稿するのは少々面倒に感じます()

幸いなことに、macからも同じApple IDで登録されてデバイスのスクリーンタイムを見ることができます。また、macにはAutomatorという操作を自動化するアプリケーションがあります。
さらに、Googleが公開しているCloud Vison APIを使えば、画像から文字を抽出することができます。

これらを上手いこと使って自動で「iPhoneのスクリーンタイムをデータとして記録する」までを行ってみました。
日々の生活を記録することで自己管理を効率化できるのではないかと思います!

スクリーンタイムについて

アートボード – 4.png

スクリーンタイムで見れる項目には、

  • App使用時間
  • 通知
  • 持ち上げ/再開回数

があります。
macのスクリーンタイム画面からは、サイドバーからこれらを選択して各項目の利用状況のグラフを見ることができます。注目すべきはシステム環境はウインドウサイズが一定で、スクリーンタイム内の文字やグラフの配置も同じような箇所にあることです。そのため、OCRで読み取りたい箇所だけを決め打ちでトリミングできます。

Automator

文字認識をしたいスクリーンタイムの画像を取得するためにAutomatorを使います。
Automatorの大まかな流れはこのような感じです。

  1. システム環境設定を開く
  2. 操作を記録して、スクリーンタイムを開き、取得したいデバイスを選択する
  3. 「App使用時間」「通知」「持ち上げ/再開回数」それぞれでスクリーンショットを撮影 & OCRしたい箇所をトリミング
  4. トリミングした画像をくっつけてVisonAPIでテキスト取得、スプレッドシートに記録

Automator

Automatorの詳細

システムの外観モードを変更

起動したタイミングで外観モードをライトに変更しています。

AppleScript

「アプリケーションを開く」でシステム環境設定を開くと画面が手前にならないことがあり、操作を記録のステップで処理がコケてしまうのでAppleScriptで起動しています。

tell application "System Preferences"
    activate
end tell

操作を記録

Automatorアプリの:red_circle:(記録)ボタンを押して、システム環境設定 > スクリーンタイム > デバイスの選択 > 日付を昨日に変更 までの流れをを記録させています

アートボード – 5 3.png

シェルスクリプトを実行

screencaptureコマンドで画面のスクリーンショットを撮影します。-lオプションで指定したwindow1dの画面を撮影することができます。また、-xは撮影音OFF、-oはスクリーンショットの影をなくすオプションです。
前の手順でクリックした直後に撮影しないように念のためシェルスクリプトの最初でsleep 1sとしています。

sleep 1s
dir="$HOME/lifelog"
filepath=$dir/`date -v -1d +%Y%m%d`_screentime.png
crop="/tmp/cropped_1.png"
screencapture -xo -l$(osascript -e 'tell app "システム環境設定" to id of window 1') $filepath
/usr/local/bin/convert $filepath -crop 828x574+466+152 $filepath  # パスが通っていないので、フルパスで書く
/usr/local/bin/convert $filepath +repage -crop 400x60+20+60 $crop

操作を記録

システム環境設定のサイドバーにある「通知」のクリック操作を記録しています。「通知」をクリックした後は上記と同じ内容のシェルスクリプトを実行します。「持ち上げ/再開」でも同様に行います。

アートボード – 5.png

画像の結合とVisionAPI

/usr/local/bin/convert /tmp/cropped_1.png /tmp/cropped_2.png /tmp/cropped_3.png -append /tmp/combined.png
python record_screentime.py  # Vision APIとシートに記録

スクリーンショットとトリミング

macでは、screencaptureコマンドでCLIからスクリーンショットを撮影できます。
トリミングにはImageMagickのconvertコマンドを使用しています。-cropオプションの値には、(width)x(height)+(left)+(top)を指定しています。
また、一度cropした画像にさらにcropしようとするとconvert: geometry does not contain imageというエラーが出てしまいましたが、-cropの前に+repageをつけることで解消できました。

# ウインドウを指定してスクリーンショットを撮影
screencapture -xo -l$(osascript -e 'tell app "システム環境設定" to id of window 1') screentime.png
# crop
convert screentime.png -crop 828x574+466+152 screentime.png
# OCRしたい箇所でさらにcrop
convert screentime.png +repage -crop 400x60+20+60 screentime_cropped.png

上記コマンドでOCRで文字認識したい箇所でcropすると、

  • App使用時間

cropped_1.png

  • 通知
    cropped_2.png

  • 持ち上げ/再開回数
    cropped_3.png

のようになります。これらを結合した画像をVisino APIに投げます。画像の結合は、以下のコマンドで行いました。+appendで縦方向、-appendで横方向に結合できます。

convert /tmp/cropped_1.png /tmp/cropped_2.png /tmp/cropped_3.png -append /tmp/combined.png

結合した画像

combined.png

これで準備完了です。

Vision APIとシートに記録

いよいよ準備した画像を使って、文字認識を行ってデータとして保存します。
以下の記事などを参考にさせていただきました。:bow:

import gspread
import json
from datetime import datetime, timedelta
from oauth2client.service_account import ServiceAccountCredentials 
import os
import io
import re
from google.cloud import vision

credential_path = '【サービスアカウントキーのパス】'

os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = credential_path
file_path = "/tmp/combined.png"
with io.open(file_path, "rb") as image_file:
    content = image_file.read()
image = vision.Image(content=content)

client = vision.ImageAnnotatorClient()
response = client.text_detection(image=image)
data = response.text_annotations[0].description.split()  # [0]でfull textを取得

scope = ['https://spreadsheets.google.com/feeds','https://www.googleapis.com/auth/drive']
credentials = ServiceAccountCredentials.from_json_keyfile_name(credential_path, scope)
gc = gspread.authorize(credentials)
SPREADSHEET_KEY = '【スプレッドシートのキー】'
worksheet = gc.open_by_key(SPREADSHEET_KEY).sheet1

yesterday = datetime.today() - timedelta(days=1)
date = datetime.strftime(yesterday, '%Y/%m/%d')

d = re.findall("\d+", data[0])  # 'xx時間yy分` -> ['xx','yy']
d = list(map(int, d))
if len(d) == 2:  # xx時間yy分の場合
    data[0] = d[0]* 60 + d[1]
elif len(d) == 1:  # yy分の場合
    data[0] = d[0]

data = list(map(int, data))
data.insert(0, date)
worksheet.append_row(data, value_input_option="USER_ENTERED")

実行すると...
スクリーンショット 2020-12-15 20.44.23.png

スクリーンタイムのデータがシートに反映されました!
あとはAutomatorのアプリを起動すれば今までの処理を自動で行ってくれます。(本当はcronで設定したかったけど、画面操作があるためできませんでした。。)

ちなみに、VisionAPIを使った文字認識の結果はこうなりました。緑がParagraph、黄色が文字を表しています。きれいに認識できていますね。
スクリーンショット 2020-12-15 20.47.28.png

おわりに

自動化ソフトや画像認識のツールを使って、スクリーンタイムをログに残してみました。せっかくならスクショだけはでなくて数字で記録に残せるといいですよね。
VisionAPIは1ヶ月1000リクエストまで無料なので毎日スクリーンタイムを記録して問題なさそうです。

今のところスクリーンタイムを記録する術は英語でググっても見つからなかったので、この記事が誰かの参考になれば幸いです。

(Appleさん、スクリーンタイムもヘルスケアから読み取れるようにしてください)

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

iPhoneのスクリーンタイムをOCRで記録してみた

はじめに

スマホ、触りすぎていませんか?

私は隙があればついついスマホでYouTubeやSNSを長々と見てしまいます。YouTubeを始めた芸能人も多く、無限にコンテンツを楽しめてしまいますよね。コロナ渦で自宅にいることが増えたこともあってか、スクリーンタイムを見て唖然としてしまう日も少なくありません...。

そんな中、スクリーンタイムをSNSに投稿して勉強のモチベーションにしている受験生を見て、こういう使い方もあるんだなぁと感心しました。
とはいえ、わざわざ設定画面を開いてスクショを撮り、それをトリミングしてSNSに手動で投稿するのは少々面倒に感じます()

幸いなことに、macからも同じApple IDで登録されてデバイスのスクリーンタイムを見ることができます。また、macにはAutomatorという操作を自動化するアプリケーションがあります。
さらに、Googleが公開しているCloud Vison APIを使えば、画像から文字を抽出することができます。

これらを上手いこと使って自動で「iPhoneのスクリーンタイムをデータとして記録する」までを行ってみました。
日々の生活を記録することで自己管理を効率化できるのではないかと思います!

スクリーンタイムについて

アートボード – 4.png

スクリーンタイムで見れる項目には、

  • App使用時間
  • 通知
  • 持ち上げ/再開回数

があります。
macのスクリーンタイム画面からは、サイドバーからこれらを選択して各項目の利用状況のグラフを見ることができます。注目すべきはシステム環境はウインドウサイズが一定で、スクリーンタイム内の文字やグラフの配置も同じような箇所にあることです。そのため、OCRで読み取りたい箇所だけを決め打ちでトリミングできます。

Automator

文字認識をしたいスクリーンタイムの画像を取得するためにAutomatorを使います。
Automatorの大まかな流れはこのような感じです。

  1. システム環境設定を開く
  2. 操作を記録して、スクリーンタイムを開き、取得したいデバイスを選択する
  3. 「App使用時間」「通知」「持ち上げ/再開回数」それぞれでスクリーンショットを撮影 & OCRしたい箇所をトリミング
  4. トリミングした画像をくっつけてVisonAPIでテキスト取得、スプレッドシートに記録

Automator

Automatorの詳細

システムの外観モードを変更

起動したタイミングで外観モードをライトに変更しています。

AppleScript

「アプリケーションを開く」でシステム環境設定を開くと画面が手前にならないことがあり、操作を記録のステップで処理がコケてしまうのでAppleScriptで起動しています。

tell application "System Preferences"
    activate
end tell

操作を記録

Automatorアプリの:red_circle:(記録)ボタンを押して、システム環境設定 > スクリーンタイム > デバイスの選択 > 日付を昨日に変更 までの流れをを記録させています

アートボード – 5 3.png

シェルスクリプトを実行

screencaptureコマンドで画面のスクリーンショットを撮影します。-lオプションで指定したwindow1dの画面を撮影することができます。また、-xは撮影音OFF、-oはスクリーンショットの影をなくすオプションです。
前の手順でクリックした直後に撮影しないように念のためシェルスクリプトの最初でsleep 1sとしています。

sleep 1s
dir="$HOME/lifelog"
filepath=$dir/`date -v -1d +%Y%m%d`_screentime.png
crop="/tmp/cropped_1.png"
screencapture -xo -l$(osascript -e 'tell app "システム環境設定" to id of window 1') $filepath
/usr/local/bin/convert $filepath -crop 828x574+466+152 $filepath  # パスが通っていないので、フルパスで書く
/usr/local/bin/convert $filepath +repage -crop 400x60+20+60 $crop

操作を記録

システム環境設定のサイドバーにある「通知」のクリック操作を記録しています。「通知」をクリックした後は上記と同じ内容のシェルスクリプトを実行します。「持ち上げ/再開」でも同様に行います。

アートボード – 5.png

画像の結合とVisionAPI

/usr/local/bin/convert /tmp/cropped_1.png /tmp/cropped_2.png /tmp/cropped_3.png -append /tmp/combined.png
python record_screentime.py  # Vision APIとシートに記録

スクリーンショットとトリミング

macでは、screencaptureコマンドでCLIからスクリーンショットを撮影できます。
トリミングにはImageMagickのconvertコマンドを使用しています。-cropオプションの値には、(width)x(height)+(left)+(top)を指定しています。
また、一度cropした画像にさらにcropしようとするとconvert: geometry does not contain imageというエラーが出てしまいましたが、-cropの前に+repageをつけることで解消できました。

# ウインドウを指定してスクリーンショットを撮影
screencapture -xo -l$(osascript -e 'tell app "システム環境設定" to id of window 1') screentime.png
# crop
convert screentime.png -crop 828x574+466+152 screentime.png
# OCRしたい箇所でさらにcrop
convert screentime.png +repage -crop 400x60+20+60 screentime_cropped.png

上記コマンドでOCRで文字認識したい箇所でcropすると、

  • App使用時間

cropped_1.png

  • 通知
    cropped_2.png

  • 持ち上げ/再開回数
    cropped_3.png

のようになります。これらを結合した画像をVisino APIに投げます。画像の結合は、以下のコマンドで行いました。+appendで縦方向、-appendで横方向に結合できます。

convert /tmp/cropped_1.png /tmp/cropped_2.png /tmp/cropped_3.png -append /tmp/combined.png

結合した画像

combined.png

これで準備完了です。

Vision APIとシートに記録

いよいよ準備した画像を使って、文字認識を行ってデータとして保存します。
以下の記事などを参考にさせていただきました。:bow:

import gspread
import json
from datetime import datetime, timedelta
from oauth2client.service_account import ServiceAccountCredentials 
import os
import io
import re
from google.cloud import vision

credential_path = '【サービスアカウントキーのパス】'

os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = credential_path
file_path = "/tmp/combined.png"
with io.open(file_path, "rb") as image_file:
    content = image_file.read()
image = vision.Image(content=content)

client = vision.ImageAnnotatorClient()
response = client.text_detection(image=image)
data = response.text_annotations[0].description.split()  # [0]でfull textを取得

scope = ['https://spreadsheets.google.com/feeds','https://www.googleapis.com/auth/drive']
credentials = ServiceAccountCredentials.from_json_keyfile_name(credential_path, scope)
gc = gspread.authorize(credentials)
SPREADSHEET_KEY = '【スプレッドシートのキー】'
worksheet = gc.open_by_key(SPREADSHEET_KEY).sheet1

yesterday = datetime.today() - timedelta(days=1)
date = datetime.strftime(yesterday, '%Y/%m/%d')

d = re.findall("\d+", data[0])  # 'xx時間yy分` -> ['xx','yy']
d = list(map(int, d))
if len(d) == 2:  # xx時間yy分の場合
    data[0] = d[0]* 60 + d[1]
elif len(d) == 1:  # yy分の場合
    data[0] = d[0]

data = list(map(int, data))
data.insert(0, date)
worksheet.append_row(data, value_input_option="USER_ENTERED")

実行すると...
スクリーンショット 2020-12-15 20.44.23.png

スクリーンタイムのデータがシートに反映されました!
あとはAutomatorのアプリを起動すれば今までの処理を自動で行ってくれます。(本当はcronで設定したかったけど、画面操作があるためできませんでした。。)

ちなみに、VisionAPIを使った文字認識の結果はこうなりました。緑がParagraph、黄色が文字を表しています。きれいに認識できていますね。
スクリーンショット 2020-12-15 20.47.28.png

おわりに

自動化ソフトや画像認識のツールを使って、スクリーンタイムをログに残してみました。せっかくならスクショだけはでなくて数字で記録に残せるといいですよね。
VisionAPIは1ヶ月1000リクエストまで無料なので毎日スクリーンタイムを記録して問題なさそうです。

今のところスクリーンタイムを記録する術は英語でググっても見つからなかったので、この記事が誰かの参考になれば幸いです。

(Appleさん、スクリーンタイムもヘルスケアから読み取れるようにしてください)

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

Firebase向けGoogle Analytics [仕組みと活用例]

ACCESS Advent Calendar 2020 15日目の記事になります。

はじめに

Firebase向けGoogle Analytics (Google Analytics for Firebase)はモバイル、Webアプリ向けのログ解析サービスです。
以前はFirebase Analyticsという名前でした。
本記事ではモバイル(iOS / Android)向けのログ収集について取り上げます。

どのようなことが分析できるか

  • どの画面がどれぐらい表示されるか (表示時間、割合)
  • ある画面操作をユーザー属性別に統計を取る
  • ある機能がどの程度利用されているか (操作回数、使用時間)
  • ある画面にたどり着くルートのうちどちらが多いか

仕組み

iOSはCocoapods、AndroidはGradleでライブラリが提供されています。
ライブラリを使ってユーザーの属性やイベントと呼ばれるユーザー操作などのログを送信するように実装します。

詳しくはFirebaseのドキュメントに記載されています。

Firebase Console

収集したデータを閲覧するサービスです。
どのような画面かはデモプロジェクトをみると雰囲気が分かります。 (Googleアカウントが必要)
iOS, Androidのアプリの両方を同じプロジェクトで管理することができます。

イベント

ユーザーの操作などをイベントという形で記録できます。

例えばscreen_viewは画面が移り変わったときに記録されます。これはライブラリを導入しただけで収集されるイベントの1つです。
cf. 自動的に収集されるイベント
これはユーザーが表示した全ての画面が記録されるので、アプリの中でどの画面が最も多く表示されているかというのも分かります。

デモプロジェクトのscreen_viewを見るとユーザーエンゲージメントの枠に画面の名前が表示されています。
iOSならViewController、AndroidならActivityがデフォルトで収集されます。
エンゲージメント率が高いと多く表示されているということになります。

カスタムイベント

任意のタイミングでイベントを送信するには次のように書きます。(Android・Kotlinの場合)

Kotlin
import com.google.firebase.analytics.FirebaseAnalytics

FirebaseAnalytics.getInstance(context).logEvent("tweet") {
    param("text_length", length)
    param("images", count)
}

paramというのはカスタムパラメータで、key-value形式で設定できます。
ただしパラメータの数は25個まで、key nameは40文字以内、英数字+アンダースコア_のみという制約があるので注意が必要です。

ユーザープロパティ

イベントにはそのアプリのユーザー情報が紐付いて送信されています。
こちらも自動的に収集されるプロパティがあります。例えば年齢、性別、居住地域といったユーザーに関する情報や端末名、OSバージョンといった端末の情報があります。
cf. 事前定義されたユーザー ディメンション

独自のプロパティを設定する

プロパティもkey-value形式で設定できます。
ただこちらは予めConsoleでキーを設定しておく必要があります。

Kotlin
import com.google.firebase.analytics.FirebaseAnalytics

FirebaseAnalytics.getInstance(context).setUserProperty("email", "hoge@example.com")

cf. ユーザープロパティの設定と登録

ユーザープロパティはイベントのフィルターとして利用できます。

cf. レポート設定

実際の活用例

方針としてクライアント側でしか収集できないようなことをハンドリングした方がよいです。
サーバー側で解析できるようなAPIのリクエスト数やエラーを解析したい場合、Analyticsは向いていないかもしれません。

またアプリのクラッシュや例外を解析するにはFirebase Crashlyticsというサービスがあるので、こちらも合わせて導入しておくと便利です。

どちらの導線が多く利用されているか調べる

例えばユーザーをブロックする機能があり、その操作をするには2つの導線があるとします。
そのどちらが多く利用されているかを調べるにはこのように

Kotlin
// ユーザープロフィールから
firebaseAnalytics.logEvent("block_user") {
    param("from", "profile_view")
}

// ツイートから
firebaseAnalytics.logEvent("block_user") {
    param("from", "tweet")
}

別の画面で同じイベントを送信するようにし、カスタムパラメータでどちらの導線から実行されたかを与えておきます。

そしてこのfromというパラメータをConsoleで分析できるようにするために、カスタムディメンションを設定しておきます。
用途は異なりますがデモプロジェクトだと、level_completeイベントのlevel_name[post_score]が似たような結果になると思います。

状態を持つ画面を区別する

例えばTweetActivityというActivityが2つの状態を持っているとします。
デフォルトではActivityの名前が画面の名前として収集されるので、状態を判別できません。
その場合、このように手動で画面の名前を設定することで区別することができます。

Kotlin
// ツイート投稿画面
firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW) {
    param(FirebaseAnalytics.Param.SCREEN_NAME, "PostTweetView")
    param(FirebaseAnalytics.Param.SCREEN_CLASS, "TweetActivity")
}

// ツイート編集画面
firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW) {
    param(FirebaseAnalytics.Param.SCREEN_NAME, "EditTweetView")
    param(FirebaseAnalytics.Param.SCREEN_CLASS, "TweetActivity")
}

cf. 画面表示の追跡

Consoleだとscreen_viewイベントのユーザーエンゲージメントスクリーン名から確認できます。

利用時間を集計する

例えば通話時間を集計したい場合、通話を始めてから切るまでの時間をタイマーかなにかで測定しておいて、それをイベントのカスタムパラメータに設定しておけば集計することができます。

デモプロジェクトだとpost_scoreイベントのscoreパラメータが該当します。
数値の単位はConsole側のカスタム指標設定で設定できます。

おわりに

Firebase向けGoogle Analyticsを導入すると、機能の追加や変更をするにあたって正確なデータを元に検討することができます。
途中でイベントやユーザープロパティを実装してもそれ以前のデータは収集できないので、初期段階で実装しておくことをおすすめします。
ライブラリを入れるだけでも価値があるので、是非使ってみてください。

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

Xcode12.3 への XVim2 導入メモ

Xcode へ Vim ライクな機能を入れるプラグイン XVim2 をインストールする方法を毎回探しに行くのが面倒だったので、備忘録としてここにまとめようと思います。

実行環境

MacOS 11.0.1(Intel)
Xcode12.3

参考

ここに記載する内容は本家 GitHubリポジトリ を参考にしています。
https://github.com/XVimProject/XVim2

1.証明書の作成

キーチェーンアクセス.app を開き、メニューから 「キーチェーンアクセス」 > 「証明書アシスタント」 > 「証明書を作成...」 を選択。

スクリーンショット 2020-12-15 15.58.54.png

名前と証明書のタイプを変更し、次へ次へと進んで作成を完了させます。

スクリーンショット_2020-12-15_16_01_44.png

2.Xcodeを自己署名

続いて次のコマンドをターミナルで実行します。

sudo codesign -f -s XcodeSigner /Applications/Xcode.app

AppStoreからインストールした場合は上記ディレクトリにあるはず。
※ Xcodeがある場所が異なる場合は適宜パスを変更してください。
※ 完了まで結構時間がかかります。。

3.XVim2 を clone し、 make を実行

git clone git@github.com:XVimProject/XVim2.git

↑の 2.Xcodeを自己署名 が完了していることを確認し、clone したディレクトリに移動して make を実行です。

cd XVim2
make

4.Xcode起動 -> Load Bundle を選択

Xcode を起動するとダイアログが出てくるので「Load Bundle」を選択します。
(「Skip Bundle」がアクティブになっているので注意!)

スクリーンショット_2020-12-15_12_22_58.png

以上でカーソルがブロックになって Vim と同じような操作が可能になります。
自分のプロジェクトを開いて確認してみましょう!

(番外編) ちなみに、Xcodeアップデート後に必要なことはある?

Xcodeのアップデートをインストールした場合は、再び Xcode の自己署名、XVim2 ディレクトリでの make 操作が必要です。

この記事を書くタイミングで AppStore でインストールした Xcode12.2 を 12.3 へアップデートをしましたが、アップデート直後に起動すると機能が解除されていました。XVim2 ディレクトリへ移動し、 make を実行しても機能せず。。

しかし、2.Xcodeを自己署名 からやり直したところ、今まで通り動作することが確認できました。

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

iOS14から使用できるUIMenuの実装について

はじめに

iOS Advent Calendar 2020 の18日目になります!

今回は、WWDC20にて紹介されていましたUIMenuの新機能について焦点を当ててみようと思います。

UIMenu自体はiOS13から使用できるのですが、
iOS14からはUIButtonUIBarButtonItemにも使用可能となった内容がありましたので、おさらい的な紹介ができればと思います?‍♂️
とはいっても、すでにとても分かりやすくまとめられている記事がありましたので、細かい説明は割愛させていただきます。
本記事では、実際にコードで書いてみて使い心地を感じられればと思います。

UIMenu
https://developer.apple.com/documentation/uikit/uimenu
WWDC20
https://developer.apple.com/videos/play/wwdc2020/10205

1. 新機能について

すでに分かりやすくまとめられている記事が多々ありますのでリンクを貼らせていただきました?‍♂️
(これ以上上手にまとめられる気がしませんでした。。)
https://medium.com/better-programming/whats-new-in-ios-14s-uimenu-and-contextmenu-433cd2037c37

2. UIMenuのメリット

スクリーンショット 2020-12-16 2.09.31.png

UIMenuと比較する機能としてUIAlertController(ActionSheet)が挙げられると思います。
WWDC20でもこちらを比較して紹介されていましたので以下にまとめてみました。

UIMenuのメリットとして以下が挙げられていました。

  • iPad表示するためにポップオーバー表示するための実装をする必要がない
  • 表示の際に背景を暗くする処理がなくなったため、その分軽量感のある遷移になる
  • 閉じるための「キャンセル」ボタンが不要(Menu外をタップすると閉じる)
  • タップした箇所からMenuが表示されるので操作しやすい(操作性) etc...

3. 実際に書いてみました

「百聞は一見にしかず」と言うことで、実際にコードを書いてみました。

  • [3-1] HIGH, MID, LOWを切り替える
  • [3-1] UIMenuを開いた際には、選択した項目にチェックマークがついている
  • [3-2] UIMenuを非同期で構築する場合

と言う内容を実装してみたいと思います。

3-1. 完成画面

uimenu.gif

3-1-1. 下準備

UIMenuを設定する前に、設定するためのUIButtonなどを下準備します。

class ViewController: UIViewController {
    // メニュー表示項目
    enum MenuType: String {
        case high = "HIGH"
        case mid = "MID"
        case low = "LOW"
    }
    // メニュー選択ボタン
    @IBOutlet weak var menuButton: UIButton!

    // 選択されたMenuType
    var selectedMenuType = MenuType.high

    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

3-1-2. UIButtonにUIMenuを設定

UIMenuUIButtonに設定するために、以下のメソッドを作成しました。

private func configureMenuButton() {
    var actions = [UIMenuElement]()
    // HIGH
    actions.append(UIAction(title: MenuType.high.rawValue, image: nil, state: self.selectedMenuType == MenuType.high ? .on : .off,
                            handler: { (_) in
                                self.selectedMenuType = .high
                                // UIActionのstate(チェックマーク)を更新するためにUIMenuを再設定する
                                self.configureMenuButton()
                            }))
    // MID
    actions.append(UIAction(title: MenuType.mid.rawValue, image: nil, state: self.selectedMenuType == MenuType.mid ? .on : .off,
                            handler: { (_) in
                                self.selectedMenuType = .mid
                                // UIActionのstate(チェックマーク)を更新するためにUIMenuを再設定する
                                self.configureMenuButton()
                            }))
    // LOW
    actions.append(UIAction(title: MenuType.low.rawValue, image: nil, state: self.selectedMenuType == MenuType.low ? .on : .off,
                            handler: { (_) in
                                self.selectedMenuType = .low
                                // UIActionのstate(チェックマーク)を更新するためにUIMenuを再設定する
                                self.configureMenuButton()
                            }))

    // UIButtonにUIMenuを設定
    menuButton.menu = UIMenu(title: "", options: .displayInline, children: actions)
    // こちらを書かないと表示できない場合があるので注意
    menuButton.showsMenuAsPrimaryAction = true
    // ボタンの表示を変更
    menuButton.setTitle(self.selectedType.rawValue, for: .normal)
}

3-1-3. 完成

あとは、作成したメソッドをviewDidLoad内で呼び出す様にします。

class ViewController: UIViewController {
    // メニュー表示項目
    enum MenuType: String {
        case high = "HIGH"
        case mid = "MID"
        case low = "LOW"
    }

    @IBOutlet weak var selectButton: UIButton!

    // 選択されたMenuType
    var selectedType = MenuType.high

    override func viewDidLoad() {
        super.viewDidLoad()

        // UIButtonにUIMenuを設定する
        self.configureMenuButton()
    }
}

3-2. 非同期でUIMenuを構築する場合

非同期でUIMenuの構築が必要な場合は、UIDeferredMenuElementを使用することで実装することができます。

uimenu_load.gif

var actions = [UIMenuElement]()

let deferredMenuElement = UIDeferredMenuElement({ completion in
    // 時間がかかる処理
   ....
    completion(actions)
})
menuButton.menu = UIMenu(title: "UIDeferredMenuElement", options: .displayInline, children: [deferredMenuElement])
menuButton.showsMenuAsPrimaryAction = true 

UIDeferredMenuElement
https://developer.apple.com/documentation/uikit/uideferredmenuelement

4. まとめ

これらの実装は、iOS14から対応なので実際にはiOS13以下の場合の分岐処理を書かなければならず、面倒ではあります。
ただ、操作感が良くや使い所が多岐に渡りそうな気がするのでとても魅力的な機能でした。
個人的にはUIAlertController(ActionSheet)実装時にiPad用に処理を書き忘れてしまうと、クラッシュする原因にもなるのでこの辺り考えなくて良くなるのは嬉しい部分ではありました。

実装してみましたが、説明やコードで誤りなどございましたらご指摘いただけると幸いです。
ご協力のほどよろしくお願いいたします?‍♂️

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

iOSアプリで環境ごとに設定を変えるベストプラクティス(Swift)

はじめに

本記事は Swift/Kotlin愛好会 Advent Calendar 2020 の5日目の記事です。
空いていたので参加しました。

iOSアプリ開発において、環境ごとに変数の値を切り替えるベストプラクティスを紹介します。

背景

私が開発しているアプリで、APIの接続先が3つ(開発用・ステージング用・リリース用)必要になりました。
Build Configurations(以下「ビルド構成」と呼ぶ)」に Staging を追加し、 #if で分岐する方法をよく見かけます。

EnvironmentVariables.swift
enum EnvironmentVariables {
#if DEBUG
    static let apiBaseUri = "https://example.com/debug/"
#elseif STAGING
    static let apiBaseUri = "https://example.com/staging/"
#elseif RELEASE
    static let apiBaseUri = "https://example.com/release/"
#endif

しかし、最近はXcodeのビルドシステムが DebugRelease の2種類を前提としており、ビルド構成を追加するとライブラリ周りで問題が発生するようです。


XcodeGenを使っている場合、環境ごとに設定を変えてプロジェクトを生成するのがいいとのことなので、その方法を紹介します。

最も伝えたいこと

本記事で最も伝えたいことは、 ビルド構成をいじらず、それ以外の方法で環境ごとに設定を変えよう です。
「それ以外の方法」として、XcodeGenを使うと比較的かんたんに実現できる、ということです。

といいつつも、実はビルド構成をいじることによる具体的な問題を把握していないので、知っていたら教えていただけると嬉しいです :bow:

ただ、私の認識は以下のツイートの通りなので、問題の有無にかかわらずビルド構成とは別の方法で設定を変えるのに賛成です。

前提条件

  • XcodeGenを使っている
    使っていない場合、おまけが参考になるかもしれない

環境

  • OS:macOS Catalina 10.15.7
  • Xcode:12.2 (12B45b)
  • Swift:5.3.1
  • XcodeGen:2.18.0

実装

環境ごとに設定を変えられるよう実装します。

Makefileの作成(任意)

プロジェクト内で使いたい値を環境変数としてエクスポートし、XcodeGenでプロジェクトを生成するコマンドを準備します。
エクスポートする環境変数を環境ごとに変えるため、 make などを使ってタスク化するのがオススメです。

私は以下のような Makefile を作成しています。

Makefile
DEBUG_ENVIRONMENT := DEBUG
STAGING_ENVIRONMENT := STAGING
RELEASE_ENVIRONMENT := RELEASE

.PHONY: generate-xcodeproj-debug
generate-xcodeproj-debug: # Generate project with XcodeGen for debug
    $(MAKE) generate-xcodeproj ENVIRONMENT=${DEBUG_ENVIRONMENT}

.PHONY: generate-xcodeproj-staging
generate-xcodeproj-staging: # Generate project with XcodeGen for staging
    $(MAKE) generate-xcodeproj ENVIRONMENT=${STAGING_ENVIRONMENT}

.PHONY: generate-xcodeproj-release
generate-xcodeproj-release: # Generate project with XcodeGen for release
    $(MAKE) generate-xcodeproj ENVIRONMENT=${RELEASE_ENVIRONMENT}

.PHONY: generate-xcodeproj
generate-xcodeproj:
    mint run xcodegen xcodegen generate

この Makefile では ENVIRONMENT 環境変数に以下の値をエクスポートしています。

環境
デバッグ DEBUG
ステージング STAGING
リリース RELEASE

環境変数の名前は ENVIRONMENT でなくても問題ありません。

例えば API_BASE_URI として、直接APIの接続先を渡すこともできます。
しかし、それだと他にも環境ごとに変えたい設定が出てきた場合、そのたびに環境変数をエクスポートしなければいけません。
私は環境を判定する値を1つのみエクスポートし、プロジェクト内でAPIの接続先を変えるようにします。

2020/12/16 追記
例えばリリース時に開発やステージング環境の設定をどうしてもバイナリに含めたくない場合、設定値を直接エクスポートするのもありだと思います。

project.ymlの修正

エクスポートした環境変数をプロジェクトに注入します。

project.yml
targets:
  {製品ターゲット名}:
    # {中略}
    settings:
      base:
+       ENVIRONMENT: ${ENVIRONMENT}

XcodeGenでは、環境変数を ${環境変数名} で取得できます。
ここでは ENVIRONMENT という名前でUser-Definedの設定を作成し、先ほどエクスポートした環境変数を注入しています。

ここまで実装して make generate-xcodeproj-release を実行すると、以下のUser-Definedが作成されます。
スクリーンショット_2020-12-15_17_09_31.jpg

見てわかる通り、ビルド構成にかかわらずすべて RELEASE の値が入っています。
つまり 環境とビルド構成は互いに独立している ということであり、「リリース環境でデバッグビルド」や「ステージング環境でリリースビルド」などができるようになります。

Info.plistにUser-Definedの設定を追加

Info.plistproject.yml で定義したUser-Definedの設定を追加します。
スクリーンショット 2020-12-15 17.18.29.png

Key Type Value
任意 String $(User-Definedの設定名)

Keyは任意ですが、わかりやすいようにUser-Definedの設定名に近い名前がいいと思います。

EnvironmentVariables.swiftの追加

Info.plist に追加したことで、 Bundle.main.object(forInfoDictionaryKey: "キー") を呼び出してSwiftファイルから設定を取得できるようになりました。

私は環境変数を一元管理したいため、1ファイルにまとめています。

EnvironmentVariables.swift
import Foundation

enum Environment: String {
    case debug = "DEBUG"
    case staging = "STAGING"
    case release = "RELEASE"
}

enum EnvironmentVariables {
    static var environment: Environment {
        guard let environmentString = Bundle.main.object(forInfoDictionaryKey: "Environment") as? String,
              let environment = Environment(rawValue: environmentString)
        else {
            fatalError("Fail to load `Environment` from `Info.plist`.")
        }
        return environment
    }

    static var apiBaseUri: String {
        switch environment {
        case .debug:
            return "https://example.com/debug/"
        case .staging:
            return "https://example.com/staging/"
        case .release:
            return "https://example.com/release/"
        }
    }
}

これで実装は完了です。

今後環境ごとに変えたい設定が増えた場合、 apiBaseUri と同様に実装すればOKです。
EnvironmentVariables.swift 以外の修正は不要です。

ちなみに EnvironmentVariables をケースなしの列挙型にしているのは、単純に名前空間が欲しいためです。

使い方

使い方は以下の通りです。

  1. make generate-xcodeproj-○○ で環境変数のエクスポートとプロジェクトを生成する
  2. EnvironmentVariables.×× で設定値をSwiftで呼び出す

例として make generate-xcodeproj-staging を実行し、 AppDelefate.swift 内で設定値を呼び出します。

$ make generate-xcodeproj-staging 
/Applications/Xcode.app/Contents/Developer/usr/bin/make generate-xcodeproj ENVIRONMENT=STAGING
mint run xcodegen xcodegen generate
⚙️  Generating plists...
⚙️  Generating project...
⚙️  Writing project...
Created project at /Users/{ユーザー名}/{中略}/{プロジェクト名}.xcodeproj
AppDelefate.swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        print(EnvironmentVariables.apiBaseUri) // "https://example.com/staging/"

        if EnvironmentVariables.environment == .staging { // true
            print("Environment is staging.")
        }

        return true
    }
}

環境ごとの設定値を取得することができました!

EnvironmentVariables.swiftenvironmentprivate にしないことで、環境ごとに処理を分岐することができます。

発展: プロトコルを噛ませてモック化できるようにする

上記の実装だと単体テスト時に値を差し替えづらいので、プロトコルを噛ませてモック化できるようにします。
列挙型だとケースがないとインスタンス化できないので、構造体に変更しています。

私は Mockolo というモック生成ライブラリを使っているため、プロトコルに /// @mockable コメントを付けています。

EnvironmentVariables.swift
import Foundation
+ 
+ /// @mockable
+ protocol EnvironmentVariablesProtocol {
+     var environment: Environment { get }
+     var apiBaseUri: String { get }
+ }

enum Environment: String {
    case debug = "DEBUG"
    case staging = "STAGING"
    case release = "RELEASE"
}

- enum EnvironmentVariables {
-     static var environment: Environment {
+ struct EnvironmentVariables: EnvironmentVariablesProtocol {
+     var environment: Environment {
        guard let environmentString = Bundle.main.object(forInfoDictionaryKey: "Environment") as? String,
              let environment = Environment(rawValue: environmentString)
        else {
            fatalError("Fail to load `Environment` from `Info.plist`.")
        }
        return environment
    }

-     static var apiBaseUri: String {
+     var apiBaseUri: String {
        switch environment {
        case .debug:
            return "https://example.com/debug/"
        case .staging:
            return "https://example.com/staging/"
        case .release:
            return "https://example.com/release/"
        }
    }
}

AppDelefate.swift 内で呼び出す例は以下のように変わります。

AppDelefate.swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

+         let environmentVariables: EnvironmentVariablesProtocol = EnvironmentVariables()
-         print(EnvironmentVariables.apiBaseUri) // "https://example.com/staging/"
+         print(environmentVariables.apiBaseUri) // "https://example.com/staging/"

-         if EnvironmentVariables.environment == .staging { // true
+         if environmentVariables.environment == .staging { // true
            print("Environment is staging.")
        }

        return true
    }
}

インスタンス化する手間は増えますが、私はプロトコルを噛ませるほうがテスタブルで好みです。

例として、 apiBaseUri をイニシャライザ経由でDIします。

ApiClient.swift
final class ApiClient {
    private let apiBaseUri: String

    init(environmentVariables: EnvironmentVariablesProtocol) {
        self.apiBaseUri = environmentVariables.apiBaseUri
    }
}

これだと EnvironmentVariablesProtocol の全プロパティやメソッドがイニシャライザ内で呼び出せるため、 apiBaseUri のみDIするのもありだと思います。

environment のみ使いたい場合、 Environment をDIすると余計なプロパティやメソッド(今回だと apiBaseUri プロパティ)を呼び出せなくなってわかりやすいです。

Foo.swift
final class Foo {
    private let environment: Environment

    init(environment: Environment) {
        self.environment = environment
    }

    func foo() {
        if environment == .staging {
            // ステージング環境で特有の処理
        }
    }
}

おまけ: BuildConfig.swiftを使う

@417_72ki さんが開発している BuildConfig.swift を使えば、XcodeGenを使っていないプロジェクトでも環境ごとに設定を変えられます。

詳細は以下のスライドをご参照ください。
https://speakerdeck.com/417_72ki/management-of-environment-variables-with-yamls-ver-dot-2

おわりに

これで環境ごとに設定を変えてほしい要求が来ても安心です!
他にいい方法があれば、コメントなどで教えていただけると嬉しいです :relaxed:

以上、 Swift/Kotlin愛好会 Advent Calendar 2020 の5日目の記事でした。
翌日も @uhooi の記事です。

参考リンク

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

iCloud・iTunesのバックアップを考慮したUnityのデータ保存における注意点

この記事はPONOS Advent Calendar 2020の18日目の記事です。
昨日は@nisei275さんの「RustとWebSocketでユーザの位置情報を双方通信する仕組みを実装してみる」でした。

はじめに

本記事では、iCloud・iTunesのバックアップにおいてUnityのどのディレクトリが対象・非対象になるのか、
Unityでのデータを保存する上で注意点について紹介していきます。

iCloud・iTunesのバックアップについて

https://support.apple.com/ja-jp/HT204136

iCloudバックアップはiCloudのオンラインストレージ上からバックアップ
iTunesバックアップはPC上からバックアップ
以前は、バックアップできる項目に差異があったようですが、現時点ではほとんど変わりません。

Unityでのバックアップ対象・非対象ディレクトリ

iCloud・iTunesのバックアップでは、アプリ内ディレクトリの一部がバックアップ対象となります。

以下、Unityで主に使用されるディレクトリを該当するiOSアプリのディレクトリを列挙したものとなります。

Unity iOSアプリ バックアップ対象・非対象
Application.persitantDataPath
永続性のあるデータを保存するためのパス
Documents/ 対象
Application.temporaryCachePath
一時的なデータを保存するためのパス
Library/Caches/ 非対象
PlayerPrefs
簡易的にデータの保存・読み込み機能
Library/Preferences/
(NSUserDefaults)
対象

データを保存する上での注意点

バックアップ対象のディレクトリに比較的容量が大きいファイルを含めない

2.23 Apps must follow the iOS Data Storage Guidelines or they will be rejected

AssetBundleや比較的容量が大きいファイルをバックアップ対象に含めてしまうと、
リジェクトされる可能性があります。

Apple側としてはバックアップ必要なものはバックアップディレクトリに含めて、
バックアップ不要なものはバックアップされないディレクトリもしくは、"do not back up"として、
バックアップされないように明示的に指定してほしいといった意図があるようです。
https://developer.apple.com/icloud/documentation/data-storage/index.html

そのためUnityではバックアップ対象から外す設定が提供されております。

#if UNITY_IOS
UnityEngine.iOS.Device.SetNoBackupFlag(Application.persitantDataPath);
#endif

端末一意のデータ、ユーザー認証情報をバックアップ対象に含めない

例えばですが、以下のケースが考えれます。

PlayerPrefsに端末を識別するIDユーザー作成時に作られる情報を保存していた場合。
(PlayerPrefsはiCloud・iTunesのバックアップ対象となっています)

  1. Aの端末でバックアップを作成
  2. Bの端末でAの端末データのバックアップデータを用いて機種変更
  3. Bの端末にはAの端末データのPlayerPrefsが含められている状態

つまり、Aの端末(ユーザーA)= Bの端末(ユーザーA)といったユーザーの複製が行えてしまいます。
これにより1つのユーザーデータに対して複数端末からのアクセスができる状況が発生します。

アプリによっては、これを良しとするものもあるかと思います。
しかし意図しない挙動が発生したり、ユーザーデータの破損にも繋がる可能性もあるため、
予め想定した設計をする必要があります。

例えば、PlayerPrefsに保存している一部の情報をバックアップ対象とされないディレクトリに逃すことや、
サーバーを介してログインを行っているアプリであれば、
ログイン時にトークンを発行してそのトークンと一致している端末のみ
プレイ可能にするといった方法も考えられます。

おわりに

  • 各種プラットフォームでアプリを提供する場合は、プラットフォームごとのディレクトリの用途を予め確認する必要があります
  • 保存する先のディレクトリがバックアップ対象・非対象かを確認し、目的に応じて使い分けることが重要

明日は@MilayYadokariさんの記事です。

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

iOS プッシュ通知の実装に必要な「p12形式の証明書」と「 p8形式の鍵」について

はじめに

私はプッシュ通知のためのMBaaS(mobile backend as a Service)はFirebaseしか使った事がありません。。
なので、基本的にこの記事で書く内容は、Firebase Cloud Messagingを使う事を前提としています!

あらかじめご了承を:grinning:

iOSでプッシュ通知を使うために必要なファイルは2種類ある

iOSアプリのPush通知を実現するには、APNs(Apple Push Notification Service)と連携する必要があります。

で、APNsを利用する際には、認証のために証明書やトークンが必要です。

スクリーンショット 2020-12-15 12.56.26.png
https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server

APNsを利用する際に必要な認証方法が実は2種類あるため、初めて調べた人は非常に混乱するのではないだろか??:sweat:

2種類の認証方法というのがタイトルにもある「p12形式の証明書」と「p8形式の鍵」の事です。

「p12形式の証明書」と「p8形式の鍵」の違い

それぞれの特徴をまとめてみました!

p12

・ファイル取得までの道のりが長くて複雑
・有効期限は1年で、毎年更新する必要がある
・App ID単位で管理ができる
・開発環境・本番環境と区別ができる
・古いやり方
・何度でもダウンロードできる

p8

・ファイル取得までの道のりが短くて簡単
・有効期限は無期限
・App ID単位で管理ができない(Apple IDが保有している全てのアプリで使える)
・開発環境・本番環境と区別がない
・新しいやり方(Firebaseではp8が推奨されている)
同じkeyは1度しかダウンロードできない
Apple IDのApple Developer Programに対して、2つまでしか認証キーが発行できない

補足

p8 keyは「1度しかダウンロードできない」となっていますが、これは「1度失ったら2度とプッシュ通知を使えない」というわけではありません。

Remember to keep the key file safely as you can only download it once, if you lost the key file, you will need to revoke and it and register a new one.
https://fluffy.es/p8-push-notification/

ダウンロードできるのは1回だけなので、キーファイルを安全に保管することを忘れないでください。キーファイルを紛失した場合は、キーファイルを取り消して新しいファイルを登録する必要があります。

同じkeyをダウンロードできるのが1回だけ、という意味で、もし無くしたら現在有効になっているp8 keyを取り消して(Revoke)、再び新しいものを作成してダウンロードすればOKという事です!
スクリーンショット 2020-12-15 14.07.08.png
https://firebase.google.com/docs/cloud-messaging/ios/certs

「p12形式の証明書」の取得方法

プッシュ通知に必要な証明書の作り方2020

「p8形式の鍵」の取得方法

使われてる画像は古いですが、参考になるかと
お手軽firebaseプッシュ通知証明書作成メモ

英語でもいいならこちらが新しい
.p8 Key File For Push Notification

最後に

「どっちが良いか?」を一概に決めることはできず、どちらも一長一短という感じ。。。

ただ、p8が新しいやり方なので、時代が進むとそっちが主流になりそうではある。

もしp8 keyを紛失したら?については下記を参考に:point_down:
iOS - What happens I lost my APNs Key file?
軽く要約しますと
Apple Developer Member Centerで新しいp8 keyを作成(もし既に2つ認証keyを作成していた場合、無くした方をRevokeする)
②Firebaseなどのサーバー側で無くしたp8 keyを削除して、新しいp8 keyを登録

参考

https://takamii.hatenablog.com/entry/2020/07/13/190027

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

Combine導入の戦略について考えてみた

はじめに

  • iOSのバージョンアップに伴いフレームワークも進化してきました。SwiftUI, Combineの登場によって、UIの構築方法の変化、アーキテクチャの変化が必要な時期になってきました。しかし、既存のプロダクトを全て書き換えるのはすぐにはできないです。今回は新しいフレームワークに徐々に移行していくための戦略に追加考えてみました。

CombineとSwiftUI

  • 新しく登場したSwiftUIですがいきなり複雑なUIをSwiftUIで実装するにはハードルが高いと思います。なぜなら、SwiftUIが登場して、2年ほどしか立っておらず、フレームワークの進化の途中であり、iOSのバージョンで使えるAPIの差分が大きいと感じます。
  • Combineに関して、すでにRxSwiftなどのReactive Programmingで実装する土壌がある程度整っていて、UIKitとSwiftUIどちらでも対応可能です。 さらにRxSwiftなどをすでに使っていれば、学習コストが低く、既存のものから置き換えやすいと考えられます。
  • 以上の結果からCombineの導入で話を進めます。

Combine導入

Target versionをiOS13に変更する

⭕️

  • サポートバージョンをiOS13にすれば問題なく使えることができます。

  • 運用中のサービスであると関係者の了解をとらないといけないです

OpenCombineを入れる

⭕️

  • 導入が容易
  • iOS13以下でも動作する
  • Combineと実装は同じような実装はできる

  • ライブラリとして追加するのでバイナリサイズが余計に増え
  • メンテナンスが継続的にされるかわからない

必要なところだけ自作してみる

⭕️

  • 自作することで内部実装まで理解することが容易になります。
  • OpenCombineを使うより、影響範囲を自分でコントロールできる

  • 自作するので実証者によっては学習コストがかかる

Combine導入後

参考リンク

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

iOS開発でのCombine導入の戦略について考えてみた

はじめに

  • iOSのバージョンアップに伴いフレームワークも進化してきました。SwiftUI, Combineの登場によって、UIの構築方法の変化、アーキテクチャの変化が必要な時期になってきました。しかし、既存のプロダクトを全て書き換えるのはすぐにはできないです。今回は新しいフレームワークに徐々に移行していくための戦略に追加考えてみました。

CombineとSwiftUI

  • 新しく登場したSwiftUIですがいきなり複雑なUIをSwiftUIで実装するにはハードルが高いと思います。なぜなら、SwiftUIが登場して、2年ほどしか立っておらず、フレームワークの進化の途中であり、iOSのバージョンで使えるAPIの差分が大きいと感じます。
  • Combineに関して、すでにRxSwiftなどのReactive Programmingで実装する土壌がある程度整っていて、UIKitとSwiftUIどちらでも対応可能です。 さらにRxSwiftなどをすでに使っていれば、学習コストが低く、既存のものから置き換えやすいと考えられます。
  • 以上の結果からCombineの導入で話を進めます。

Combine導入

Target versionをiOS13に変更する

⭕️

  • サポートバージョンをiOS13にすれば問題なく使えることができます。

  • 運用中のサービスであると関係者の了解をとらないといけないです

OpenCombineを入れる

⭕️

  • 導入が容易
  • iOS13以下でも動作する
  • Combineと実装は同じような実装はできる

  • ライブラリとして追加するのでバイナリサイズが余計に増え
  • メンテナンスが継続的にされるかわからない

必要なところだけ自作してみる

⭕️

  • 自作することで内部実装まで理解することが容易になります。
  • OpenCombineを使うより、影響範囲を自分でコントロールできる

  • 自作するので実証者によっては学習コストがかかる

Combine導入後

参考リンク

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

非エンジニアに読んでほしい、iOSアプリクラッシュレポートの話

この記事はプロジェクトマネージャーなど、非iOSエンジニアの方に知って欲しい

  • アプリのクラッシュとは?
  • アプリがクラッシュしたことを知るには?
  • その仕組みを利用する上で知っておくべきことは?

これらのことを書きます。

アプリのクラッシュとは

アプリを操作中に突然ホーム画面に移動してしまう現象です。
アプリが継続不可となり強制終了する状態です。
ユーザー体験を大きく損なうため避けるべきエラーです。

アプリがクラッシュしたことを知るには?

サーバーサイドのエラーであればログを手に入れることは可能ですが、アプリユーザーにログ提出を依頼するのは現実的ではありません。
Appleが提供している仕組みでクラッシュレポート (アプリがクラッシュしたこと、及びクラッシュした経緯) を収集することは可能ではありますが
全てのクラッシュを知ることはできません。

※補足
https://support.apple.com/ja-jp/HT202100

Firebase Crashlytics

Appleが提供する仕組みでは全てのクラッシュレポートを取得できないため、サードバーティの仕組みを利用することが一般的です。
Firebase Crashlyticsはその代表と言える、広く利用されているサービスです。
クラッシュレポートの収集に関しては無料で利用可能です。

Firebase Crashlyticsを利用する上で知っておくべきことは?

プライバシーポリシーや利用規約

アプリのプライバシーポリシーや利用規約に「Crashlyticsを利用して情報を収集する」旨を記述するか検討する必要があります。
(ほとんどのアプリは明記されています)
AppleのアプリReviewガイドラインにも個人情報収集に関する記載があります。
法務担当とよく相談しておくべき事項です。

dSYMファイルをFirebaseに送信しなければならない

AppStoreを経由したアプリの配信時は、dSYMというファイルをAppStoreConnectからダウンロードしFirebaseへ送信する必要があります。
この操作は新しいバージョンをリリースするたびに毎回必要な作業になります。
この操作を忘れるとクラッシュレポートは収集できません。
コマンドラインツールの利用などが必要なため、エンジニアに相談しましょう。
※補足
ENABLE_BITCODEがデフォルト値のままtrueになっている前提です

それでも検出できないクラッシュがある

アプリがクラッシュレポートを送信するタイミングはクラッシュ後、つぎにアプリを起動したときです。
よって、クラッシュ後もう2度とアプリを起動しなかった場合はクラッシュレポートが収集できません。
また、起動直後に繰り返しクラッシュするような状況でもクラッシュレポートは収集できません。 (ある程度アプリの実装に左右されます)

最後に

アプリのクラッシュは避けるべきですが、完全になくすことは困難です。
クラッシュが起こったことを知り、修正していくことは重要です。
この記事に書いたクラッシュレポート収集に関する事柄を、予めプロジェクト計画に組み込んでおく必要があると私は考えます。

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

iOSのCoroutineについて(Async/Await)

Abstract

現在iOSの非同期処理は一般的にGCDを使うのです、closure或いはdelegateでコールバックして、処理を追うんだが。関係性がある複数の非同期処理を実行する場合、コールバック地獄になる。

例えば、以下の画像処理のコードがある。

func fetchImage(url: String, completeHandler: @escaping (UIImage) -> ()) {
    ...
    completeHandler(image)
}

func imageDetectFace(image: UIImage, completeHandler: @escaping (UIImage) -> ()) {
    ...
    completeHandler(image)
}

func imageAddFilter(image: UIImage, completeHandler: @escaping (UIImage) -> ()) {
    ...
    completeHandler(image)
}

使う時は以下のようになる。

fetchImage(url: "hoge/url") { (originImage) in
    imageDetectFace(image: originImage) { (faceImage) in
        imageAddFilter(image: faceImage) { (filterImage) in
            imageView.image = filterImage
        }
    }
}

処理を増えると、可読性とメンテナンス性がどんどん下がる。

Introduction

上記の問題の解決策として、RxSwiftとCombineなどのライブラリを使って、リアクティブプログラミングでコールバックを撲滅するのが一番多いですが。リアクティブプログラミングのデメリットとして、debugが大変なので。
今回はcoobjcというライブラリを使って、Coroutine風に非同期処理してみる。

Coobjc

Coobjcは中国のアリババグループがOSSしたiOS用のCoroutineライブラリです。このライブラリはアセンブリとCで開発して、Objc-CとSwiftをサポートしている。このライブラリを通して、kotlin、JavaScriptやPythonなどみたいに、Coroutine風でプログラミングできる。
Github: https://github.com/alibaba/coobjc

Usage

coobjcはAsync/AwiatとGeneratorとActorなどのCoroutine機能をサポートしている。今回はAsync/Awiatだけ使ってみる。

Async/Awiat

上記のコールバック地獄のコードを、Async/Awiatを使ってCoroutine風に書き直すと、↓になる。

func fetchImage(url: String) -> Promise<UIImage> {
    return Promise(on: DispatchQueue.global()) { (fullFill, reject) in
        fullFill(UIImage())
    }
}

func imageDetectFace(image: UIImage) -> Promise<UIImage> {
    return Promise(on: DispatchQueue.global()) { (fullFill, reject) in
        fullFill(UIImage())
    }
}

func imageAddFilter(image: UIImage) -> Promise<UIImage> {
    return Promise(on: DispatchQueue.global()) { (fullFill, reject) in
        fullFill(UIImage())
    }
}

使う時は以下のようになる。

co_launch(queue: DispatchQueue.main) {
    var image: UIImage = UIImage()
    let imgResult = try await(promise: self.fetchImage(url: "url"))
    if case .fulfilled(let fetchedImg) = imgResult {
        image = fetchedImg
    } else {
        return
    }

    let faceResult = try await(promise: self.imageDetectFace(image: image))
    if case .fulfilled(let faceImg) = faceResult {
        image = faceImg
    } else {
        return
    }

    let filterResult = try await(promise: self.imageAddFilter(image: image))
    if case .fulfilled(let filterImg) = filterResult {
        imageView.image = filterImg
    }
}

これで非同期処理が終わったらMainThreadに戻り、imageViewを更新する。GCDとコールバックもすっきりになりました。

Conclusion

もしiOSで、Coroutine風にプログラミングしたい場合は、Coobjcが結構役に立てると思いますけど、プロジェクトへの侵入性がちょっと強くて、やめたい時は面倒になる。

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

Mintをプロジェクトディレクトリにインストールする

はじめに

Swift製コマンドラインツールのMintですが、複数のプロジェクトで使用されているビルドマシンにインストールせずにプロジェクトディレクトリ内にインストールし、プロジェクトごとに管理する様にしてみました。

Mintとは

mint_logo.png
https://github.com/yonaskolb/Mint

Swift製コマンドラインツールのインストールと実行を行うパッケージマネージャーです。
以下の様なパッケージを管理できます。

  • XcodeGen
  • SwiftLint
  • SwiftGen
  • Carthage
  • LicensePlist

普通にインストールした場合は、
packageは/usr/local/lib/mintにインストールされ、
packageをbuildしたcommandのリンクは/usr/local/binにインストールされます。

プロジェクトディレクトリにインストール

MintのREADME.mdによると、以下の環境変数でインストール先を指定できるという事なのでこれらを使用します。

  • MINT_PATH : packageのインストール先ディレクトリ
  • MINT_LINK_PATH : packageをbuildしたcommandのリンクのインストール先ディレクトリ

1. Mintのディレクトリを作成しクローンする

プロジェクトディレクトリ内にMint用のディレクトリを作成しその中にクローンします。

$ mkdir mint
$ tree
.
├── Mintfile
└── mint

1 directory, 1 file
$ cd mint
$ git clone https://github.com/yonaskolb/Mint.git
Cloning into 'Mint'...
remote: Enumerating objects: 36, done.
remote: Counting objects: 100% (36/36), done.
remote: Compressing objects: 100% (28/28), done.
remote: Total 1952 (delta 10), reused 17 (delta 6), pack-reused 1916
Receiving objects: 100% (1952/1952), 357.89 KiB | 669.00 KiB/s, done.
Resolving deltas: 100% (1113/1113), done.
$  cd ..
$ tree
.
├── Mintfile
└── mint
    └── Mint
        ├── CHANGELOG.md
        ├── LICENSE
        ├── Makefile
        ├── Package.resolved
        ├── Package.swift
        ├── README.md
        ├── Sources
        │   ├── Mint
        │   │   └── main.swift
(一部省略)
10 directories, 42 files

2. パスを指定してインストール

MINT_PATH, MINT_LINK_PATHを指定してインストールする

$ cd mint/Mint
$ export MINT_PATH="../lib" MINT_LINK_PATH="../bin"
$ swift run mint install yonaskolb/mint
Fetching https://github.com/jakeheis/SwiftCLI.git
(一部省略)
[70/70] Linking mint
? Finding latest version of mint
? Cloning mint 0.16.0
? Resolving package
? Building package
? Installed mint 0.16.0
? Linked mint 0.16.0 to /Users/User_Name/projects/mint_test/mint/bin
$ cd ../..
$ tree
.
├── Mintfile
└── mint
    ├── Mint
    │   ├── CHANGELOG.md
 (一部省略)
    ├── bin
    │   └── mint -> /Users/User_Name/projects/mint_test/mint/lib/packages/github.com_yonaskolb_mint/build/0.16.0/mint
    └── lib
        ├── metadata.json
        └── packages
            └── github.com_yonaskolb_mint
                └── build
                    └── 0.16.0
                        └── mint

16 directories, 45 files

3. シンボリックリンクを変更

上のログを確認してもらうとmint/bin/mintのリンクが絶対パスになっているのが分かると思います。
ビルドマシンなどで実行できなくなるので、これを相対パスに変更します。

$ cd mint/bin
$ export mint_bin_path=$(find ../lib/packages/github.com_yonaskolb_mint/build/*/mint)
$ ln -sf $mint_bin_path mint
$ tree
.
└── mint -> ../lib/packages/github.com_yonaskolb_mint/build/0.16.0/mint

4. Packagesをインストール

Mintfileに設定してあるpackageをインストールします。

$ export MINT_PATH="mint/lib" MINT_LINK_PATH="mint/bin"
$ mint/bin/mint bootstrap
? Cloning SwiftLint 0.41.0
? Resolving package
? Building package
? Installed SwiftLint 0.41.0
? Cloning XcodeGen 2.18.0
? Resolving package
? Building package
? Copying resources for XcodeGen: SettingPresets ...
? Installed XcodeGen 2.18.0
? Installed 2/2 packages
$ mint/bin/mint run xcodegen --version
Version: 2.18.0

シェルスクリプト

MINT_PATH, MINT_LINK_PATHを指定してインストールしたり実行するのが面倒なのでシェルスクリプトを作成しました。
https://github.com/yd2x/mint_sh

使用方法:
$ tree
.
├── Mintfile
├── mint
└── mint.sh

1 directory, 2 files

[インストール]
$ sh mint.sh --install    

[packageインストール]
$ sh mint.sh bootstrap

[package実行]
$ sh mint.sh run xcodegen

References

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

[swift5]URLを指定してアプリから外部サイトへ遷移する方法

投稿のポイント

今回は、開発中のアプリケーションから開発者のSNSやホームページに遷移する機能を紹介します。

実装コード

ViewController.swift
// url = 遷移したいサイトのURLをString型で指定
let url = NSURL(string: "")

if UIApplication.shared.canOpenURL(url! as URL) {
  UIApplication.shared.open(url! as URL, options: [:], completionHandler: nil)
}

これでOK!
ボタンやTableViewのCellをタップすると遷移するようにしてみるとシンプルでユーザーも直感的に理解しやすいかなと思います。

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

App Clip Code Generator の使い方(テンプレート?一覧つき)

AppClipCodes.png

2020年12月15日、iOS 14.3 がリリースされ、App Clip の起動方法の1つである、「App Clip Code」が利用できるようになりました。

App Clip Codes
The best way for your users to discover your App Clip. It’s visually beautiful and distinct, so when someone sees one, they’ll know there’s an App Clip waiting for them. Each App Clip code encodes a URL and incorporates an NFC tag, so the code can be tapped on or scanned by the camera. Tools for creating these new codes will be available later this year.

App Clips - Apple Developer https://developer.apple.com/app-clips/

ここで言及されている、App Clip Code を作成するためのコマンドラインツール、「App Clip Code Generator」の使い方を確認していきましょう。

目次

App Clip Code Generator をダウンロード & インストール

まずは、App_Clip_Code_Generator.dmgApple Developer の Download - More からダウンロードします。

App_Clip_Code_Generator.dmg がダウンロードできたら、AppClipCodeGenerator.pkg をインストーラで開いて、画面の指示に従います。

スクリーンショット 2020-12-15 3.52.27.png

デフォルトで /Library/Developer/AppClipCodeGenerator/ に App Clip Code Generator がインストールされます。

ターミナルで App Clip Code Generator を実行する

では AppClipCodeGenerator -help をターミナルで実行し、どのようなことができるかを見てみましょう。

スクリーンショット 2020-12-15 3.55.14.png

% AppClipCodeGenerator -help
AppClipCodeGenerator tool.

Usage:
    AppClipCodeGenerator generate --url URL --foreground COLOR --background COLOR [--type TYPE] [--logo LOGO_TYPE] --output FILE_PATH
    AppClipCodeGenerator generate --url URL --index INDEX [--type TYPE] [--logo LOGO_TYPE] --output FILE_PATH
    AppClipCodeGenerator templates
    AppClipCodeGenerator suggest --foreground COLOR --background COLOR
    AppClipCodeGenerator --version

Options:
    -u URL, --url URL               The URL to be used by the App Clip Code
    -f COLOR, --foreground COLOR    A color in its six-digit hexadecimal representation. For example, pass FFFFFF to use white as the App Clip Code’s foreground color
    -b COLOR, --background COLOR    A color in its six-digit hexadecimal representation. For example, pass 000000 to use black as the App Clip Code’s background color
    -i INDEX, --index INDEX         The index of the predefined template color to use. Use in place of foreground and background colors. For example, pass 1 to use the template color with index 1. To see the list of 'template color' pairs, use the templates command
    -o FILE_PATH, --output FILE_PATH  The location where you want the App Clip Code Generator app to save the generated App Clip Code’s SVG file
    -t TYPE, --type TYPE            Accepts: 'cam' (default), 'nfc'. Specify the App Clip Code’s type: use 'cam' for scan-only codes and 'nfc' for NFC-enabled codes
    -l LOGO_TYPE, --logo LOGO_TYPE  Accepts: 'none', 'badge' (default). To generate an App Clip Code without a badge, pass 'none'. To generate an App Clip Code inside a badge, pass 'badge'.
    -v, --version                   Displays the version of the tool

テンプレートを使って App Clip Code を生成する

最も手早く App Clip Code を生成する一例は、以下のとおりです。

% AppClipCodeGenerator generate -u URL -i INDEX -o FILE_PATH

例えば、https://japannfcreader.tret.jp の App Clip Code を code.svg として生成するには、

% AppClipCodeGenerator generate -u https://japannfcreader.tret.jp -i 0 -o code.svg

となります。実行後、App Clip Code successfully generated. と表示され、code.svg が生成されていれば成功です。実際に生成できた App Clip Code はこちらです。TYPELOGO_TYPE を指定しなかったので、それぞれ cambadge になっています。

スクリーンショット 2020-12-15 4.01.12.png

App Clip Code のテンプレートを確認する

先ほど生成した App Clip Code ですが、オプションで INDEX を渡す必要がありました。この INDEX には予め定義された foregroundCOLORbackgroundCOLOR の組み合わせを指定することになっています。この INDEXAppClipCodeGenerator templates で確認することができます。

% AppClipCodeGenerator templates                                                  
Use any of the following predefined color pairs for your App Clip Code by using the `generate` command and passing a color pair’s index to the -i --index option. For example pass `-i 1` to use the color template with the index 1.
Index: 0 Foreground: FFFFFF Background: 000000
Index: 1 Foreground: 000000 Background: FFFFFF
Index: 2 Foreground: FFFFFF Background: 777777
Index: 3 Foreground: 777777 Background: FFFFFF
Index: 4 Foreground: FFFFFF Background: FF3B30
Index: 5 Foreground: FF3B30 Background: FFFFFF
Index: 6 Foreground: FFFFFF Background: EE7733
Index: 7 Foreground: EE7733 Background: FFFFFF
Index: 8 Foreground: FFFFFF Background: 33AA22
Index: 9 Foreground: 33AA22 Background: FFFFFF
Index: 10 Foreground: FFFFFF Background: 00A6A1
Index: 11 Foreground: 00A6A1 Background: FFFFFF
Index: 12 Foreground: FFFFFF Background: 007AFF
Index: 13 Foreground: 007AFF Background: FFFFFF
Index: 14 Foreground: FFFFFF Background: 5856D6
Index: 15 Foreground: 5856D6 Background: FFFFFF
Index: 16 Foreground: FFFFFF Background: CC73E1
Index: 17 Foreground: CC73E1 Background: FFFFFF

先ほどは

% AppClipCodeGenerator generate -u https://japannfcreader.tret.jp -i 0 -o code.svg

のように INDEX0 を指定したので、Index: 0 Foreground: FFFFFF Background: 000000 の色の組み合わせが使用されました。例えば INDEX11 にすると、このような App Clip Code が生成されます。また、このページの下部に、用意されている18種類のテンプレートを使って生成した App Clip Code のサンプルを掲載します。

TYPE を指定する

TYPE には camnfc のどちらかを指定します。NFC に対応した App Clip Code の場合は nfc を指定し、そうでない場合は cam を指定します。未指定の場合はデフォルトで cam です。

TYPE 生成結果
cam(デフォルト) スクリーンショット 2020-12-15 4.30.41.png
nfc スクリーンショット 2020-12-15 4.30.47.png

LOGO_TYPE を指定する

LOGO_TYPE には nonebadge を指定できます。バッジなしの App Clip Code のみとしたい場合は none を指定します。未指定の場合はデフォルトで badge です。

LOGO_TYPE 生成結果
none スクリーンショット 2020-12-15 4.36.10.png
badge(デフォルト) スクリーンショット 2020-12-15 4.36.16.png

COLOR を指定する

すでに18種類の foregroundbackground カラーの組み合わせが用意されていますが、それらを自分で設定したい場合には6桁の16進数表現で指定します。

例えば、foreground#FFFFFFbackground に Google+ のカラーコード #DD4B39 を用いる場合は、

% AppClipCodeGenerator generate -u https://japannfcreader.tret.jp -f FFFFFF -b DD4B39 -o code_custom_google+color.svg

と実行し、生成結果はこのようになります。

それでは、foreground はそのまま #FFFFFFbackground には Qiita のヘッダーのカラーコード #55C500 を指定してみましょう。すると…

% AppClipCodeGenerator generate -u https://japannfcreader.tret.jp -f FFFFFF -b 55C500 -o code_custom.svg       
Color combination not supported: The given color pair is invalid. To have the App Clip Code Generator suggest alternative color combinations, use the suggest command.

「その色の組み合わせはダメだよ」と怒られてしまいました。しかし、App Clip Code Generator は代替の色の組み合わせを提案してくれるようです。

「Color combination not supported」と言われたとき、代わりの色の組み合わせを提案させる

前セクションで、foreground#FFFFFFbackground には Qiita のヘッダーのカラーコード #55C500 を指定して App Clip Code を生成しようとしたところ、「Color combination not supported」と怒られてしまいました。そのようなときは AppClipCodeGenerator suggest で代わりの色の組み合わせを提案してもらいましょう。

% AppClipCodeGenerator suggest -f FFFFFF -b 55C500           
Foreground: 000000 Background: 55C500
Foreground: 774466 Background: 55C500
Foreground: 990022 Background: 55C500
Foreground: 111111 Background: 55C500
Foreground: 443344 Background: 55C500
Foreground: 881100 Background: 55C500
Foreground: 222222 Background: 55C500

提案された色の組み合わせの中で、私は foreground#222222background#55C500 とするものを気に入りました。こちらの色の組み合わせを指定し直して、生成してみましょう。

% AppClipCodeGenerator generate -u https://japannfcreader.tret.jp -f 222222 -b 55C500 -o code_custom_qiitacolor.svg

付録 - App Clip Code Generator のテンプレート一覧(スクリーンショットつき)

INDEX foreground background 生成結果
0 #FFFFFF #000000 スクリーンショット 2020-12-15 4.09.13.png
1 #000000 #FFFFFF スクリーンショット 2020-12-15 4.09.20.png
2 #FFFFFF #777777 スクリーンショット 2020-12-15 4.09.26.png
3 #777777 #FFFFFF スクリーンショット 2020-12-15 4.09.30.png
4 #FFFFFF #FF3B30 スクリーンショット 2020-12-15 4.09.35.png
5 #FF3B30 #FFFFFF スクリーンショット 2020-12-15 4.09.40.png
6 #FFFFFF #EE7733 スクリーンショット 2020-12-15 4.09.45.png
7 #EE7733 #FFFFFF スクリーンショット 2020-12-15 4.09.50.png
8 #FFFFFF #33AA22 スクリーンショット 2020-12-15 4.09.55.png
9 #33AA22 #FFFFFF スクリーンショット 2020-12-15 4.09.59.png
10 #FFFFFF #00A6A1 スクリーンショット 2020-12-15 4.10.03.png
11 #00A6A1 #FFFFFF スクリーンショット 2020-12-15 4.10.08.png
12 #FFFFFF #007AFF スクリーンショット 2020-12-15 4.10.13.png
13 #007AFF #FFFFFF スクリーンショット 2020-12-15 4.10.18.png
14 #FFFFFF #5856D6 スクリーンショット 2020-12-15 4.10.21.png
15 #5856D6 #FFFFFF スクリーンショット 2020-12-15 4.10.25.png
16 #FFFFFF #CC73E1 スクリーンショット 2020-12-15 4.10.29.png
17 #CC73E1 #FFFFFF スクリーンショット 2020-12-15 4.10.36.png

まとめ

  • App Clip Code Generator はローカルの Mac 上で簡単に App Clip Code を生成できるコマンドラインツールです。
  • あらかじめ色の組み合わせが用意されており、もちろん、好きな色の組み合わせを指定することもできます。
  • App Clip Code の読みとりに問題が発生しそうな色の組み合わせが指定された場合、コードの生成が拒否されます。
  • その場合でも代わりの色の組み合わせの提案をしてもらうことができます。

それでは、ハッピー App Clip ライフを。

環境

この記事は以下の環境で動作を確認しました。

% AppClipCodeGenerator -v
Version: 1.3.6
  • macOS Big Sur バージョン 11.0.1(20B29)
  • MacBook Pro (13-inch, M1, 2020)
  • Darwin Kernel Version 20.1.0: Sat Oct 31 00:07:10 PDT 2020; root:xnu-7195.50.7~2/RELEASE_ARM64_T8101 arm64
  • Darwin Kernel Version 20.1.0: Sat Oct 31 00:07:10 PDT 2020; root:xnu-7195.50.7~2/RELEASE_ARM64_T8101 x86_64

arm64 と、Rosetta を使用した x86_64 で動作を確認しています。

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