20200702のiOSに関する記事は10件です。

UIImagePickerControllerに代わるPHPickerViewControllerの紹介

WWDC 2020のMeet the new Photos pickerを見たのでメモです。

※ここに出てくるスクリーンショットは、全て上記の動画のものです。

概要

UIImagePickerControllerに代わるピッカーの紹介。このPHPickerを使えばPhoto Libraryへの完全なアクセスがなくても写真や動画を選択することができる。

PHPickerとは

ユーザのPhoto Library内の写真や動画にアクセスすることができるシステム標準のピッカー。

PHPickerには以下の特徴がある。

  • 検索ができる
  • グリッド表示時にズームイン/アウトができる
  • 写真や動画にアクセスするのに一番良い方法
  • まったく新しいデザイン
  • 使いやすいAPI
  • 複数選択ができる
  • フィルター選択が可能

プライバシーへの配慮

写真ピッカーを表示するときに直接Photos Libraryにアクセスを要求する必要がないので、許諾アラートを出す必要もない。

内部の仕組み

PHPickerはアプリ内部で動作しているように見えるが、実際は別プロセスで動いている。
スクリーンショット 2020-07-02 10.16.00.png
アプリはピッカーに直接アクセスすることも、ピッカーコンテンツのスクショを撮ることもできない。ユーザが選択したものだけ、アプリ側は知ることができる。

新しいAPI

PHPickerViewControllerはPHPickerFilterなどの設定を渡されて初期化を行う。ユーザが何かを選択したら、PHPickerは結果を保持しておく用のスペースを作成し、delegateに渡す。
スクリーンショット 2020-07-02 11.06.17.png

コード

まず設定について。選択上限はselectionLimitで指定できる。デフォルトでは1が設定されており、0にすると無制限にできる。アイテムタイプのフィルタリングはfilterで行うことができる。タイプ指定は1つでも、複数でもできる。
スクリーンショット 2020-07-02 11.06.50.png

設定を詰め込んだらPHPickerViewControllerにその情報を渡して初期化する。あとはdelegateを設定して、presentで表示するだけ。
スクリーンショット 2020-07-02 11.14.31.png

ユーザがアイテムを選択し終わったらdidFinishPickingが呼ばれる。まずはピッカーを閉じ、次にNSItemProviderに結果を聞き、期待するオブジェクトがあったら処理をする。このItemProviderは非同期で、エラーが発生したときの処理もここで書く必要がある。
スクリーンショット 2020-07-02 11.19.52.png

デモ

サンプルコードページにあるアプリを使用)

真ん中にUIImageViewのplaceholder、右上に+ボタンがある。今は+ボタンをタップしても何も起きない。
やるべきことは2ステップ。まずは1枚の画像をImageViewに表示できるように、+ボタンがタップされたらピッカーを表示し、選択した写真を貼り付ける処理を書く。次のステップとして、画像が複数枚選択されたときは、画面をタップ時に次の画像を表示するような処理を書く。

受け取った画像(1枚)のハンドリング例は以下の通り。
スクリーンショット 2020-07-02 12.59.32.png

次に複数選択対応をする。itemProvidersで取得したアイテム全てが、iteratorで現在表示されている画像情報がわかるようにする。
スクリーンショット 2020-07-02 14.16.56.png
次の画像を表示するメソッドはこのように書く。
スクリーンショット 2020-07-02 15.36.38.png
単一選択と異なるのはiteratorを使っているかどうかと、delegateの部分。
スクリーンショット 2020-07-02 15.49.00.png
実行するとこのような表示になる。画面下部のボタンをタップすると、今どんな写真がステージされているかがわかる。全ての写真を最初にロードしているわけではないので、読み込み速度は単一のときと変わらない。
スクリーンショット 2020-07-02 15.51.41.png

PHPicker x PhotoKit

PhotoKitベースのアプリの中には、PHAssetsを取得したいと思っているかもしれない。
PhotoKitを使うことでメリットが得られるアプリは、元画像を保持しつつ画像編集を行うアプリや、Photo Libraryの機能を使ったりするアプリ。とはいえPhotos Libraryへのアクセスは必要な場合のみに限定する必要はあるし、アクセスが拒否されたパターンの処理もする必要がある。

実装としては最初にPHPhotoLibraryを使ってPickerを初期化する。callbackを受け取ったらassetIdentifierを取得し、PhotoKitを使ってassetにアクセスできるようにする。
スクリーンショット 2020-07-02 16.26.02.png

Limited Photos Library

iOS 14ではLimited Photos Libraryという新しい機能が使える。もしPhotoKitを使っていなければ特に心配する必要はない。Photo Libraryへのアクセスを求めるアプリでは、既存の選択肢に加え、Photos Libraryの一部のPHAssetsアクセスが許される選択肢が増える。
Limited Phots Libraryを使う場合、注意しなければいけないことがある。ピッカーはPhotos Libraryの全体を表示し、ユーザは全ての写真、動画を選択できるが、ピッカーで何を選択してもアクセスできるPHAssetsは変わらない。より詳しい情報は「Handle the Limited Photos Library in Your App」セッションを参照する。

deprecatedなAPI

AssetsLibraryは数年前からdeprecatedになっているので、PhotoKitに乗り換えを。
新しい発表としては、UIImagePickerControllerの写真ライブラリ部分もdeprecatedになる。代わりにPHPickerViewControllerを採用することをおすすめする。

まとめ

  • 自分で写真動画を選択するピッカーを作るのではなく、PHPickerを採用することをおすすめする
  • PHPickerは複数選択ができる
  • PHPickerだと写真アプリと同じで一貫したUIを提供できるので、ユーザは操作方法がわかっている
  • ピッカーを表示する前に写真ライブラリへのアクセスを要求しない
  • アプリがPhotoKitを利用して写真ライブラリにアクセスしている場合、今一度ライブラリへのアクセスが必要なのか考えたり、代わりにPHPickerを使うようにする
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アラート表示

アラートを表示する

swift
 SimpleAlert.showAlert(viewController: self, title: "タイトル", message: "内容を描いてください", buttonTitle: "OK")

