chakokuのブログ(rev4)

テック・コミック・DTM・・・ごくまれにチャリ

Flutterによるアプリ開発の勉強(2日目)-> AppStoreConnect経由でTest Flightまで

かなり適当にMacBookにFlutterを入れて、最低動くようになったので、次はソースコード作成(の前に、昨日適当に入れたFlutter環境の復習)。よくできたIDEを使うのがまともなんだけど、エディタのKeyBindの関係で自分は素のTerminal(Emacs)派なので、、TestDriveの章では、Terminal & editor を選択、
docs.flutter.dev

ちなみに、、Emacsな人用のプラグイン?(ELプログラム?)もあるようであった。
https://docs.flutter.dev/get-started/editor?tab=emacs
どういう理屈か分からないが、以下のコマンドで、MAC上でSimulatorが起動できる

% pwd
/Users/<user_name>/lang/flutter/my_app
% open -a Simulator

この状態で再度flutterを実行すると、どの環境で走らせるのか?と聞いてくる

% flutter run
[1]: iphone (46717xxxxxxxxxxxxxxxxxxxxx3434e)
[2]: iPod touch (7th generation) (EF6548CA-291A-4848-8EA1-48BB692CC5A2)
[3]: Chrome (chrome)
Please choose one (To quit, press "q/Q"):

[1]を選ぶと自分のiPhoneにアプリが表示されるはずだが、signatureでエラーになった。(実機でテストするのは大事だが、すぐにロックするで面倒というのもあり)後で調べるとして、、先ほど走らせたSimulatorの[2]にしてみる。
MAC上のSimulatorでテストプログラムが走り出した。この時のコンソールのメッセージは以下

Launching lib/main.dart on iPod touch (7th generation) in debug mode...
Running Xcode build...
 └─Compiling, linking and signing...                        71.8s
Xcode build done.                                           116.6s
Syncing files to device iPod touch (7th generation)...           2,284ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

💪 Running with sound null safety 💪

An Observatory debugger and profiler on iPod touch (7th generation) is available
at: http://127.0.0.1:49458/0h3Zk1RL3vg=/
The Flutter DevTools debugger and profiler on iPod touch (7th generation) is
available at: http://127.0.0.1:9100?uri=http://127.0.0.1:49458/0h3Zk1RL3vg=/

rを押すと Hot reloadと言ってるので、ソースを修正して、r の押下でSimulatorの画面が変わるよということか。

試しに、my_app/lib/main.dartの文字列を編集して、先ほどのコンソールでr を押してみる。
コンソールには以下のメッセージが表示されて、Simulatorに修正された文字列が表示された。

Performing hot reload...
Reloaded 1 of 583 libraries in 497ms.

デモ版のmain.dartに対して文字列を変えたところ。hot reloadでほとんど遅延なく更新が反映される

rを押すのが面倒だが、、まぁリビルドより格段に楽ではある。だいたい、やることが分かったので、、スクラッチから画面を作るレッスンに移りたい。Flutterで最初に起動されるのが、main.dartと思うがそれなりにややこしい。画面というかフレームの入れ子構造になっているのだろうという程度しから分からん。でも、フレームワークが全部把握できるのだとしたら、裏で何やってるのか分からないというフラストレーションは軽減される(公開されているソースが理解できるかどうかはおいといて)。
tutorialに倣っておらず順番はめちゃくちゃだが、今の環境で使えるデバイスは以下3種類、一つ目は自分の実機、残りはエミュレータやブラウザ

% flutter devices
ipodune (mobile)                     • 46717xxxxxxxxxxxxxxxxxx34e          • ios            • iOS 15.1 19B74
iPod touch (7th generation) (mobile) • EF6548CA-291A-4848-8EA1-48BB692CC5A2     • ios            • com.apple.CoreSimulator.SimRuntime.iOS-15-2 (simulator)
Chrome (web)                         • chrome                                   • web-javascript • Google Chrome 97.0.4692.99

次にやるべきは、、サンプルから離れて、自力?でスマフォアプリのHelloWorldを出すと。。
docs.flutter.dev
プロジェクトをすべて手で書くのは到底無理なので、多分、flutter create だろうと思いひな型は作ってもらう。作られたmain.dartを自力(tutorial)のソースに変更する

% flutter create testapp01
Signing iOS app for device deployment using developer identity: "Apple Development: <usr_name> (26xxxxxxxD)"
Creating project testapp01...
Running "flutter pub get" in testapp01...                           3.6s
Wrote 96 files.
All done!
In order to run your application, type:
  $ cd testapp01
  $ flutter run
Your application code is in testapp01/lib/main.dart.

