- 投稿日:2020-03-19T22:56:48+09:00
Flutterを始めてListViewが難しい
きっかけ
長期休みに入り興味があったFlutterを始めた。
いろいろやってみて、まずListViewにつまづいた。つまづいたポイントとして、
- クラスの使い方
- 要素のUIの構成
- どのように要素にデータを渡すか
がある。
開発はXamarinを軽く触ったことがある程度で、初心者。
書籍は基礎から学ぶFlutterを購入し、公式サイトも見ながらスタートしたFlutter超初心者。また、Flutterを開発するにあたって日本語の記事が異常に少ないので書こうと思った。これからも何かつまづくことがあればちょくちょく書く予定。ListView
ListViewとは要素をスクロールできる画面に表示していくウィジェット。
FlutterにはYoutubeに公式チャンネルがあり、多くのウィジェットの紹介動画があり、短く、見やすい動画となっているのでおすすめ。もちろんListViewの動画もある。
ListView (Flutter Widget of the Week)ListView.separated
ちょっと調べてみて、
ListView.separated
コンストラクタが使いやすそうだったので使ってみた。
他にもListView
、ListView.builder
、ListView.custom
があるがまだよくわからない(そのうち追記するかも)。
ListView.separated
コンストラクタの使用例は、最後に書いたので参考にして欲しい。公式サイトによると、このコンストラクタは以下のようになっているらしい。
https://api.flutter.dev/flutter/widgets/ListView/ListView.separated.html
ListView.separated({
Key key,
Axis scrollDirection: Axis.vertical,
bool reverse: false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap: false,
EdgeInsetsGeometry padding,
@required IndexedWidgetBuilder itemBuilder,
@required IndexedWidgetBuilder separatorBuilder,
@required int itemCount,
bool addAutomaticKeepAlives: true,
bool addRepaintBoundaries: true,
bool addSemanticIndexes: true,
double cacheExtent
})
@required
がついているものは必須の引数で、これらを解説する。itemBuilder
この引数には、
ListView
のアイテムを作るウィジェットを入れる。ここのウィジェットで作ったものがListViewの1アイテムとしてitemCount
の数分作られる。
また、関数はsample.dartWidget functionName(BuildContext context, int index){}のように書く。
separatorBuilder
itemBuilder
と同様の形式の関数を書く。公式サイトより引用し、sample.dart(BuildContext context, int index) => Divider()と書いたが、よくわかっていないため後に追記する。
(知っている方いたら教えてください!)itemCount
要素数。
int
型の整数値のみを入れる。List<T> list
を使う場合、list.length
を使うといいっぽい。使用例
書いてみたプログラムと実行結果を示す。
プログラム
sample.dartimport 'package:flutter/material.dart'; import 'dart:math'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: MainPage(), ); } } class Person { int age; final String name; Person({this.age, this.name}); } class MainPage extends StatelessWidget { List<Person> _data = [ Person(age: 20, name: "Satou"), Person(age: 18, name: "Suzuki"), Person(age: 30, name: "Tanaka") ]; Widget MyListView() { return ListView.separated( itemBuilder: (BuildContext context, int index) { return Container( padding: EdgeInsets.all(10), child: Row( children: <Widget>[ Container( alignment: Alignment.topLeft, child: Text( _data[index].name, style: TextStyle(fontSize: 25) ), ), Spacer(), Container( alignment: Alignment.bottomRight, child: Text( _data[index].age.toString(), style: TextStyle(fontSize: 20) ), ) ], ) ); }, separatorBuilder: (BuildContext context, int index) => Divider(), itemCount: _data.length); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar(title: Text("List Page")), body: Container(color: Colors.white, child: MyListView()))); } }実行結果
- 投稿日:2020-03-19T20:35:15+09:00
【Sign In with Apple】iOSアプリから投げられたidentityTokenをPHPで認証する
概要
identityToken
というJWTがアプリから届いてパースしてsub
をユニークキーとして認証するのですが
改ざん防止の為、正しくデコードする必要があります。細かいことはバージョンが違ったり環境で変わってくると思うので大まかな流れだけ書いておきます。
※コメントで指摘をもらったのですが、これだけではAppleが発行したことしか担保出来ないため
別のアプリからの認証もしてしまうので足りていません。後日ドキュメントを追加するなりします。
- appleの公開鍵を取得
- 取得した公開鍵を利用して検証
- 問題なければ取得できた値から
sub
を利用して認証以上!!
要所要所の僕が使った部分を以下に記載してるので参考になれば幸いです。
JWTとは
JWT(ジョット)とは JSON Web Token の略で、電子署名付きの URL-safe(URLとして利用出来る文字だけ構成される)な JSONのことです。
電子署名により、JSON の改ざんをチェックできるようになっています。ざっくり言うと、改ざんできない JSON ということになります。
パースしてみる
こちらのサイトでパース出来るので便利でした。
https://jwt.io/Appleの鍵を取得する
Appleの鍵はこちらにありました。
Fetch Apple's public key for verifying token signatureAppleの鍵をFirebaseのJWTライブラリが理解出来る形に変更する
JWTの検証にFirebaseのライブラリを使ったのですが、複数の鍵で署名されてる場合
配列で鍵を指定出来るのですが形式が以下のような形なので変換して上げる必要があります。Appleの鍵の中にある
e
とn
を使ってPEMを作れるので頑張って作ります。
この方の記事をめちゃくちゃ参考にさせてもらいました。
【PHP】AzureAPIなどで配布されている公開鍵の「e」「n」とはなにで、どう使うのか$publicKeys = [ 'kidの値' => 'PEM形式のテキスト', 'kidの値' => 'PEM形式のテキスト', ];JWTの検証をして中身を取得する
Firebaseのライブラリを使って
アプリから投げられたidentityToken
とさっき作った公開鍵の配列とAppleの鍵に書いてあったalg
の値RS256
を指定すれば
改ざんされてなければJSONが返ってきます。$payload = JWT::decode($identityToken, $publicKeys, ['RS256']);
- 投稿日:2020-03-19T17:01:38+09:00
UnityでiOS向けにビルドしたアプリにおいて、音声をバックグラウンド再生する備忘録(20200319)
目的
UnityでiOS向けにビルドしたアプリにおいて
アプリがバックグラウンドになっても音声を再生しつづけたいステップ
- Unityスクリプト内 OnApplicationPause でバックグラウンド時に音声を再生する関数を実行
- iOS用にunityからビルドし、 書き出されたxcode pjファイルを開く
Signing & capabilitiesでbackground modesを追加。Audio,Airplay~にチェックを入れる
UnityAppController.mmのstartUnity関数内ににaudioSessionを定義している箇所があるので、下記記述を追加する
[audioSession setCategory:AVAudioSessionCategoryPlayback error:nil]; [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];この記事を書いた理由
下記記事の内容を参考にしたが、
スレッドが長い上に古い情報が混ざっていて解決に時間がかかったため。
https://forum.unity.com/threads/how-do-i-get-the-audio-running-in-background-ios.319602/備考 swiftで書くとき
extension AppDelegate { //background play(audio) func prepareBackgrounPlay(){ let audioSession = AVAudioSession.sharedInstance() //background audio play do { try audioSession.setCategory(AVAudioSession.Category.playback, mode: AVAudioSession.Mode.default, options: [.mixWithOthers, .allowAirPlay]) print("Playback OK") try audioSession.setActive(true) print("Session is Active") } catch { print(error) } } }
- 投稿日:2020-03-19T15:08:46+09:00
1人でアプリを作る
はじめに
こんにちは。
今回から、『Unity』を使ってスクロールゲームを作っていきます。
企画から完成までの過程を日記感覚で記録していきます!制作環境
macOS Mojave バージョン 10.14.6
Unity 2019.2.19f1企画
「近未来の世界で、主人公は機械で...」という、妄想を膨らませながら、
イメージを紙にサラサラと書いていきます。
ステージ構成や、プレイしてもらう時間、ターゲットなどをじっくり考えました。実際に書いたものが下記画像になります。(きたない...)
まあ、見づらくても、自分が分かっていればいいと思います。
ただ、他の人に見せるなら綺麗にまとめた方がいいかもしれません...。
今回は、一人で作るので、このまま進めます。イメージイラスト
世界観や、設定が決まったので、イメージイラストを書きます。
手が動くままに書きました。
イメージイラストを書いてから、世界観や設定を決めていってもいいかもしれません。
まとめ
今回は、企画をかんがえました。
内容を、事細かに説明していないので短くなっちゃいました...。次回は、ゲームの流れがわかる、簡易的なモックアップを作ります。
- 投稿日:2020-03-19T15:08:46+09:00
1人でアプリを作る【その1】
はじめに
こんにちは。
今回から、『Unity』を使ってスクロールゲームを作っていきます。
企画から完成までの過程を日記感覚で記録していきます!制作環境
macOS Mojave バージョン 10.14.6
Unity 2019.2.19f1企画
「近未来の世界で、主人公は機械で...」という、妄想を膨らませながら、
イメージを紙にサラサラと書いていきます。
ステージ構成や、プレイしてもらう時間、ターゲットなどをじっくり考えました。実際に書いたものが下記画像になります。(きたない...)
まあ、見づらくても、自分が分かっていればいいと思います。
ただ、他の人に見せるなら綺麗にまとめた方がいいかもしれません...。
今回は、一人で作るので、このまま進めます。イメージイラスト
世界観や、設定が決まったので、イメージイラストを書きます。
手が動くままに書きました。
イメージイラストを書いてから、世界観や設定を決めていってもいいかもしれません。
まとめ
今回は、企画をかんがえました。
内容を、事細かに説明していないので短くなっちゃいました...。次回は、ゲームの流れがわかる、簡易的なモックアップを作ります。
- 投稿日:2020-03-19T14:49:55+09:00
iCloud Documentsを使ってファイルの読み書きをする
まえがき
iCloudに置いたファイルをユーザー/アプリ間で共有し、どちらからでも自由にアクセスできる機能を実装します。(Keynoteとかがそうですね)
iCloud Documents Storageを使うと上手くいきそうな事はすぐ分かったのですが、実際に動くところまで持っていくのに結構苦労しました。そこで得られた知見を共有したいと思います。
公式ドキュメントはこちらです。
開発環境
macOS 10.15.3
Xcode 11.3.1
iPad OS 13.3.1Apple Developer Programに登録したアカウントが必要です。
下準備
実装を始める前に、環境構築します。
プロジェクトの作成
Xcodeを立ち上げて、ツールバーからFile → New → Project... を選んでください。
Single View Appを選択し、 好きなProduct Nameを入力します。Capabilityの追加
iCloudの機能を使うには、Capabilityを追加する必要があります。
赤丸で囲ったボタンを押して、iCloudを追加してください。
その後、
iCloud Documents
にチェックを入れます。コンテナの追加
iCloudのCapabilityを追加したからと言って、iCloud内の全てにアクセスはできません。コンテナ(フォルダみたいなもの)を指定し、そのコンテナ内のみアクセスできます。
+ボタンを押して新しくコンテナを作成します。コンテナ名は好きなものを入れてください。こだわりが無ければProduct Nameと同じでいいと思います。
公式ドキュメントにある通り、コンテナは一度作ると削除することができません。typoしてないかよく確認してください。テスト用に適当に作ったものももちろん削除できないので注意してください。
コンテナ作成後、文字が赤くなっている場合は更新ボタンを押してください。
Info.plistの編集
Info.plistを右クリックし、 Open As → Source Codeを選びます。そして、以下のコードを追加してください。
<key>NSUbiquitousContainers</key> <dict> <key>iCloud.kakeru.iCloudDocumentTest</key> ← ここにコンテナ名を入れます <dict> <key>NSUbiquitousContainerIsDocumentScopePublic</key> <true/> <key>NSUbiquitousContainerName</key> <string>iCloudDocumentTest</string> <key>NSUbiquitousContainerSupportedFolderLevels</key> <string>Any</string> </dict> </dict>Property List形式だと以下のような形になります。
- NSUbiquitousContainerIsDocumentScopePublic
- trueにするとコンテナのDocumentsフォルダ内が、iCloud Drive上にフォルダとして見えるようになります。
- NSUbiquitousContainerName
- iCloud Drive上で表示するフォルダ名です。
- NSUbiquitousContainerSupportedFolderLevels
- None: Documentsフォルダ内には、フォルダを作ることができません。
- One: Documentsフォルダ直下であれば、フォルダを作れます。
- Any: 制限なし
各キーの詳しい情報はこちらの公式ドキュメントを確認してください。
実装
test.txt
というファイルの読み書きを実装します。コードが長くなるため、エラーハンドリングは省略しています。各自で追加してください。まず、UIDocumentを継承したクラスを作成します。このクラスは適当にググって出てきたものをベースに作ったため、もっとスマートな書き方があるかもしれません。
class Document: UIDocument { var text: String? = "" override func contents(forType typeName: String) throws -> Any { text?.data(using: .utf8) ?? Data() } override func load(fromContents contents: Any, ofType typeName: String?) throws { guard let contents = contents as? Data else { return } text = String(data: contents, encoding: .utf8) } }ファイルの新規作成
以下のコードだけで、
test.txt
ファイルがiCloud上に作成されます。かんたんですね。let url = FileManager.default.url(forUbiquityContainerIdentifier: nil)! .appendingPathComponent("Documents") .appendingPathComponent("test.txt") let document = Document(fileURL: url) document.save(to: url, for: .forCreating)ファイルへの書き込み
新規作成とあまり変わりません。
let url = FileManager.default.url(forUbiquityContainerIdentifier: nil)! .appendingPathComponent("Documents") .appendingPathComponent("test.txt") let document = Document(fileURL: url) document.text = document.text! + "追記" document.save(to: url, for: .forOverwriting)ファイルの読み込み
以下を実行すると、
test.txt
ファイルの検索が始まります。ファイルが見つかれば中身を出力します。これも適当にググって出てきたものがベースなので、もっとスマートな書き方があるかもしれません。(特にNSMetadataQuery)let metadata = NSMetadataQuery() metadata.predicate = NSPredicate(format: "%K like 'test.txt'", NSMetadataItemFSNameKey) metadata.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: metadata, queue: nil) { notification in let query = notification.object as! NSMetadataQuery if query.resultCount == 0 { return } let url = (query.results[0] as AnyObject).value(forAttribute: NSMetadataItemURLKey) as! URL let document = Document(fileURL: url) document.open { success in if success { print(document.text) } } } metadata.start()完全に動くコード
みなさんが欲しいのはこれですよね。
ContentView.swiftimport SwiftUI struct ContentView: View { @State private var text: String = "" private let containerManager = ContainerManager() var body: some View { VStack() { TextField("テキストを入力...", text: $text) .textFieldStyle(RoundedBorderTextFieldStyle()) .frame(width: 200) Button("Save") { self.containerManager.save(self.text) } Button("Load") { self.containerManager.load { self.text = $0 ?? "" } } Button("Clear") { self.text = "" } Spacer() } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } class ContainerManager { private var metadata: NSMetadataQuery! // 参照を保持するため、メンバとして持っておく。load()内のローカル変数にするとうまく動かない。 private var url: URL { FileManager.default.url(forUbiquityContainerIdentifier: nil)! .appendingPathComponent("Documents") .appendingPathComponent("test.txt") } func load(completion: @escaping (String?) -> Void) { metadata = NSMetadataQuery() metadata.predicate = NSPredicate(format: "%K like 'test.txt'", NSMetadataItemFSNameKey) metadata.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: metadata, queue: nil) { notification in let query = notification.object as! NSMetadataQuery if query.resultCount == 0 { print("ファイルが見つからなかったので新規作成") let document = Document(fileURL: self.url) document.save(to: self.url, for: .forCreating) { success in print(success ? "作成成功" : "作成失敗") completion(nil) } return } let url = (query.results[0] as AnyObject).value(forAttribute: NSMetadataItemURLKey) as! URL let document = Document(fileURL: url) document.open { success in if success { print("ファイル読み込み: \(document.text ?? "nil")") completion(document.text) } else { print("ファイル読み込み失敗") completion(nil) } } } metadata.start() } func save(_ text: String) { let document = Document(fileURL: url) document.text = text document.save(to: url, for: .forOverwriting) { success in print("ファイル保存\(success ? "成功" : "失敗")") } } } class Document: UIDocument { var text: String? = "" override func contents(forType typeName: String) throws -> Any { text?.data(using: .utf8) ?? Data() } override func load(fromContents contents: Any, ofType typeName: String?) throws { guard let contents = contents as? Data else { return } text = String(data: contents, encoding: .utf8) } }ハマったところ
Info.plistを編集しても、うまく反映されない事が多々あります。例えば、
NSUbiquitousContainerIsDocumentScopePublic
の値をtrue → falseに変えても、iCloud Drive上でフォルダが見えたままとか。
公式のQ&Aにもありますが、仕様のようです。Info.plistを編集を編集した場合は、以下おまじないをすると反映されるかと思います。
- 投稿日:2020-03-19T13:18:44+09:00
iOS版chromeでCSSのtransitionが動作しない
CSSで何気にtransitionを使ってたら詰んでしまったのでメモ。
1.現象
・iOS版ChromeでCSS transition(opacityとtransform)が機能せず、がたついてしまう
→アプリを再起動するとうまくいくが、しばらくするとなぜかうまくいかなくなる。・自分のサイトだけおかしいのかと思い、別サイト(例えば https://kenwheeler.github.io/slick/ とか)を開いてみたが、同様にがたつくので、やばいのでは??と思い始める
・GPU起動させる、will-changeをつける、ベンダープレフィックスを頑張る、などなど色々やったが何も解決に繋がらない。
・PC版Chromeやその他のブラウザでは再現されない。
2.背景
おそらくはバグ
「いったんタブを切り替えて、そこから元のページに戻ると、transitionのさいのfpsが落ちる」という現象が確認されている模様。
参考にした開発側のスレッド → https://bugs.chromium.org/p/chromium/issues/detail?id=899130今後のアップデートで治るのを期待します。
3.対策
iOS版ChromeだけJavaScriptの処理で弾くことにしました。
たとえばこれを参照 → https://qiita.com/tatunori4210/items/11a05bf54984d36732ae
- 投稿日:2020-03-19T12:59:33+09:00
Azure PipelinesのYAMLでiOSアプリのTestFlight配布パイプラインを構築する方法
「Azure PipelinesのYAMLでiOSアプリのCI/CD環境を構築する」は3部構成です。
記事を順番に読み進めると、Azure PipelinesでiOSアプリのCI/CD環境が構築できるようになります。
- 第一部:CI環境の構築
- 第二部:App Center配布パイプラインの構築
- 第三部:TestFlight配布パイプラインの構築 ←イマココ
はじめに
Azure Pipelinesを使い、iOSアプリをTestFlightへ配布するCDを構築します。
本記事で書かないこと
- Azure Pipelinesの概要や基本的な操作方法
私が以前書いた記事が参考になると思います- CI環境の構築とAppCenter配布パイプラインの構築で説明した内容
以前説明した内容を説明するのは冗長なのでしません証明書とProvisioning Profileのアップロード
証明書とProvisioning ProfileをAzure Pipelines内の安全な場所へアップロードします。
AppCenter配布パイプラインの構築で紹介したので省略します。
App Store Connectのユーザー名とパスワードの追加
App Store Connectのユーザー名とパスワードをAzure Pipelinesの安全な場所で保持します。
AppCenter配布パイプラインの構築を参考に、
AppStoreConnectUserName
、AppStoreConnectPassword
のようにわかりやすい名前で変数を追加してください。
パスワードにロックをかけるのを忘れないようにしましょう。Apple IDに2ファクタ認証を設定している場合、パスワードはApp用パスワードを使う必要があります 。
App用パスワードの設定方法はこちらの記事をご参照ください。Apple IDのパスワードを使った場合、認証が通らず以下のエラーになります。
$ xcrun altool --upload-app -f ./Foo.ipa -t ios -u {username} -p {password} … "Error Domain=ITunesSoftwareServiceErrorDomain Code=-22014 \"We are unable to create an authentication session.\" UserInfo={NSLocalizedDescription=We are unable to create an authentication sessionMakefileの作成
IPAファイルのバリデーションやTestFlight配布などの実行コマンドをまとめたMakefileを作成します。
CI環境の構築とAppCenter配布パイプラインの構築で紹介したコマンドは省略します。
Makefile
APPSTORE_EXPORT_OPTIONS_PATH := ./ExportOptions/ExportOptionsAppStore.plist
としているので、このパスに.plistファイルを配置してください。
Xcode上からアーカイブ→エクスポートすると.plistが生成されるので、それをそのまま使うのがオススメです。MakefileIPA_PATH := ${EXPORT_PATH}/${PRODUCT_NAME}.ipa APPSTORE_PROVISIONING_PROFILE_SPECIFIER := Foo_AppStore APPSTORE_EXPORT_OPTIONS_PATH := ./ExportOptions/ExportOptionsAppStore.plist .PHONY: generate-ipa-appstore generate-ipa-appstore: # Generate IPA file for App Store $(MAKE) archive-appstore $(MAKE) export-archive-appstore .PHONY: archive-appstore archive-appstore: $(MAKE) archive PROVISIONING_PROFILE_SPECIFIER=${APPSTORE_PROVISIONING_PROFILE_SPECIFIER} .PHONY: export-archive-appstore export-archive-appstore: $(MAKE) export-archive EXPORT_OPTIONS_PATH=${APPSTORE_EXPORT_OPTIONS_PATH} .PHONY: validate validate: # Validate IPA file # ASC_USERNAME=[user name] ASC_PASSWORD=[password] xcrun altool --validate-app -f ${IPA_PATH} -t ios -u ${ASC_USERNAME} -p ${ASC_PASSWORD} .PHONY: upload upload: # Distribute IPA file to App Store Connect # ASC_USERNAME=[user name] ASC_PASSWORD=[password] xcrun altool --upload-app -f ${IPA_PATH} -t ios -u ${ASC_USERNAME} -p ${ASC_PASSWORD}今回のMakefileは、本記事で使うコマンドのみを抜粋および編集して紹介しています。
私が普段使っているMakefileの全容はGitHub Gistにあるので、よかったら参考にしてください。設定ファイルの構成
CI環境の構築で紹介したので省略します。
各項目の紹介
各項目を上から順に紹介します。
trigger
AppCenter配布パイプラインの構築と同様、私は手動で配布したいため、トリガーをOFFにしています。
trigger: nonepool
CI環境の構築と同様なので省略します。
variables
AppCenter配布パイプラインの構築と同様なので省略します。
steps
パイプライン内のステップを1つずつ紹介します。
ライブラリや証明書のインストールなど、CI環境の構築やAppCenter配布パイプラインの構築と同様のステップは省略します。IPAファイルのバリデーション(任意)
IPAファイルをバリデーションします。
必須ではありませんが、配布前にアプリアイコンの画像が不足しているなどの問題に気づけます。- script: make validate ASC_USERNAME=$(AppStoreConnectUserName) ASC_PASSWORD=$(AppStoreConnectPassword) displayName: Validate IPA fileTestFlightへ配布
作成したIPAファイルをTestFlightへ配布します。
- script: make upload ASC_USERNAME=$(AppStoreConnectUserName) ASC_PASSWORD=$(AppStoreConnectPassword) displayName: Distribute IPA file to TestFlight設定ファイルの全体図
最後に設定ファイルの全体図を載せます。
trigger: none pool: vmImage: 'macos-latest' variables: - group: Foo-iOS steps: # 環境変数のエクスポート - script: | export DEVELOPER_DIR=/Applications/Xcode_11.3.1.app/Contents/Developer export MINT_PATH=mint/lib export MINT_LINK_PATH=mint/bin displayName: Export environment variables # Xcodeの一覧出力 - script: ls /Applications | grep 'Xcode' displayName: Show Xcode list # Xcodeのバージョン出力 - script: xcodebuild -version displayName: Show Xcode version # Bundlerで管理しているライブラリのインストール - script: make install-bundler displayName: Bundle install # Mintのインストール - script: brew install mint displayName: Install Mint # Carthageで管理しているライブラリのインストール - script: make install-carthage displayName: Install Carthage frameworks # ライセンス情報の生成、プロジェクトファイルの生成、CocoaPodsで管理しているライブラリのインストール - script: make generate-licenses displayName: Generate licenses, Xcode project, And Pod install # 証明書のインストール - task: InstallAppleCertificate@2 inputs: certSecureFile: 'Certificate.p12' certPwd: $(P12Password) keychain: 'temp' # Provisioning Profileのインストール - task: InstallAppleProvisioningProfile@1 inputs: provisioningProfileLocation: 'secureFiles' provProfileSecureFile: 'Foo_AppStore.mobileprovision' # IPAファイルの作成 - script: make generate-ipa-appstore displayName: Generate IPA file for App Store # アーカイブとIPAファイルをアーティファクトのステージングへコピー - task: CopyFiles@2 inputs: Contents: | **/*.xcarchive/**/* **/output/iphoneos/Release/* TargetFolder: '$(Build.ArtifactStagingDirectory)' # アーティファクトへアップロード - task: PublishBuildArtifacts@1 inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)' ArtifactName: 'drop' publishLocation: 'Container' # IPAファイルのバリデーション - script: make validate ASC_USERNAME=$(AppStoreConnectUserName) ASC_PASSWORD=$(AppStoreConnectPassword) displayName: Validate IPA file # TestFlightへ配布 - script: make upload ASC_USERNAME=$(AppStoreConnectUserName) ASC_PASSWORD=$(AppStoreConnectPassword) displayName: Distribute IPA file to TestFlightおまけ:App Store Connect API
App Store Connect APIを使うと、ユーザー名やパスワードを指定せずにTestFlightへの配布などを自動化できるようです。
https://developer.apple.com/app-store-connect/api/私は業務で使っているアカウントに権限がなかったので使えませんでした。
おわりに
Azure PipelinesのYAMLでiOSアプリをTestFlightへ配布することができました!
これでiOSアプリのCI/CD環境が整いました。
どんなアプリにも適用できるので、ぜひCI/CD環境構築の参考にしてください。参考リンク
- 投稿日:2020-03-19T10:16:23+09:00
Storyboardの名前を変えようとしたら思った以上に変えるべき場所が多かった話
環境
Xcode 11.3.1でSceneDelegateも含めた新規プロジェクトを作成した。
対応デバイスはiPadのみにしていた。何が起きたか?
アプリを製作中、ストーリーボード分割のためにMain.storyboardからViewController.storyboardに名前を変えて以下の部分を設定
これで大丈夫だと実行したところこのエラーが出た
Could not find a storyboard named 'Main' in bundle NSBundle解決法
iOS13にはiPad対応にするとStoryboardの名前を指定するところがInfo.plistに三ヶ所ありそのすべてを新しい名前にしないといけなかった。
この画像のViewControllerと書かれているところに新しいStoryboardの名前を入れておかなければいけない。
特にこのネストの深いところにある部分が本当に気づけない。ちなみにGUI上のMain Interfaceと同期したのはMain storyboard file name (iPad)のみだった。
感想
いや、知らんがな笑
- 投稿日:2020-03-19T07:39:53+09:00
flutter + firebaseで本番環境と開発環境を切り替える
この記事ですること
flutter run
とflutter run --release
でfirebaseの環境(プロジェクト)を切り替えるflutter run
とflutter run --release
でflutterの環境を切り替えるこの記事でしないこと
- 別アプリ化
- ステージング環境の構築
対象
個人開発者など、複雑な環境構築を必要としないけど、最低限、本番と開発環境を分けたい人
複雑に環境を分けたい人は
以下の記事が参考になります。
flutterで本番/ステージング/開発を切り替える - Qiita
Flutterで環境ごとにビルド設定を切り替える — iOS編
【iOS】FlutterでFlavorを使って環境ごとに切り替えてビルドする(debug/stg/prod)flutterのcreate
defalutでorganaizationがcom.exampleとなっていますが、exampleだとandroid play storeが受け付けてくれませんので、exampleはやめましょう。
com.exampleは禁止されているため、別のパッケージ名を使用する必要があります。
アプリケーション ID の設定よりfirebaseの設定
プロジェクトの追加
Firebaseプロジェクトから本番用と開発用の2つのプロジェクトを作ります。
名前は何でもいいです。規則やルールはありません。私は開発に-devと名付けています。
本番はbase-appで開発はbase-app-devと名付けました。アプリの追加
firebaseのプロジェクトの設定からアプリを追加してください。
androidとiosの設定を本番環境(base-app)と開発環境(base-app-dev)の両方に作ってください。iosのアプリの追加
IOSのバンドルIDはここのBundle Identiferです。
androidのアプリの追加
androidのバンドルIDはandroid -> app -> src -> main -> AndroidManifest.xmlの2行目
デバッグ用の署名証明書問題
デバッグ用の署名証明書 SHA-1(省略可)はgoogle sign in などで使用しますが、重複できませんので、本番環境側に入力しておきます。
重複するとこのパッケージ名と SHA-1 の組み合わせを持つ OAuth2 クライアントは、別のプロジェクトに既に存在しますというエラーがでます。
この手順の既存のプロジェクトはインポートしたくなく、Invites は使用していない。
の指示に従うと、開発と本番の両方でgoogle sign in が使用できるようになるのらしいですが、私はうまくいきませんでした。(詳細求む)firebase sdkの追加をします。ここでは省略します。
設定ファイルをダウンロードしておいてください。本番と開発が混ざらないように注意してください。
この時点でandroidとiosの2設定ファイル × 本番と開発の2環境 = 4ファイルflutterのiosの設定(xcode上)
iosの設定は少し面倒です。
targetのrunnerを開いてください。開き方がわからない方は下のgifを見てください。
一番左のフォルダマークをクリック -> Runnerをクリック -> TERGETSのRunnerをクリック
Build Phasesをクリックします。
+ボタンからnew run script phase
をクリック一番下に
Run Script
ができますので、リネームします。なんでもいいです。私はChoose Firebase Environment
と名付けました。ドラッグアンドドロップで
Run Script
の真下に持ってきます。
shellに以下を上書きしてください。
echo文いらないですね。必要ない人は削ってください。# Type a script or drag a script file from your workspace to insert its path. rm -rf "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist" echo "★★★" echo "-----${CONFIGURATION}-----" echo "-----${SRCROOT}-----" echo "-----${BUILT_PRODUCTS_DIR}-----" echo "-----${PRODUCT_NAME}-----" if [ "${CONFIGURATION}" = "Debug" ] ; then cp "$SRCROOT/Runner/GoogleService-Info-dev.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist" echo "Development GoogleService-Info copied." elif [ "${CONFIGURATION}" = "Release" ] ; then cp "$SRCROOT/Runner/GoogleService-Info-release.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist" echo "Release GoogleService-Info copied." fi何をしているのかというと、ビルドの度に
"${CONFIGURATION}"
で開発か本番か判断してビルドされたデータのGoogleService-Info.plist
を書き換えているのです。
力技でゴリ押してますね。次にコピー元ファイルを作ります。
GoogleService-Info-dev.plist
とGoogleService-Info-release.plist
を作ってRunner配下においてください。
名前は上記のshellと一致していればなんでもいいでです。
androidと同じようにフォルダつくってもいいかもしれません。私は試したことないですが。
xcodeの右側の画面を確認してみてください。FullPathがdownloadフォルダになっていないことを確認してください。
これでfirebaseの環境の切り替え設定が完了しました。
(補足) info.plistを分けたい場合
info.plistはアプローチが変わります。
下記のように$(変数名)
と指定します。
じゃあ、その変数はどこで設定するかというと、
TARGETのRunner -> Build Swtting -> +ボタン -> Add User-Defined Settingから設定します。
flutterのandroidの設定
androidの設定は簡単です。
以下のスクショと同じになるようにしてください。
開発環境
android/app/src/debug
フォルダに開発(base-app-dev)のgoogle-services.josn
を格納する本番環境
android/app/src/
にreleaseフォルダを作るandroid/app/src/release/
に本番(base-app)のgoogle-services.josn
を格納する
フォルダ名は必ずrelease
です。階層もdebugと同じ階層です。オリジナルのフォルダ名やオリジナルの階層ではいけません。
どうやら、debugとreleaseはandroid側のデフォルトの設定のようです。ですから、本番と開発だけなら複雑な設定なしで簡単に切り替えられるみたいです。詳細はこちら最後に
実際に
google-services.josn
を開いて5行目のproject_id
などから開発と本番が逆になってないか確かめてください。
あと、android studioから同期とflutter clean
コマンドを打つとよいでしょう。本番と開発が分離できているか確かめる
firestoreのデータを取得してみて環境の切り替えができているか確かめます。
firebase(firestore)の設定
テストモードで開始します。30日間で期限切れるのがいいですね。
ロケーションには気をつけましょう。変更できませんので。
asia-northeast1が東京、2が大阪です。データを作成します。
checkコレクションにドキュメントidは自動で、"name" = "本番"もしくは"開発"をつくることにします。
flutterの設定
- pubspec.yamlにcloud_firestoreを記述
- main.dartを以下に書き換え
main.dartimport 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } // 本番かリリースかを判断するには bool.fromEnvironment('dart.vm.product')を使う。 // よりわかりやすくするためにラップして使っている。 bool isRelease() { bool _bool; bool.fromEnvironment('dart.vm.product') ? _bool = true : _bool = false; return _bool; } class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( // This is the theme of your application. // // Try running your application with "flutter run". You'll see the // application has a blue toolbar. Then, without quitting the app, try // changing the primarySwatch below to Colors.green and then invoke // "hot reload" (press "r" in the console where you ran "flutter run", // or simply save your changes to "hot reload" in a Flutter IDE). // Notice that the counter didn't reset back to zero; the application // is not restarted. primarySwatch: Colors.blue, ), home: MyHomePage(), ); } } class MyHomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Center( child: Text( 'firestoreの動作確認\n(${isRelease() ? 'リリース' : 'デバック'}モード)'))), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text(isRelease() ? "リリースモード" : "デバッグモード", style: Theme.of(context).textTheme.title), NewWidget(), ], ), ), ); } } class NewWidget extends StatelessWidget { @override Widget build(BuildContext context) { return StreamBuilder<QuerySnapshot>( stream: Firestore.instance.collection('check').snapshots(), builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) { if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } switch (snapshot.connectionState) { case ConnectionState.waiting: return CircularProgressIndicator(); default: return Text('取得した値: ${snapshot.data.documents[0]['name']}'); } }, ); } }うまくいくと画面はこうなります。
実機でのリリースビルド
リリースビルドはシュミレーターでは動きませんので実機を使用します。
PCにはPixel3が接続されています。ビルドします。
android:flutter build apk
ios:flutter build ios
コンソールから
flutter devices
を実行します。このコマンドでインストール先のデバイスを探します。
実機にインストール
flutter install -d 8
先頭の1文字だけでもいいのが良いですね。
スクリーンは省略しますが、「本番」という値が取得できます。