アラート表示

swift
import UIKit
struct SimpleAlert {
    static func showAlert(viewController: UIViewController, title: String, message: String, buttonTitle: String) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        let okAction = UIAlertAction(title: buttonTitle, style: .default) { (action) in
            alert.dismiss(animated: true, completion: nil)
        }
        alert.addAction(okAction)
        viewController.present(alert, animated: true, completion: nil)
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Flutter】メモアプリ開発で使ったオススメのパッケージを紹介

メモアプリ作成で作ったFltutterパッケージ

iphone11top.jpg

今こそ「ひらめきたい」というあなたに!

6月末にFlutterでアプリ開発を始めてから2作目のアプリ、アイデアを発想するためのメモアプリ「IdeaShuffleMemo(アイデアシャッフルメモ)」をリリースしました。

■AppStore
https://apps.apple.com/jp/app/id1517535550

■Google Play
https://play.google.com/store/apps/details?id=com.IdeaShuffleMemoApp&hl=ja

■アプリの詳細記事
https://yukio.site/idea_shuffle_memo/

1作目は、とりあえずアプリをリリースすることを主眼にしていたため、技術的に実装が難しそうなところは省きました。
2作目はステップアップのために、

  • 画面のパスワードロック
  • DBのバックアップ・リストア
  • 有料課金(サブスクリプション)など

いろいろな機能実装に挑戦的に取り組みました

この3つの機能実装について詳細を書くと、とっても長くなりますので、詳細は別の記事でまた書く予定です 泣汗汗汗

この記事では、メモアプリ作成で使って良かったFlutterパッケージについて紹介していきたいと思います。

データベースへの保存

moor | Dart Package

Flutter開発でデータベース(DB)が使えるようにするパッケージが、Moorです。
IMG_9925.jpg

最初に学習したFlutter教材がmoorを採用していたのでDB操作のパッケージはこれ一択だったのですが、一通りやりたいことはやれるので満足しています。

日本語の情報は少ないですが、英語ではしっかりした資料もあったり、Youtubeもあったりと情報は充実していると思います。(Google翻訳があれば問題ありません・・・白目。)

Getting Started | Moor

ただパッケージを入れたからといって簡単に扱えるわけではなく、プログラミングの完全な初心者だったり、RubyonRailsしかやったことがない人にとってはちょっと難しいも。

Railsのように至れり尽くせりではないので、Rails感覚でやると辛いです。

初期設定やデータの保存・取得など、それなりに資料とにらめっこしながら勉強する必要があるかもしれません。

「DB?余裕っしょ」とナメていると、ヒョードルにタコ殴りされた気分になります。

慣れてくれば、それなりに使いやすいと思います。

派手目のダイアログ

awesome_dialog | Flutter Package

今まで更新完了などの表示はToastを使っていたのですが、いかんせん地味で、表示されているのに気づかなかったり。

awesome_dialogであれば、簡単に大阪のおばちゃん並の主張をする、派手なダイアログを表示することができます。
IMG_9924.jpg

実装は、公式ドキュメントを見れば簡単にできると思います。

OKやCANCELのボタン、ボタンの色などカスタマイズもかなり自由にできます。

ただ・・・ダイアログが自動的に消えてくれる機能がありません。
@tepci さんにご指摘頂いたのですが、現在は消せるようになっていました!ありがとうございます。
この記事のコメントに書いて頂いてますので、そちらをご覧ください。

ダイアログを消すために毎回ボタンをを押すのは、メンディーですよね。

〜〜〜〜古い内容です〜〜〜〜
もう、半分が優しさでできていないバファリン並にユーザーにとって好ましい状況でありません。

「作成しました〜」っていう表示をした後に、数秒後に消えてくれるか、ダイアログの外側をタップしたら消えてくれるほうがいいですよね。

Timer t = null;
AwesomeDialog(context: context,
  dialogType: DialogType.SUCCES,
  animType: AnimType.BOTTOMSLIDE,
  tittle: '追加したぜ〜〜〜',
  desc: '',//何か文章入れたければ
  onDissmissCallback: () async {
    Navigator.pop(context); //編集画面を閉じるため
    t.cancel(); //キャンセル処理
    //やりたい処理
  },
).show();
t = Timer(Duration(seconds: 3), () {
  Navigator.pop(context);//編集画面を閉じるため
  //やりたい処理
});

いろいろやり方はあると思いますが、こんな感じにしてみました。
何もしない場合、タイマーで3秒後に消えるようにしています。
ただし、ダイアログの外側をタップ後にタイマーをキャンセルしないと、トラップカードのごとく処理が2重で発動してしまいます。
なので、画面の外側を押した時に、「 t.cancel(); 」でタイマーをキャンセルしています。
〜〜〜〜古い内容終了〜〜〜〜

アイコン「font_awesome_flutter」

font_awesome_flutter | Flutter Package

IMG_9926.jpg

マテリアルアイコンでもいいのですが、ぴったりのアイコンとか見つからないことがあります。
今回バックアップで、GoogleDriveを使ったのですが、アイコンでいちいち画像を表示させるのがメンディーです。
なので、各サービスのアイコンとか使える「font_awesome_flutter」を使ってみました。
使えるアイコンが増えるので、いいですね。
こんな感じで、簡単に呼び出せます。

icon: FaIcon(FontAwesomeIcons.google),

バイブレーションの通知「vibrate」

vibrate | Flutter Package

Webサービスだとあまり気にならないのですが、アプリの場合、更新とか保存など、ちょっとした処理の時にフィードバックがないと不安になったりしません?

サバンナの八木さんみたいに「スマホさん動いてますか?」って聞きたくなりますよね。

ユーザーに、「処理しているよ〜」とか、「処理終わったよ〜」って分かりやすくする方が親切なので、バイブレーションの機能を実装しました。
こんな感じで簡単に、よきタイミングでコードを差し込めば、スマホを震わせることができます。

Vibrate.*feedback*(FeedbackType.medium);

バイブレーションのタイプも色々あります。

