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

Flutterを始めてListViewが難しい

きっかけ

長期休みに入り興味があったFlutterを始めた。
いろいろやってみて、まずListViewにつまづいた。つまづいたポイントとして、

  • クラスの使い方
  • 要素のUIの構成
  • どのように要素にデータを渡すか

がある。

開発はXamarinを軽く触ったことがある程度で、初心者。
書籍は基礎から学ぶFlutterを購入し、公式サイトも見ながらスタートしたFlutter超初心者。また、Flutterを開発するにあたって日本語の記事が異常に少ないので書こうと思った。これからも何かつまづくことがあればちょくちょく書く予定。

ListView

ListViewとは要素をスクロールできる画面に表示していくウィジェット。
FlutterにはYoutubeに公式チャンネルがあり、多くのウィジェットの紹介動画があり、短く、見やすい動画となっているのでおすすめ。もちろんListViewの動画もある。
ListView (Flutter Widget of the Week)

ListView.separated

ちょっと調べてみて、ListView.separatedコンストラクタが使いやすそうだったので使ってみた。
他にもListViewListView.builderListView.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.dart
Widget functionName(BuildContext context, int index){}

のように書く。

separatorBuilder

itemBuilderと同様の形式の関数を書く。公式サイトより引用し、

sample.dart
(BuildContext context, int index) => Divider()

と書いたが、よくわかっていないため後に追記する。
(知っている方いたら教えてください!)

itemCount

要素数。int型の整数値のみを入れる。List<T> listを使う場合、list.lengthを使うといいっぽい。

使用例

書いてみたプログラムと実行結果を示す。

プログラム

sample.dart
import '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-19 22.17.15.png

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

【Sign In with Apple】iOSアプリから投げられたidentityTokenをPHPで認証する

概要

identityToken というJWTがアプリから届いてパースしてsubをユニークキーとして認証するのですが
改ざん防止の為、正しくデコードする必要があります。

細かいことはバージョンが違ったり環境で変わってくると思うので大まかな流れだけ書いておきます。
※コメントで指摘をもらったのですが、これだけではAppleが発行したことしか担保出来ないため
別のアプリからの認証もしてしまうので足りていません。後日ドキュメントを追加するなりします。

  • appleの公開鍵を取得
  • 取得した公開鍵を利用して検証
  • 問題なければ取得できた値から sub を利用して認証

以上!!

要所要所の僕が使った部分を以下に記載してるので参考になれば幸いです。

JWTとは

JWT(ジョット)とは JSON Web Token の略で、電子署名付きの URL-safe(URLとして利用出来る文字だけ構成される)な JSONのことです。
電子署名により、JSON の改ざんをチェックできるようになっています。

ざっくり言うと、改ざんできない JSON ということになります。

JWT について調べた内容をまとめました。

パースしてみる

こちらのサイトでパース出来るので便利でした。
https://jwt.io/

Appleの鍵を取得する

Appleの鍵はこちらにありました。
Fetch Apple's public key for verifying token signature

Appleの鍵をFirebaseのJWTライブラリが理解出来る形に変更する

JWTの検証にFirebaseのライブラリを使ったのですが、複数の鍵で署名されてる場合
配列で鍵を指定出来るのですが形式が以下のような形なので変換して上げる必要があります。

Appleの鍵の中にあるenを使ってPEMを作れるので頑張って作ります。
この方の記事をめちゃくちゃ参考にさせてもらいました。
【PHP】AzureAPIなどで配布されている公開鍵の「e」「n」とはなにで、どう使うのか

$publicKeys = [
    'kidの値' => 'PEM形式のテキスト',
    'kidの値' => 'PEM形式のテキスト',
];

JWTの検証をして中身を取得する

Firebaseのライブラリを使って
アプリから投げられた identityTokenとさっき作った公開鍵の配列とAppleの鍵に書いてあった alg の値 RS256を指定すれば
改ざんされてなければJSONが返ってきます。

$payload = JWT::decode($identityToken, $publicKeys, ['RS256']);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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)
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

1人でアプリを作る

はじめに