tutorial通りなので書くほどでもないが、、以下のhello worldに書き換えて、flutter runでSimulator上でhello worldが表示される。

// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Welcome to Flutter',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Welcome to Flutter'),
        ),
        body: const Center(
          child: Text('Hello World'),
        ),
      ),
    );
  }
}

(WindowsPCからVNCMACの画面を表示)

自分の備忘録とご参考のためtutorialの解説を和訳して転記

  • このサンプルは、マテリアルアプリである。Materialは、モバイルとWebで標準となっているビジュアルデザイン言語である。
  • pubspec.yamlファイルのflutterセクションにuses-material-design: trueエントリを含めることを推奨。これにより、マテリアルのより多くの機能が使用可能となる
  • MyAppクラスは上位のStatelessWidgetクラスから継承されており、アプリ自体がウィジェット。Flutterでは、配置、パディング、レイアウトなど、ほとんどすべてがウィジェット
  • Scaffoldウィジェットは、デフォルトのアプリバーと、ホーム画面のウィジェットツリーを保持するbodyプロパティを提供する
  • ウィジェットの主な仕事はbuild()メソッドを提供すること。build()では下位レベルのウィジェットの表示方法を規定する
  • サンプルでは、子ウィジェットCenterを含むウィジェットで構成されている。TextCenterウィジェットは、ウィジェットサブツリーを画面の中央にそろえる

次のお題は、無限リストビューであった。ソースは以下

// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context){
    return const MaterialApp(
        title: 'Startup Name Generator',
        home: RandomWords(),
    );
  }
}
class _RandomWordsState extends State<RandomWords> {
   final _suggestions = <WordPair>[];
   final _biggerFont = const TextStyle(fontSize: 18);

   Widget _buildSuggestions(){
     return ListView.builder(
        padding: const EdgeInsets.all(16),
        itemBuilder: /*1*/ (context, i){
           if (i.isOdd){
                 return const Divider();  /*2*/
         }
         final index = 1 ~/2;  /*3*/
           if (index >= _suggestions.length){
            _suggestions.addAll(generateWordPairs().take(10)); /*4*/
           }
         return _buildRow(_suggestions[index]);
        },
     );
   }
   Widget _buildRow(WordPair pair){
      return ListTile(
        title: Text(
          pair.asPascalCase,
        style: _biggerFont,
        ),
      );
   }
  @override
  Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
           title: const Text('Startup Name Generator'),
        ),
        body: _buildSuggestions(),
      );
   }
}
class RandomWords extends StatefulWidget {
   const RandomWords({Key? key }):super(key:key);
   @override
   _RandomWordsState createState() => _RandomWordsState();
}

以下が、ソースの作成、ならびに、実行中の画面*1

正しく動くとランダムに名前が出るはずだが、同じ名前が出ている。どこかにバグがある。が、、まだコード分かっていないのでデバッグできない。スキルが高まると、原因が分かるようになるだろう。そのうち、多分。
バグっているとはいえ、自分のiphoneでも動くように、releaseビルドをしている。。エラーになった。

% flutter run  --release
Multiple devices found:
iphone (mobile)                      ? 4671xxxxxxxxxxxx4e ? ios            ? iOS 15.1 19B74
iPod touch (7th generation) (mobile) ? EF6548CA-291A-4848-8EA1-48BB692CC5A2 ? ios ? com.apple.CoreSimulator.SimRuntime.iOS-15-2 (simulator)
Chrome (web)                         ? chrome                                   ? web-javascript ? Google Chrome 97.0.4692.99
[1]: iphone (4671xxxxxxxxxxxxxxxxxxxxxxxx434e)
[2]: iPod touch (7th generation) (EF6548CA-291A-4848-8EA1-48BB692CC5A2)
[3]: Chrome (chrome)
Please choose one (To quit, press "q/Q"): 1

Launching lib/main.dart on iphone in release mode...
Automatically signing iOS for device deployment using specified development team in Xcode project: JWxxxxxxS9
Running Xcode build...
Xcode build done.                                           14.1s
Failed to build iOS app
Error output from Xcode build:
?
    ** BUILD FAILED **

Xcode's output:
?
    Writing result bundle at path:
        /var/folders/ky/0l1q46ts4tx2sn7dg9hsjbpw0000gp/T/flutter_tools.3csiU6/flutter_ios_build_temp_dirarTlAc/temporary_xcresult_bundle

    Failed to package /Users/<user_name>/lang/flutter/testapp01.
    Command PhaseScriptExecution failed with a nonzero exit code
    note: Using new build system
    note: Planning
    note: Build preparation complete
    note: Building targets in dependency order

    Result bundle written to path:
        /var/folders/ky/0l1q46ts4tx2sn7dg9hsjbpw0000gp/T/flutter_tools.3csiU6/flutter_ios_build_temp_dirarTlAc/temporary_xcresult_bundle