  success,
  error,
  warning,
  selection,
  impact,
  heavy,
  medium,
  light

私は面倒なので、mediumオンリーで使ってます。

スマホを振ると動く機能

shake | Flutter Package

せっかくスマホなので、スマホだからこそできる機能をつけたい。

スマホを振ると、処理を発動できるパッケージが、ブギーな胸騒ぎしそうな「shake」です。

このパッケージを発動する方法が2つあって。
「ShakeDetector.waitForStart」& 「detector.stopListening();」のように任意に終了させるか、「ShakeDetector.autoStart」のように自動を選ぶことになります。

後者は、initState、アプリの開始時に発動させればいいのですが、他のページに移っても発動が続くことがあり、ブルブルと震えてしまうのでチョベリベリ最高でない状態になりました。

なので、前者でinitState、アプリ開始時にスタートさせて、dispose、画面の終了時に止まるようにしています。

@override
void initState() {
  super.initState();
  detector = ShakeDetector.waitForStart(
      onPhoneShake: () {
    //やりたい処理
      }
  );
}

@override
void dispose() {
  // *TODO: implement dispose*
  super.dispose();
  detector.stopListening(); //振ったら動く処理終了
}

パスワードロック機能

passcode_screen | Flutter Package

パスワードロックの画面を作るパッケージ

shared_preferences | Flutter Package

DBなしでデータを保存できるパッケージ

local_auth | Flutter Package

生体認証を実装する機能。

どれもパッケージ自体の使い方は難しくないです。やりたいことは簡単に実現できます。

ただしこれらを組み合わせて、パスワードロックとして機能させるところが大変でした。

IMG_9928.PNG

パスワードロックでは、Flutterのライフサイクル(読み込まれてから、画面のデータが破棄されるまでの過程)などを理解する必要があります。

詳細を書くと長くなってしまいますので、別の記事で書こうと思いますので、もうしばらくお待ちください。

一つ問題があって、パスワードロックの画面が表示される前に、一瞬だけ閉じた時の画面が表示されます。

ほぼ内容を読み取るが難しいほど一瞬ですが、完璧を目指すなら、すでにアプリを立ち上げた時点でパスワードロック画面が表示されている状態がいいですよね。

しかし、Flutterの仕様なのか、画面を閉じた時、立ち上げた時を感知できるても、どちらにタイミングも処理が走るのが、 画面が完全にたち上がってからでした。

なので、ちょっぴり中身が見えてしまいます。これはどうにも解決できる術が見つかりませんでした。

ネイティブでコードを書けば解決はできるそうなのですが、ネイティブは触れないので、今回はほぼ実害がないだろうという判断で、今回はそこまでの作り込みはしていません。

バックアップ・リストア機能

バックアップ、リストアの方法はいろいろあると思いますが、GoogleDriveにDBのデータを保存する、ダウンロードして保存することにしました。

Googleドライブを採用した理由は、Googleアカウントはほぼみんな持っているだろうし、個人のアカウントに紐づいた管理の方が安心だろうという配慮、またこちらもストレージ容量のコストを気にする必要がないからです。
またAPIも無料のようなので、一番コストと機能がバランスがありそうです。

ただし、こちらはパッケージをインストールしたら、簡単に実装できるわけではありません。

FirebaseやGoogleDriveのAPIなどの外部サービスとの連携が必要になってきますので、始めて使う方はそれなりにお勉強する覚悟が必要だと思います。

GoogleDriveに保存する方法を記載した記事があまり見つからないのですが、テスト前にいやいやながらもノートをコピーさせてくれそうな優しい感じの方が書いているこちらの記事を参考にしました。パッケージをたくさんインストールするので、下記を参考にしました。(英語)

GoogleAPI利用の設定:参考

Google Firebase Email/Password And Google Login In Flutter

Googleドライブの実装:参考

https://www.c-sharpcorner.com/article/google-drive-integration-in-flutter-upload-download-list-files/

これも詳細を書くとかなりのボリュームになるので、別の記事で書こうと思います。

こちらのポイントは、概念を理解するまでが時間がかかりました。

IMG_9929.jpg

大雑把に概要を書くと
- ユーザーにアプリからGoogleDriveにアクセスできるようにログインしてもらう。
- スマホに保存されているDBのファイルを、Googleドライブのユーザーが見えない領域に保存。
- 保存はIDで管理されているので、該当のファイルをダウンロードして、スマホのDBファイルにコピーでリストア

最初はなんのこっちゃよく分からないと思うので、一度まるまる、この方が書いている通りに作ってみてください。

どのコードが何をしているのかを把握してからコードを変更しておくと分かりやすいと思います。

あと、所々ハマりポイントがあったので、別の記事で書くのでしばらくお待ちくださいね。

有料課金機能

このアプリを開発する上でも最大難所だった有料課金、サブスクのところ。
これはFlutter公式?のin_app_purchaseを使いました。

in_app_purchase | Flutter Package

Simulator Screen Shot - iPhone 11 Pro - 2020-06-26 at 08.52.19.png

この有料課金機能もパッケージ入れたら簡単に導入!というわけにはいかず、有料課金のサービスにする場合レシート(購入履歴)のチェックなどの機能を実装する必要があります。
ただ、レシートのチェックは、アプリ単体で動作させることはセキュリティの観点から推奨されておらず、外部のサービスと連携する必要があります。(Firebaseなど)

そして、現在進行形で世界中で議論されているハマりポイントもあります。

例えば購入操作のキャンセルやエラー時に、きっちりとトランザクション(一連の購入処理)を消しておかないと、Apple様の方に未処理のトランザクションが溜まってしまい、正常に動作しなかったりします。

私の場合、さらに有料課金の仕組みが理解できていなかったので、意味プーさん状態でかなり苦労しました。

もしプログラミング初心者、初めてアプリを作る方で、課金が必須でないのなら、有料課金は飛ばして作ったほうがいいと思います。

もし必須の場合は、ほぼほぼアプリが完成して、最後の仕上げとして取り組むほうがいいと思います。

くどいようですが、これも詳細な実装方法を書くと、かなりのボリュームになるので、別の記事で書いていきますね。

チュートリアル機能

introduction_screen | Flutter Package

アプリをインストールしてくれたユーザーにきっちり機能を伝えることも大切だと思ったので、チュートリアルの画面を初回起動のみ表示するために、上記のパッケージを使いました。
IMG_9930.PNG

初回起動の判断は、パスワードロックでも使った「shared_preferences」で、bool型で保存しておくことで、2回目以降は表示されなくなります。

これもほぼテンプレートが決まっているので、難しくなく導入できると思います。

バッジ関連

badges
badges | Flutter Package

有料課金で強調したい部分があったので、バッジ表示として上記のパッケージを利用しました。
簡単にできるので、便利ですね。
分かりづらいですが、You can use~~~のところで使ってます。

Simulator Screen Shot - iPhone 11 Pro - 2020-06-26 at 08.52.19.png

まとめ

いかがでしたでしょうか?詳しい説明は別の記事でが多かったのですが、今回は、難しい機能の実装に挑戦してみました。

正直1ヶ月で作ろうと思っていましたが、丸々3ヶ月もかかってしまいましたが、自分ではそれなりに実現させたかった機能を作ることができました。

保留にしている、
- パスワード機能
- バックアップ機能
- 有料課金
については、おいおい書いています。

特にFlutterは日本語の資料が少ないので、頑張りたいと思います。

少してもで、Flutter開発者の仲間の参考になれば幸いです。

ツイッターでも色々呟いているので、良かったらチェックください。

■ツイッターアカウント
YuKiOのツイッター

■YuKiOのブログ(個人開発のこと書いていたりします。)
YuKiOのアウトプット

アプリも良かったら使ってみてください。

■AppStore
https://apps.apple.com/jp/app/id1517535550

■Google Play
https://play.google.com/store/apps/details?id=com.IdeaShuffleMemoApp&hl=ja

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

Flutterのメモアプリ開発で使ったオススメのパッケージを紹介

メモアプリ作成で作ったFltutterパッケージ

iphone11top.jpg

今こそ「ひらめきたい」というあなたに!

6月末にFlutterでアプリ開発を始めてから2作目のアプリ、アイデアを発想するためのメモアプリ「IdeaShuffleMemo(アイデアシャッフルメモ)」をリリースしました。

■AppStore
https://apps.apple.com/jp/app/id1517535550

■Google Play
https://play.google.com/store/apps/details?id=com.IdeaShuffleMemoApp&hl=ja

■アプリの詳細記事
https://yukio.site/idea_shuffle_memo/

1作目は、とりあえずアプリをリリースすることを主眼にしていたため、技術的に実装が難しそうなところは省きました。
2作目はステップアップのために、