こんにちは。
今回から、『Unity』を使ってスクロールゲームを作っていきます。
企画から完成までの過程を日記感覚で記録していきます!

制作環境

macOS Mojave バージョン 10.14.6
Unity 2019.2.19f1

企画

「近未来の世界で、主人公は機械で...」という、妄想を膨らませながら、
イメージを紙にサラサラと書いていきます。
ステージ構成や、プレイしてもらう時間、ターゲットなどをじっくり考えました。

実際に書いたものが下記画像になります。(きたない...)
gameRough.jpgflow.jpg
まあ、見づらくても、自分が分かっていればいいと思います。
ただ、他の人に見せるなら綺麗にまとめた方がいいかもしれません...。
今回は、一人で作るので、このまま進めます。

イメージイラスト

世界観や、設定が決まったので、イメージイラストを書きます。
手が動くままに書きました。
イメージイラストを書いてから、世界観や設定を決めていってもいいかもしれません。
characterRough.jpg

まとめ

今回は、企画をかんがえました。
内容を、事細かに説明していないので短くなっちゃいました...。

次回は、ゲームの流れがわかる、簡易的なモックアップを作ります。

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

1人でアプリを作る【その1】

はじめに

こんにちは。
今回から、『Unity』を使ってスクロールゲームを作っていきます。
企画から完成までの過程を日記感覚で記録していきます!

制作環境

macOS Mojave バージョン 10.14.6
Unity 2019.2.19f1

企画

「近未来の世界で、主人公は機械で...」という、妄想を膨らませながら、
イメージを紙にサラサラと書いていきます。
ステージ構成や、プレイしてもらう時間、ターゲットなどをじっくり考えました。

実際に書いたものが下記画像になります。(きたない...)
gameRough.jpgflow.jpg
まあ、見づらくても、自分が分かっていればいいと思います。
ただ、他の人に見せるなら綺麗にまとめた方がいいかもしれません...。
今回は、一人で作るので、このまま進めます。

イメージイラスト

世界観や、設定が決まったので、イメージイラストを書きます。
手が動くままに書きました。
イメージイラストを書いてから、世界観や設定を決めていってもいいかもしれません。
characterRough.jpg

まとめ

今回は、企画をかんがえました。
内容を、事細かに説明していないので短くなっちゃいました...。

次回は、ゲームの流れがわかる、簡易的なモックアップを作ります。

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

iCloud Documentsを使ってファイルの読み書きをする

まえがき

iCloudに置いたファイルをユーザー/アプリ間で共有し、どちらからでも自由にアクセスできる機能を実装します。(Keynoteとかがそうですね)

iCloud Documents Storageを使うと上手くいきそうな事はすぐ分かったのですが、実際に動くところまで持っていくのに結構苦労しました。そこで得られた知見を共有したいと思います。

公式ドキュメントはこちらです。

開発環境

macOS 10.15.3
Xcode 11.3.1
iPad OS 13.3.1

Apple Developer Programに登録したアカウントが必要です。

下準備

実装を始める前に、環境構築します。

プロジェクトの作成

Xcodeを立ち上げて、ツールバーからFile → New → Project... を選んでください。
Single View Appを選択し、 好きなProduct Nameを入力します。

1.png

Capabilityの追加

iCloudの機能を使うには、Capabilityを追加する必要があります。
赤丸で囲ったボタンを押して、iCloudを追加してください。
c.png

その後、iCloud Documents にチェックを入れます。

コンテナの追加

iCloudのCapabilityを追加したからと言って、iCloud内の全てにアクセスはできません。コンテナ(フォルダみたいなもの)を指定し、そのコンテナ内のみアクセスできます。

+ボタンを押して新しくコンテナを作成します。コンテナ名は好きなものを入れてください。こだわりが無ければProduct Nameと同じでいいと思います。

公式ドキュメントにある通り、コンテナは一度作ると削除することができません。typoしてないかよく確認してください。テスト用に適当に作ったものももちろん削除できないので注意してください。
d.png

コンテナ作成後、文字が赤くなっている場合は更新ボタンを押してください。