Could not build the precompiled application for the device.
It appears that your application still contains the default signing identifier.
Try replacing 'com.example' with your signing id in Xcode:
  open ios/Runner.xcworkspace
Error running application on iphone.

signing idがおかしいと書かれているようだが、、署名が違ってるのか?

% pwd
/Users/<user_name>/lang/flutter/testapp01/ios/Runner.xcodeproj

% cat project.pbxproj | grep example
                                PRODUCT_BUNDLE_IDENTIFIER = com.example.testapp01;
                                PRODUCT_BUNDLE_IDENTIFIER = com.example.testapp01;
                                PRODUCT_BUNDLE_IDENTIFIER = com.example.testapp01;

上記はexampleのままなので、、これは変える必要があるだろう。。
これは、もとのTutorialの、「Deploy to iOS devices」のセクションで、bundle IDをチェックしろよというのをすっ飛ばしているからだろう。。

appleのdeveloperサイトでAppIDを新たに取って、bundleIDに設定した。以下がAppIDをbundleIDとして入力しているところ。名前は編集しています。

で再度ビルドした。先ほどのsigningエラーは出なくなった。が、、どうも実機に焼くところか何かで新たなエラーが出ている。

% flutter run  --release
Multiple devices found:
iphone (mobile)                     ? 4671xxxxxxxxxxxxxxxxxxxx34e ? ios            ? iOS 15.1 19B74
iPod touch (7th generation) (mobile) ? EF6548CA-291A-4848-8EA1-48BB692CC5A2     ? ios            ? com.apple.CoreSimulator.SimRuntime.iOS-15-2 (simulator)
Chrome (web)                         ? chrome                                   ? web-javascript ? Google Chrome 97.0.4692.99
[1]: iphone (4671xxxxxxxxxxxxxxxxxxxxxxx3434e)
[2]: iPod touch (7th generation) (EF6548CA-291A-4848-8EA1-48BB692CC5A2)
[3]: Chrome (chrome)
Please choose one (To quit, press "q/Q"): 1

Launching lib/main.dart on iphone in release mode...
Automatically signing iOS for device deployment using specified development team in Xcode project: JWxxxxxxS9
Running Xcode build...
Xcode build done.                                           21.7s
Failed to build iOS app
Error output from Xcode build:
?
    2022-02-05 17:50:55.341 xcodebuild[6245:135119]  DVTAssertions: Warning in
    /Library/Caches/com.apple.xbs/Sources/DVTiOSFrameworks/DVTiOSFrameworks-19527/DTDeviceKitBase/DTDKRemoteDeviceData.m:373
    Details:  (null) deviceType from 46717xxxxxxxxxxxxxxxxxxxx3434e was NULL when -platform called.
    Object:   <DTDKMobileDeviceToken: 0x7f948bf6afe0>
    Method:   -platform
    Thread:   <NSThread: 0x7f948bf0f990>{number = 4, name = (null)}
    Please file a bug at https://feedbackassistant.apple.com with this warning message and any useful information you can provide.
    2022-02-05 17:50:55.607 xcodebuild[6245:135215]  DVTAssertions: Warning in
    /Library/Caches/com.apple.xbs/Sources/DVTiOSFrameworks/DVTiOSFrameworks-19527/DTDeviceKitBase/DTDKRemoteDeviceData.m:373
    Details:  (null) deviceType from 4671xxxxxxxxxxxxxxxxxxxxxxxx3434e was NULL when -platform called.
    Object:   <DTDKMobileDeviceToken: 0x7f948bf6afe0>
    Method:   -platform
    Thread:   <NSThread: 0x7f948bfab5c0>{number = 8, name = (null)}
    Please file a bug at https://feedbackassistant.apple.com with this warning message and any useful information you can provide.
    2022-02-05 17:50:55.681 xcodebuild[6245:135215]  DVTAssertions: Warning in
    /Library/Caches/com.apple.xbs/Sources/DVTiOSFrameworks/DVTiOSFrameworks-19527/DTDeviceKitBase/DTDKRemoteDeviceData.m:373
    Details:  (null) deviceType from 4671xxxxxxxxxxxxxxxxxxxxxxx3434e was NULL when -platform called.
    Object:   <DTDKMobileDeviceToken: 0x7f948bf6afe0>
    Method:   -platform
    Thread:   <NSThread: 0x7f948bfab5c0>{number = 8, name = (null)}
    Please file a bug at https://feedbackassistant.apple.com with this warning message and any useful information you can provide.
    ** BUILD FAILED **