  • 画面のパスワードロック
  • DBのバックアップ・リストア
  • 有料課金(サブスクリプション)など

いろいろな機能実装に挑戦的に取り組みました

この3つの機能実装について詳細を書くと、とっても長くなりますので、詳細は別の記事でまた書く予定です 泣汗汗汗

この記事では、メモアプリ作成で使って良かったFlutterパッケージについて紹介していきたいと思います。

データベースへの保存

moor | Dart Package

Flutter開発でデータベース(DB)が使えるようにするパッケージが、Moorです。
IMG_9925.jpg

最初に学習したFlutter教材がmoorを採用していたのでDB操作のパッケージはこれ一択だったのですが、一通りやりたいことはやれるので満足しています。

日本語の情報は少ないですが、英語ではしっかりした資料もあったり、Youtubeもあったりと情報は充実していると思います。(Google翻訳があれば問題ありません・・・白目。)

Getting Started | Moor

ただパッケージを入れたからといって簡単に扱えるわけではなく、プログラミングの完全な初心者だったり、RubyonRailsしかやったことがない人にとってはちょっと難しいも。

Railsのように至れり尽くせりではないので、Rails感覚でやると辛いです。

初期設定やデータの保存・取得など、それなりに資料とにらめっこしながら勉強する必要があるかもしれません。

「DB?余裕っしょ」とナメていると、ヒョードルにタコ殴りされた気分になります。

慣れてくれば、それなりに使いやすいと思います。

派手目のダイアログ

awesome_dialog | Flutter Package

今まで更新完了などの表示はToastを使っていたのですが、いかんせん地味で、表示されているのに気づかなかったり。

awesome_dialogであれば、簡単に大阪のおばちゃん並の主張をする、派手なダイアログを表示することができます。
IMG_9924.jpg

実装は、公式ドキュメントを見れば簡単にできると思います。

OKやCANCELのボタン、ボタンの色などカスタマイズもかなり自由にできます。

ただ・・・ダイアログが自動的に消えてくれる機能がありません。

ダイアログを消すために毎回ボタンをを押すのは、メンディーですよね。

もう、半分が優しさでできていないバファリン並にユーザーにとって好ましい状況でありません。

「作成しました〜」っていう表示をした後に、数秒後に消えてくれるか、ダイアログの外側をタップしたら消えてくれるほうがいいですよね。

Timer t = null;
AwesomeDialog(context: context,
  dialogType: DialogType.SUCCES,
  animType: AnimType.BOTTOMSLIDE,
  tittle: '追加したぜ〜〜〜',
  desc: '',//何か文章入れたければ
  onDissmissCallback: () async {
    Navigator.pop(context); //編集画面を閉じるため
    t.cancel(); //キャンセル処理
    //やりたい処理
  },
).show();
t = Timer(Duration(seconds: 3), () {
  Navigator.pop(context);//編集画面を閉じるため
  //やりたい処理
});

いろいろやり方はあると思いますが、こんな感じにしてみました。
何もしない場合、タイマーで3秒後に消えるようにしています。
ただし、ダイアログの外側をタップ後にタイマーをキャンセルしないと、トラップカードのごとく処理が2重で発動してしまいます。
なので、画面の外側を押した時に、「 t.cancel(); 」でタイマーをキャンセルしています。

アイコン「font_awesome_flutter」

font_awesome_flutter | Flutter Package

IMG_9926.jpg

マテリアルアイコンでもいいのですが、ぴったりのアイコンとか見つからないことがあります。
今回バックアップで、GoogleDriveを使ったのですが、アイコンでいちいち画像を表示させるのがメンディーです。
なので、各サービスのアイコンとか使える「font_awesome_flutter」を使ってみました。
使えるアイコンが増えるので、いいですね。
こんな感じで、簡単に呼び出せます。

icon: FaIcon(FontAwesomeIcons.google),

バイブレーションの通知「vibrate」

vibrate | Flutter Package

Webサービスだとあまり気にならないのですが、アプリの場合、更新とか保存など、ちょっとした処理の時にフィードバックがないと不安になったりしません?

サバンナの八木さんみたいに「スマホさん動いてますか?」って聞きたくなりますよね。

ユーザーに、「処理しているよ〜」とか、「処理終わったよ〜」って分かりやすくする方が親切なので、バイブレーションの機能を実装しました。
こんな感じで簡単に、よきタイミングでコードを差し込めば、スマホを震わせることができます。

Vibrate.*feedback*(FeedbackType.medium);

バイブレーションのタイプも色々あります。