vv.png

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形式だと以下のような形になります。
df.png

  • 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.swift
import 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を編集を編集した場合は、以下おまじないをすると反映されるかと思います。

  1. Bundle Identifierを変更する。 Version, Buildの数字を増やす。スクリーンショット 2020-03-18 17.55.13.png
  2. Xcodeを再起動する
  3. Info.plistの編集が元に戻っていないか確認する(してることがあります!)
  4. Xcodeのツールバー → Product → Clean Build Folder
  5. Product → Run
  6. 変更が反映されたのを確認したら、Bundle Identifier, Version, Buildを元に戻す。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

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

Azure PipelinesのYAMLでiOSアプリのTestFlight配布パイプラインを構築する方法

「Azure PipelinesのYAMLでiOSアプリのCI/CD環境を構築する」は3部構成です。
記事を順番に読み進めると、Azure PipelinesでiOSアプリのCI/CD環境が構築できるようになります。

はじめに

Azure Pipelinesを使い、iOSアプリをTestFlightへ配布するCDを構築します。

本記事で書かないこと

証明書とProvisioning Profileのアップロード

証明書とProvisioning ProfileをAzure Pipelines内の安全な場所へアップロードします。

AppCenter配布パイプラインの構築で紹介したので省略します。

App Store Connectのユーザー名とパスワードの追加

App Store Connectのユーザー名とパスワードをAzure Pipelinesの安全な場所で保持します。

AppCenter配布パイプラインの構築を参考に、 AppStoreConnectUserNameAppStoreConnectPassword のようにわかりやすい名前で変数を追加してください。
パスワードにロックをかけるのを忘れないようにしましょう。

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 session

Makefileの作成

IPAファイルのバリデーションやTestFlight配布などの実行コマンドをまとめたMakefileを作成します。
CI環境の構築AppCenter配布パイプラインの構築で紹介したコマンドは省略します。

Makefile

APPSTORE_EXPORT_OPTIONS_PATH := ./ExportOptions/ExportOptionsAppStore.plist としているので、このパスに.plistファイルを配置してください。
Xcode上からアーカイブ→エクスポートすると.plistが生成されるので、それをそのまま使うのがオススメです。

Makefile
IPA_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: none

pool

CI環境の構築と同様なので省略します。

variables

AppCenter配布パイプラインの構築と同様なので省略します。

steps

パイプライン内のステップを1つずつ紹介します。
ライブラリや証明書のインストールなど、CI環境の構築AppCenter配布パイプラインの構築と同様のステップは省略します。

IPAファイルのバリデーション(任意)

IPAファイルをバリデーションします。
必須ではありませんが、配布前にアプリアイコンの画像が不足しているなどの問題に気づけます。

- script: make validate ASC_USERNAME=$(AppStoreConnectUserName) ASC_PASSWORD=$(AppStoreConnectPassword)
  displayName: Validate IPA file

TestFlightへ配布

作成した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環境構築の参考にしてください。

参考リンク

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

Storyboardの名前を変えようとしたら思った以上に変えるべき場所が多かった話

環境

Xcode 11.3.1でSceneDelegateも含めた新規プロジェクトを作成した。
対応デバイスはiPadのみにしていた。

何が起きたか?

アプリを製作中、ストーリーボード分割のためにMain.storyboardからViewController.storyboardに名前を変えて以下の部分を設定

image.png

これで大丈夫だと実行したところこのエラーが出た

Could not find a storyboard named 'Main' in bundle NSBundle

解決法

iOS13にはiPad対応にするとStoryboardの名前を指定するところがInfo.plistに三ヶ所ありそのすべてを新しい名前にしないといけなかった。

image.png

この画像のViewControllerと書かれているところに新しいStoryboardの名前を入れておかなければいけない。
特にこのネストの深いところにある部分が本当に気づけない。

ちなみにGUI上のMain Interfaceと同期したのはMain storyboard file name (iPad)のみだった。

感想

いや、知らんがな笑

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

flutter + firebaseで本番環境と開発環境を切り替える