Xcode's output:
?
    Writing result bundle at path:
        /var/folders/ky/0l1q46ts4tx2sn7dg9hsjbpw0000gp/T/flutter_tools.SqfCOY/flutter_ios_build_temp_dirXe5XyX/temporary_xcresult_bundle


    Failed to package /Users/<user_name>/lang/flutter/testapp01.
    Command PhaseScriptExecution failed with a nonzero exit code
    note: Using new build system
    note: Planning
    note: Build preparation complete
    note: Building targets in dependency order

    Result bundle written to path:
        /var/folders/ky/0l1q46ts4tx2sn7dg9hsjbpw0000gp/T/flutter_tools.SqfCOY/flutter_ios_build_temp_dirXe5XyX/temporary_xcresult_bundle


Could not build the precompiled application for the device.

Error running application on iphone.

Result bundle written to pathと書かれていて、パスに書き込まれたバンドル結果?? うーん分からん。少なくとも上記パスは存在しない
同じコマンドをもう一度打つと今度はメッセージが少なくなっている(がエラーは変わらない)

              *略*

Running Xcode build...
Xcode build done.                                           21.0s
Failed to build iOS app
Error output from Xcode build:
?
    ** BUILD FAILED **

Xcode's output:
?
    Writing result bundle at path:
        /var/folders/ky/0l1q46ts4tx2sn7dg9hsjbpw0000gp/T/flutter_tools.sTvHsK/flutter_ios_build_temp_dirSiGCl1/temporary_xcresult_bundle

    Failed to package /Users/<user_name>/lang/flutter/testapp01.
    Command PhaseScriptExecution failed with a nonzero exit code
    note: Using new build system
    note: Planning
    note: Build preparation complete
    note: Building targets in dependency order

    Result bundle written to path:
        /var/folders/ky/0l1q46ts4tx2sn7dg9hsjbpw0000gp/T/flutter_tools.sTvHsK/flutter_ios_build_temp_dirSiGCl1/temporary_xcresult_bundle
Could not build the precompiled application for the device.
Error running application on iphone.

いずれにせよXcodeが出しているエラーなので、flutterのせいというより、Xcodeのビルドオプションか、ターゲットデバイスの設定で何か問題と思われる。CLIで動かさずにXcodeGUIから動かしたらどうなるか。。

Xcodeでリリース用ビルドをやってみた。一応ビルドができて、AppStoreConnectにもアップロードできた。TestFlightで動くか試してみた。以下がTestFlightの画面、テスターとして自分を加える。

iPhoneのTestFlightで、アップロードしたアプリが表示されるので、開くボタンを押してインストール実施

アプリがインストールされる。

実行する。バグは残ったままだが動作が確認できた。

このバイナリがFlutter的にDebugモードなのかReleaseモードなのか?自分は理解不十分で分かっていない。また、flutterコマンドでRelease用バイナリをXcodeで生成できないといけないのだが、今はそこが成功していない(今回は手動で行った。しかもそれはDebugコードなのか、Releaseコードなのか分かっておらず)

Flutterのドキュメントが大量でちゃんと読んでいなかったが、iOS用のビルド・リリース手順が以下にあった。Xcodeは普通に動いていてあまりCLIで設定変更はやりたくないのだが、、このコマンドを一通り実行しないといけないのだろうか。CLIでやる作業とXcodeでやる作業の切り分けがよくわかっていない。
docs.flutter.dev

上記を読むと、Xcodeで諸々の設定を終えた後で、以下のように、 flutter build ipa と打ち込むようである*2

Run 
   flutter build ipa 
to produce a build archive.

後半に書かれている、「Create a build archive with Codemagic CLI tools」の章は、Xcodeを使わず、全部CLIでやりたい場合の手順と理解しました。普段はCLIで動かしたい人間ですが、、ビルド、署名、AppStoreConnectへのアップロードをCLIでやるにはあまりにもトラップが多そうで、ためらってしまう。CLIでコンフィグ変えてしまったら戻すに戻せないような気も。。。
よって、自分が適当にやったアップロードとの違いは、Xcodeから手動でビルドするか、あるいは、flutter build ipaをコマンドで実行するかの違いと理解しました。

*1:作業はWindowsノートでやっている。sshMac BookにログインしてEmacs上でソース編集、VNCMacの画面を表示してSimulatorの結果を確認

*2:On versions of Flutter where flutter build ipa is unavailable, open Xcode and select Product > Archive. とも書かれているので、XcodeでArchiveを起動しても同じ結果になるはず