  success,
  error,
  warning,
  selection,
  impact,
  heavy,
  medium,
  light

私は面倒なので、mediumオンリーで使ってます。

スマホを振ると動く機能

shake | Flutter Package

せっかくスマホなので、スマホだからこそできる機能をつけたい。

スマホを振ると、処理を発動できるパッケージが、ブギーな胸騒ぎしそうな「shake」です。

このパッケージを発動する方法が2つあって。
「ShakeDetector.waitForStart」& 「detector.stopListening();」のように任意に終了させるか、「ShakeDetector.autoStart」のように自動を選ぶことになります。

後者は、initState、アプリの開始時に発動させればいいのですが、他のページに移っても発動が続くことがあり、ブルブルと震えてしまうのでチョベリベリ最高でない状態になりました。

なので、前者でinitState、アプリ開始時にスタートさせて、dispose、画面の終了時に止まるようにしています。

@override
void initState() {
  super.initState();
  detector = ShakeDetector.waitForStart(
      onPhoneShake: () {
    //やりたい処理
      }
  );
}

@override
void dispose() {
  // *TODO: implement dispose*
  super.dispose();
  detector.stopListening(); //振ったら動く処理終了
}

パスワードロック機能

passcode_screen | Flutter Package

パスワードロックの画面を作るパッケージ

shared_preferences | Flutter Package

DBなしでデータを保存できるパッケージ

local_auth | Flutter Package

生体認証を実装する機能。

どれもパッケージ自体の使い方は難しくないです。やりたいことは簡単に実現できます。

ただしこれらを組み合わせて、パスワードロックとして機能させるところが大変でした。

IMG_9928.PNG

パスワードロックでは、Flutterのライフサイクル(読み込まれてから、画面のデータが破棄されるまでの過程)などを理解する必要があります。

詳細を書くと長くなってしまいますので、別の記事で書こうと思いますので、もうしばらくお待ちください。

一つ問題があって、パスワードロックの画面が表示される前に、一瞬だけ閉じた時の画面が表示されます。

ほぼ内容を読み取るが難しいほど一瞬ですが、完璧を目指すなら、すでにアプリを立ち上げた時点でパスワードロック画面が表示されている状態がいいですよね。

しかし、Flutterの仕様なのか、画面を閉じた時、立ち上げた時を感知できるても、どちらにタイミングも処理が走るのが、 画面が完全にたち上がってからでした。

なので、ちょっぴり中身が見えてしまいます。これはどうにも解決できる術が見つかりませんでした。

ネイティブでコードを書けば解決はできるそうなのですが、ネイティブは触れないので、今回はほぼ実害がないだろうという判断で、今回はそこまでの作り込みはしていません。

バックアップ・リストア機能

バックアップ、リストアの方法はいろいろあると思いますが、GoogleDriveにDBのデータを保存する、ダウンロードして保存することにしました。

Googleドライブを採用した理由は、Googleアカウントはほぼみんな持っているだろうし、個人のアカウントに紐づいた管理の方が安心だろうという配慮、またこちらもストレージ容量のコストを気にする必要がないからです。
またAPIも無料のようなので、一番コストと機能がバランスがありそうです。

ただし、こちらはパッケージをインストールしたら、簡単に実装できるわけではありません。

FirebaseやGoogleDriveのAPIなどの外部サービスとの連携が必要になってきますので、始めて使う方はそれなりにお勉強する覚悟が必要だと思います。

GoogleDriveに保存する方法を記載した記事があまり見つからないのですが、テスト前にいやいやながらもノートをコピーさせてくれそうな優しい感じの方が書いているこちらの記事を参考にしました。パッケージをたくさんインストールするので、下記を参考にしました。(英語)

GoogleAPI利用の設定:参考

Google Firebase Email/Password And Google Login In Flutter

Googleドライブの実装:参考

https://www.c-sharpcorner.com/article/google-drive-integration-in-flutter-upload-download-list-files/

これも詳細を書くとかなりのボリュームになるので、別の記事で書こうと思います。

こちらのポイントは、概念を理解するまでが時間がかかりました。

IMG_9929.jpg

大雑把に概要を書くと
- ユーザーにアプリからGoogleDriveにアクセスできるようにログインしてもらう。
- スマホに保存されているDBのファイルを、Googleドライブのユーザーが見えない領域に保存。
- 保存はIDで管理されているので、該当のファイルをダウンロードして、スマホのDBファイルにコピーでリストア

最初はなんのこっちゃよく分からないと思うので、一度まるまる、この方が書いている通りに作ってみてください。

どのコードが何をしているのかを把握してからコードを変更しておくと分かりやすいと思います。

あと、所々ハマりポイントがあったので、別の記事で書くのでしばらくお待ちくださいね。

有料課金機能

このアプリを開発する上でも最大難所だった有料課金、サブスクのところ。
これはFlutter公式?のin_app_purchaseを使いました。

in_app_purchase | Flutter Package

Simulator Screen Shot - iPhone 11 Pro - 2020-06-26 at 08.52.19.png

この有料課金機能もパッケージ入れたら簡単に導入!というわけにはいかず、有料課金のサービスにする場合レシート(購入履歴)のチェックなどの機能を実装する必要があります。
ただ、レシートのチェックは、アプリ単体で動作させることはセキュリティの観点から推奨されておらず、外部のサービスと連携する必要があります。(Firebaseなど)

そして、現在進行形で世界中で議論されているハマりポイントもあります。

例えば購入操作のキャンセルやエラー時に、きっちりとトランザクション(一連の購入処理)を消しておかないと、Apple様の方に未処理のトランザクションが溜まってしまい、正常に動作しなかったりします。

私の場合、さらに有料課金の仕組みが理解できていなかったので、意味プーさん状態でかなり苦労しました。

もしプログラミング初心者、初めてアプリを作る方で、課金が必須でないのなら、有料課金は飛ばして作ったほうがいいと思います。

もし必須の場合は、ほぼほぼアプリが完成して、最後の仕上げとして取り組むほうがいいと思います。

くどいようですが、これも詳細な実装方法を書くと、かなりのボリュームになるので、別の記事で書いていきますね。

チュートリアル機能

introduction_screen | Flutter Package

アプリをインストールしてくれたユーザーにきっちり機能を伝えることも大切だと思ったので、チュートリアルの画面を初回起動のみ表示するために、上記のパッケージを使いました。
IMG_9930.PNG

初回起動の判断は、パスワードロックでも使った「shared_preferences」で、bool型で保存しておくことで、2回目以降は表示されなくなります。

これもほぼテンプレートが決まっているので、難しくなく導入できると思います。

バッジ関連

badges
badges | Flutter Package

有料課金で強調したい部分があったので、バッジ表示として上記のパッケージを利用しました。
簡単にできるので、便利ですね。
分かりづらいですが、You can use~~~のところで使ってます。

Simulator Screen Shot - iPhone 11 Pro - 2020-06-26 at 08.52.19.png

まとめ

いかがでしたでしょうか?詳しい説明は別の記事でが多かったのですが、今回は、難しい機能の実装に挑戦してみました。

正直1ヶ月で作ろうと思っていましたが、丸々3ヶ月もかかってしまいましたが、自分ではそれなりに実現させたかった機能を作ることができました。

保留にしている、
- パスワード機能
- バックアップ機能
- 有料課金
については、おいおい書いています。

特にFlutterは日本語の資料が少ないので、頑張りたいと思います。

少してもで、Flutter開発者の仲間の参考になれば幸いです。

ツイッターでも色々呟いているので、良かったらチェックください。

■ツイッターアカウント
YuKiOのツイッター

■YuKiOのブログ(個人開発のこと書いていたりします。)
YuKiOのアウトプット

アプリも良かったら使ってみてください。

■AppStore
https://apps.apple.com/jp/app/id1517535550

■Google Play
https://play.google.com/store/apps/details?id=com.IdeaShuffleMemoApp&hl=ja

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

textFieldShouldReturn

リターンキーを押した時にキーボードを閉じる

swift
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()

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

NavigationController下においての画面遷移

1つ前の画面に戻る

swift
    @IBAction func calendarButton(_ sender: Any) {
        self.navigationController?.popViewController(animated: true)

    }

階層のトップ画面に戻る

swift
    @IBAction func walkButton(_ sender: Any) {
        self.navigationController?.popToRootViewController(animated: true)
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

メルカリ・フリルなどのフリマアプリホーム画面の3カラムデザインの作り方

はじめに

メルカリ/フリルなどのフリマアプリでよく目にするホーム画面の3カラムのデザインを作成する方法を紹介します。
今回はUICollectionViewControllerをSwiftUI上で動かしています。(これでUIKitにホットリロードのCanvasを導入する事が出来ます)

*ストーリーボードは使用していないので、Extension.swiftファイルに制約のルールを設定しています。(ステップ3をご覧ください)

スクリーンショット 0002-07-01 午後5.34.15.png

開発環境

Swift 5.2.4
Xcode 11.5(Deployment Target 13.0)
ストーリーボードなし

ステップ1

SceneDelegate.swift内に初期ページ設定をします。初期ページはFrimaContentViewとします

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

        // Create the SwiftUI view that provides the window contents.
        let frimaContentView = FrimaContentView()
        // Use a UIHostingController as window root view controller.
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: frimaContentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }

ステップ2: SwiftUIからUICollectionViewControllerを表示する

以下を実装するとUIKitでもホットリロード機能(canvas)が使えるため一々Runしなくてもいいので便利です。

import UIKit
import SwiftUI

class FrimaController: UICollectionViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        //背景を白にする
        collectionView.backgroundColor = .white

    }
}

