Counts the Clouds

FlutterでiOSアプリ内にAppleの地図を表示する
2021.08.23

Flutter
iOS

revolt-P7giqV_xSQA-unsplash.jpg

apple_maps_flutter

platform_maps_flutterは導入でコケたので、まずは簡単なapple_maps_flutterの方から。

プロジェクトを作成してSimulatorでアプリを起動。

% flutter create flutter_platform_maps_trial
% cd flutter_platform_maps_trial
% open -a Simulator
% flutter run

pubspec.yamlの編集

dependencies:
  apple_maps_flutter: ^1.0.1

Info.plistの編集

To use this plugin on iOS you need to opt-in for the embedded views preview by adding a boolean property to the app’s Info.plist file, with the key io.flutter.embedded_views_preview and the value YES. You will also have to add the key Privacy - Location When In Use Usage Description with the value of your usage description.

以下のように追加した。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <!-- ... -->
  <key>io.flutter.embedded_views_preview</key>
  <true/>
  <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
  <string>This app requires user’s location for better user experience.</string>
</dict>
</plist>

io.flutter.embedded_views_previewはWebViewを許可する項目のようだ。WebViewが必要なんだろうか?(後述)

NSLocationAlwaysAndWhenInUseUsageDescriptionの中身は、位置情報を使いますよ、というユーザー向けの説明文でいいようだ。

main.dartの編集

apple_maps_flutterをインポートする部分。

import 'package:apple_maps_flutter/apple_maps_flutter.dart';

公式のサンプルは少し古いので修正。

  class AppleMapsExample extends StatelessWidget {
+   AppleMapsExample({Key? key}) : super(key: key);
+
-   AppleMapController mapController;
+   late AppleMapController mapController;

    void _onMapCreated(AppleMapController controller) {
      mapController = controller;
    }

    @override
    Widget build(BuildContext context) {
      return Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          Expanded(
            child: Container(
              child: AppleMap(
                onMapCreated: _onMapCreated,
                initialCameraPosition: const CameraPosition(
                  target: LatLng(0.0, 0.0),
                ),
              ),
            ),
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              Column(
                children: <Widget>[
-                 FlatButton(
+                 TextButton(
                    onPressed: () {
                      // ...
                    },
                    child: const Text('newCameraPosition'),
                  ),
-                 FlatButton(
+                 TextButton(
                    onPressed: () {
                      // ...
                    },
                    child: const Text('newLatLngZoom'),
                  ),
                ],
              ),
              Column(
                children: <Widget>[
-                 FlatButton(
+                 TextButton(
                    onPressed: () {
                      // ...
                    },
                    child: const Text('zoomIn'),
                  ),
-                 FlatButton(
+                 TextButton(
                    onPressed: () {
                      // ...
                    },
                    child: const Text('zoomOut'),
                  ),
-                 FlatButton(
+                 TextButton(
                    onPressed: () {
                      // ...
                    },
                    child: const Text('zoomTo'),
                  ),
                ],
              ),
            ],
          )
        ],
      );
    }
  }

finalじゃないフィールドがある、というLintメッセージの意味はわかったが解決はできなかった。

This class (or a class that this class inherits from) is marked as ‘@immutable’, but one or more of its instance fields aren’t final: AppleMapsExample.mapController

apple_maps_flutterで問題なく地図を表示できた。

platform_maps_flutter

platform_maps_flutterはiOSにもAndroidにも対応できるのでマルチプラットフォームに便利そうだが、ビルドがなかなか通らなくて難儀した。

pubspec.yamlの編集

dependencies:
  platform_maps_flutter: ^1.0.2

Info.plistは同じ。

main.dartの編集

import 'package:platform_maps_flutter/platform_maps_flutter.dart';

こちらのサンプルは最終的にそのままでも動いた。Lintに注意されたところだけ修正。

  class MyHomePage extends StatefulWidget {
    MyHomePage({Key? key}) : super(key: key);

    @override
    _MyHomePageState createState() => _MyHomePageState();
  }

  class HomePage extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        body: PlatformMap(
-         initialCameraPosition: CameraPosition(
+         initialCameraPosition: const CameraPosition(
-           target: const LatLng(47.6, 8.8796),
+           target: LatLng(47.6, 8.8796),
            zoom: 16.0,
          ),
          markers: // この部分は後述,
          myLocationEnabled: true,
          myLocationButtonEnabled: true,
          onTap: (location) => print('onTap: $location'),
          onCameraMove: (cameraUpdate) => print('onCameraMove: $cameraUpdate'),
          compassEnabled: true,
          onMapCreated: (controller) {
-           Future.delayed(Duration(seconds: 2)).then(
+           Future.delayed(const Duration(seconds: 2)).then(
              (_) {
                controller.animateCamera(
                  CameraUpdate.newCameraPosition(
                    const CameraPosition(
                      // ...
                    ),
                  ),
                );
              },
            );
          },
        ),
      );
    }
  }

しかし、buildに失敗。

% flutter run
Launching lib/main.dart on iPhone 12 in debug mode...
Running pod install...                                           2,806ms
Running Xcode build...
 └─Compiling, linking and signing...                      2,556ms
Xcode build done.                                           12.6s
Failed to build iOS app
Error output from Xcode build:

    ** BUILD FAILED **


Xcode's output:

    ld: building for iOS Simulator, but linking in object file built for iOS, file
    '/Users/.../ios/Pods/GoogleMaps/Base/Frameworks/GoogleMapsBase.framework/GoogleMa
    psBase' for architecture arm64
    clang: error: linker command failed with exit code 1 (use -v to see invocation)
    note: Using new build system
    note: Building targets in parallel
    note: Planning build
    note: Constructing build description

Could not build the application for the simulator.
Error launching application on iPhone 12.

調べると、EXCLUDED_ARCHSarm64を指定すればいいらしい。

  post_install do |installer|
    installer.pods_project.targets.each do |target|
      flutter_additional_ios_build_settings(target)
    end
+   installer.pods_project.build_configurations.each do |config|
+     config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
+   end
  end

Podfileに追加した上のコードは、下の画像のようなXCodeの設定をプログラム的に行っているようだ。

flutter-platform-maps-xcode-ss

pod installしたら動いた。

% cd ios
% pod install

いろいろ弄っている場合、pod cache cleanが必要かもしれない。わからん。

% cd ios
% pod cache clean --all
% pod install

FlutterでAppleの地図を表示できた。

Use collection literals when possible.

サンプルの以下の部分。

Set<Marker>.of(
  [
    Marker(
      // ...
    ),
  ],
)

Setリテラル{}を使う。

<Marker>{
  Marker(
    // ...
  ),
},

io.flutter.embedded_views_previewは何を指定している?

io.flutter.embedded_views_previewフラグも不要に( ´・‿・`)

Info.plistからio.flutter.embedded_views_previewの指定を外して、キャッシュを削除して問題なくビルドできるようだった。確信はない。

そして、io.flutter.embedded_views_previewはWebViewではなくPlatformViewを使うためのフラグのようだ。以前は必要だったが、今は指定しなくても良くなったと考えられる。

PlatformView を使えばビューが Flutter のウイジェットツリーに組み込まれるので、表示の階層やレイアウトが Flutter 側で制御できるようになり、取り回しが大変良くなります。