この記事ですること

  • flutter runflutter run --releaseでfirebaseの環境(プロジェクト)を切り替える
  • flutter runflutter 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つのプロジェクトを作ります。
image.png

名前は何でもいいです。規則やルールはありません。私は開発に-devと名付けています。
本番はbase-appで開発はbase-app-devと名付けました。

アプリの追加

firebaseのプロジェクトの設定からアプリを追加してください。
androidとiosの設定を本番環境(base-app)と開発環境(base-app-dev)の両方に作ってください。

iosのアプリの追加

image.png
IOSのバンドルIDはここのBundle Identiferです。
image.png

androidのアプリの追加

androidのバンドルIDはandroid -> app -> src -> main -> AndroidManifest.xmlの2行目

デバッグ用の署名証明書問題

デバッグ用の署名証明書 SHA-1(省略可)はgoogle sign in などで使用しますが、重複できませんので、本番環境側に入力しておきます。
重複するとこのパッケージ名と SHA-1 の組み合わせを持つ OAuth2 クライアントは、別のプロジェクトに既に存在しますというエラーがでます。

この手順の既存のプロジェクトはインポートしたくなく、Invites は使用していない。の指示に従うと、開発と本番の両方でgoogle sign in が使用できるようになるのらしいですが、私はうまくいきませんでした。(詳細求む)

image.png

firebase sdkの追加をします。ここでは省略します。
設定ファイルをダウンロードしておいてください。本番と開発が混ざらないように注意してください。
この時点でandroidとiosの2設定ファイル × 本番と開発の2環境 = 4ファイル

flutterのiosの設定(xcode上)

iosの設定は少し面倒です。
targetのrunnerを開いてください。開き方がわからない方は下のgifを見てください。

一番左のフォルダマークをクリック -> Runnerをクリック -> TERGETSのRunnerをクリック

Feb-21-2020 16-29-20.gif

Build Phasesをクリックします。
+ボタンからnew run script phaseをクリック

image.png

一番下にRun Scriptができますので、リネームします。なんでもいいです。私はChoose Firebase Environmentと名付けました。

ドラッグアンドドロップでRun Scriptの真下に持ってきます。
image.png

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.plistGoogleService-Info-release.plistを作ってRunner配下においてください。

名前は上記のshellと一致していればなんでもいいでです。
androidと同じようにフォルダつくってもいいかもしれません。私は試したことないですが。

image.png

xcodeの右側の画面を確認してみてください。FullPathがdownloadフォルダになっていないことを確認してください。
image.png

これでfirebaseの環境の切り替え設定が完了しました。

(補足) info.plistを分けたい場合

info.plistはアプローチが変わります。
下記のように$(変数名)と指定します。
image.png

じゃあ、その変数はどこで設定するかというと、
TARGETのRunner -> Build Swtting -> +ボタン -> Add User-Defined Settingから設定します。
image.png

debugとreleaseに値を入れます。
image.png

flutterのandroidの設定

androidの設定は簡単です。
以下のスクショと同じになるようにしてください。
image.png

開発環境

  • 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)の設定

firestoreのデータベースを作成します。
image.png

テストモードで開始します。30日間で期限切れるのがいいですね。   
ロケーションには気をつけましょう。変更できませんので。  
asia-northeast1が東京、2が大阪です。

データを作成します。
checkコレクションにドキュメントidは自動で、"name" = "本番"もしくは"開発"をつくることにします。
image.png
image.png
image.png

flutterの設定

  • pubspec.yamlにcloud_firestoreを記述
  • main.dartを以下に書き換え
main.dart
import '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']}');
        }
      },
    );
  }
}

うまくいくと画面はこうなります。

image.png

実機でのリリースビルド

リリースビルドはシュミレーターでは動きませんので実機を使用します。
PCにはPixel3が接続されています。

ビルドします。
android: flutter build apk
ios: flutter build ios

コンソールからflutter devicesを実行します。このコマンドでインストール先のデバイスを探します。
image.png

実機にインストール
flutter install -d 8 先頭の1文字だけでもいいのが良いですね。
スクリーンは省略しますが、「本番」という値が取得できます。

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