struct FrimaIntegratedViewController: UIViewControllerRepresentable {

    func makeUIViewController(context: UIViewControllerRepresentableContext<FrimaIntegratedViewController>) -> FrimaController {
        return FrimaController(collectionViewLayout: UICollectionViewFlowLayout())
    }

    func updateUIViewController(_ uiViewController: FrimaController, context: Context) {

    }
}

struct FrimaContentView: View {
    var body: some View {
        FrimaIntegratedViewController().edgesIgnoringSafeArea(.all)
    }
}

struct FrimaContentView_Previews: PreviewProvider {
    static var previews: some View {
        FrimaContentView()
    }
}

ステップ3: 個々のアイテムセルを作成

セル上にアイテムの名前と値段、いいねボタンを作成します。

class FrimaItemCell: UICollectionViewCell {

    let priceLabel: UILabel = {
        let label = UILabel()
        label.text = "¥1,000"
        label.textColor = .white
        return label
    }()

    let itemNameLabel: UILabel = {
        let label = UILabel()
        label.text = "Tシャツ"
        label.textColor = .white
        return label
    }()

    let likeButton: UIButton = {
        let button = UIButton(type: .system)
        //Xcodeにデフォルトである画像を使用 *これはDeplotment Targetが13.0以降で使えます
        button.setImage(UIImage(systemName: "heart")?.withRenderingMode(.alwaysOriginal), for: .normal)
        return button
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)

        backgroundColor = .systemRed

        setupView()
    }

    fileprivate func setupView() {
        addSubview(priceLabel)
        priceLabel.anchor(top: nil, left: leftAnchor, bottom: bottomAnchor, right: nil, paddingTop: 0, paddingLeft: 5, paddingBottom: 5, paddingRight: 0, height: 0, width: 0)

        addSubview(itemNameLabel)
        itemNameLabel.anchor(top: nil, left: priceLabel.leftAnchor, bottom: priceLabel.topAnchor, right: nil, paddingTop: 0, paddingLeft: 0, paddingBottom: 5, paddingRight: 0, height: 0, width: 0)

        addSubview(likeButton)
        likeButton.anchor(top: nil, left: nil, bottom: bottomAnchor, right: rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 5, paddingRight: 5, height: 0, width: 0)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

制約ルールは別ファイルExtensions.swiftで設定しています。

//Extensions.swift
import Foundation
import UIKit

extension UIView {
func anchor(top: NSLayoutYAxisAnchor?, left: NSLayoutXAxisAnchor?,  bottom: NSLayoutYAxisAnchor?, right: NSLayoutXAxisAnchor?, paddingTop: CGFloat, paddingLeft: CGFloat, paddingBottom: CGFloat, paddingRight: CGFloat, height: CGFloat, width: CGFloat){

    translatesAutoresizingMaskIntoConstraints = false

    if let top = top {
        self.topAnchor.constraint(equalTo: top, constant: paddingTop).isActive = true
    }

    if let left = left {
        self.leftAnchor.constraint(equalTo: left, constant: paddingLeft).isActive = true
    }

    if let bottom = bottom {
        bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom).isActive = true
    }

    if let right = right {
        rightAnchor.constraint(equalTo: right, constant: -paddingRight).isActive = true
    }

    if width != 0 {
        widthAnchor.constraint(equalToConstant: width).isActive = true
    }

    if height != 0 {
        heightAnchor.constraint(equalToConstant: height).isActive = true
    }
    }
}


ステップ4: レイアウトを作成

ステップ3で作成したCellをFrimaController上に登録し、レイアウトを作成します。

class FrimaController: UICollectionViewController, UICollectionViewDelegateFlowLayout {

    let frimaItemCellId = "frimaItemCellId"

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView.backgroundColor = .white

        //ステップ3で作成したFrimaCellを登録
        collectionView.register(FrimaItemCell.self, forCellWithReuseIdentifier: frimaItemCellId)

    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 20
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: frimaItemCellId, for: indexPath) as! FrimaItemCell
        return cell
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: view.frame.width / 3 - 4, height: view.frame.width / 3)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 4)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return 4
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return 0
    }

}

完成

これで完成。
スクリーンショット 0002-07-01 午後5.34.15.png

最後に

今は全てのアイテムが"Tシャツ"、と¥1,000そして画像がありませんが、
次回はアイテムクラスを作り個々のアイテムを設定していきます。

最後に一句(初心者時代にやっていたNG行動)
テラテイル 聞きすぎ怒られ 気がメイル

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

WidgetKitノート

WWDC20を見て、WidgetKitにキャッチアップしたので、基礎知識をまとめます。
これからキャッチアップしたいよ、という方に向けたノートです。

構成要素

  • kind: ウィジェットは1アプリに対していくつかつくれる(たとえばStockアプリのリスト表示、チャート表示など)
  • configuration: ウィジェットの基本設定
    • StaticConfiguration: ユーザーがカスタマイズ不可の静的デザイン
    • IntentConfiguration: ユーザーがカスタマイズ可能な動的デザイン
  • supportedFamilies: systemSmall/systemMedium/systemLarge
  • placeholder: Default Content but no user data

Snapshot

  • Widget Galleyに並ぶスクリーンショット的なView(正確にはスクリーンショットではない)

Timeline

  • ウィジェットの更新タイミング
  • ウィジェットの更新タイミングも三種類
  • Timeline.ReloadPolicy: after(Date), atEnd, never
  • atEndはデフォルトでfetchされたら(タイミングは自分で指定)、afterは指定した時間以降、neverは更新しない
  • WidgetCenterのメソッドを使うことで開発者の任意のタイミングでreloadが可能(バックグラウンド)
  • サーバー通信が必要なときはURLSessionも使える

機能ガイドライン

  • ウィジェットはミニアプリではない
  • スクロール不可
  • 動画不可
  • タップしたら表示している情報に関連するアプリ内の詳細Viewに飛ぶ
    • そのためにWidgetURLとLinkという新APIが追加されている
  • ウィジェットはホーム画面でライブ更新するものではないので、毎秒reloadするような頻繁な更新はやめて、適切なreloadタイミングを設定すること

参考

Meet WidgetKit
Widgets Code-along, part 1: The adventure begins

「Meet WidgetKit」を見た後で、「Widgets Code-along」に移る順番がオススメです。
(僕は逆に見てしまったせいで)前提知識の説明なくて困惑しました)

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

iOS 14 の Widgets 全サイズを一度に Preview する【コピペで使える】

本記事は Qrunch とのクロス投稿です。
iOS 14 の Widgets 全サイズを一度に Preview する【コピペで使える】 | Qrunch(クランチ)


iOS & iPadOS 14 および macOS Big Sur から WidgetKit が導入され、Widget を作るためには SwiftUI で書くこととなっています。ここでは iOS 14 での Widget のお話をします。

Widgets の iOS Human Interface Guidelines を見ると "Adapting to Different Screen Sizes" セクションに1つの表があります。そこには各 iPhone のスクリーンサイズと、それぞれに対応する "Small"、"Medium"、"Large" Widget のサイズが記載されています。

せっかく SwiftUI で書くので、Xcode Previews で全てのサイズを表示させながらレイアウトするのはいかがでしょうか。このように一度に表示されると何かと便利です。

スクリーンショット 2020-07-02 3.19.00.png
※スクリーンショットは Xcode 11.5 のものです。

以下、3サイズの Widget のサイズを記載済みの PreviewProvider です。コピペしてお使いください。

Small Widget

struct SmallWidgetContentView_Previews: PreviewProvider {
    static var contentView: some View {
        ContentView()
            .preferredColorScheme(.dark)
    }

    static var previews: some View {
        Group {
            // 320pt × 568pt (iPhone SE 第1世代)
            contentView
                .previewLayout(.fixed(width: 141.0, height: 141.0))

            // 375pt × 667pt (iPhone 6/6s/7/7s/8/SE 第2世代)
            contentView
                .previewLayout(.fixed(width: 148.0, height: 148.0))

            // 414pt × 736pt (iPhone 6 Plus/6s Plus/7 Plus/8 Plus)
            contentView
                .previewLayout(.fixed(width: 159.0, height: 159.0))

            // 375pt × 812pt (iPhone X/XS/11 Pro)
            contentView
                .previewLayout(.fixed(width: 155.0, height: 155.0))

            // 414pt × 896pt (iPhone XR/XS Max/11/11 Pro Max)
            contentView
                .previewLayout(.fixed(width: 169.0, height: 169.0))
        }
    }
}

Medium Widget

struct MediumWidgetContentView_Previews: PreviewProvider {
    static var contentView: some View {
        ContentView()
            .preferredColorScheme(.dark)
    }

    static var previews: some View {
        Group {
            // 320pt × 568pt (iPhone SE 第1世代)
            contentView
                .previewLayout(.fixed(width: 291.0, height: 141.0))

            // 375pt × 667pt (iPhone 6/6s/7/7s/8/SE 第2世代)
            contentView
                .previewLayout(.fixed(width: 322.0, height: 148.0))

            // 414pt × 736pt (iPhone 6 Plus/6s Plus/7 Plus/8 Plus)
            contentView
                .previewLayout(.fixed(width: 348.0, height: 159.0))

            // 375pt × 812pt (iPhone X/XS/11 Pro)
            contentView
                .previewLayout(.fixed(width: 329.0, height: 155.0))

            // 414pt × 896pt (iPhone XR/XS Max/11/11 Pro Max)
            contentView
                .previewLayout(.fixed(width: 360.0, height: 169.0))
        }
    }
}

Large Widget

struct LargeWidgetContentView_Previews: PreviewProvider {
    static var contentView: some View {
        ContentView()
            .preferredColorScheme(.dark)
    }

    static var previews: some View {
        Group {
            // 320pt × 568pt (iPhone SE 第1世代)
            contentView
                .previewLayout(.fixed(width: 291.0, height: 299.0))

            // 375pt × 667pt (iPhone 6/6s/7/7s/8/SE 第2世代)
            contentView
                .previewLayout(.fixed(width: 322.0, height: 324.0))

            // 414pt × 736pt (iPhone 6 Plus/6s Plus/7 Plus/8 Plus)
            contentView
                .previewLayout(.fixed(width: 348.0, height: 357.0))

            // 375pt × 812pt (iPhone X/XS/11 Pro)
            contentView
                .previewLayout(.fixed(width: 329.0, height: 345.0))

            // 414pt × 896pt (iPhone XR/XS Max/11/11 Pro Max)
            contentView
                .previewLayout(.fixed(width: 360.0, height: 376.0))
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Apple APNsでのPUSH通知のテストスクリプト

はじめに

Firebase Cloud Messaging でPUSH通知の実装していましたが、なぜか、iosでPUSH通知されなくなっていました。何が原因なのかを切り分けるために、AppleのPUSH通知サービス(APNs)でPUSHが送信されるか試すことにしました。

APNsでの認証ですが、以前使われていた「APNs 証明書」と最近使われている「APNs 認証キー」の2つがあります。「APNs 認証キー」を使ったPUSH通知のスクリプトを記載します。このスクリプトは、macで動かしています。おそらく、curlとopensslがある環境であれば動くと思います。

パラメータは、Firebaseから取得するように書いていますが、APNsのパラメータを使えばPUSH通知できるので、Firebaseを使っていない場合もこのスクリプトでテストできます。

準備

以下のシェルスクリプトで記載するキーIDチームIDアプリのバンドルIDは、Firebase Consoleの以下で確認できます。設定クラウドメッセージングからiosアプリ設定の項目を確認します。図の赤で囲っている部分に、上記の3つのIDが記載されています。
APNs認証キーのファイルは、Firebaseのここの設定をした時に保存していると思いますので、そのファイルを使います。

cachepolicy

cachepolicy

PUSH通知のシェルスクリプト

以下のシェルスクリプトの設定項目以下の5行のパラメータを自分のアプリのパラメータに設定して、シェルスクリプトを動かすとiPhoneへのPUSH通知ができます。
デバイストークンは、PUSH通知を送信したいiPhoneのデバイストークンを指定してください。

#!/bin/sh

# ------ 設定項目
deviceToken=1234567890abcdefghij  # デバイストークン
authKey="./AuthKey_AZ8N4W9YYC.p8" # APNs 認証キーのファイルを指定
authKeyId=AB8C4D9EFG              # キー ID
teamId=ABCD12EFGH                 # チーム ID
bundleId=com.example.appbundleId  # アプリのバンドルID
# ------ 設定項目終了

endpoint=https://api.development.Push.Apple.com

read -r -d '' payload <<-'EOF'
{
   "aps": {
      "alert": {
         "title": "notification title",
         "subtitle": "notification subtitle",
         "body": "notification body "
      }
   }
}
EOF

base64() {
   openssl base64 -e -A | tr -- '+/' '-_' | tr -d =
}

sign() {
   printf "$1"| openssl dgst -binary -sha256 -sign "$authKey" | base64
}

time=$(date +%s)
header=$(printf '{ "alg": "ES256", "kid": "%s" }' "$authKeyId" | base64)
claims=$(printf '{ "iss": "%s", "iat": %d }' "$teamId" "$time" | base64)
jwt="$header.$claims.$(sign $header.$claims)"

curl --verbose \
   --header "content-type: application/json" \
   --header "authorization: bearer $jwt" \
   --header "apns-topic: $bundleId" \
   --data "$payload" \
   $endpoint/3/device/$deviceToken
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む