MapLibre Native for iOS のセットアップ (SwiftUI) 2024年版

[MapLibre](https://maplibre.org/) は、フリーでオープンソースな地図ライブラリのプロジェクトです。様々なライブラリがありますが、iOSやAndroidといったネイティブアプリ向けのライブラリもあります。

この記事では [MapLibre Native for iOS](https://github.com/maplibre/maplibre-native/tree/main/platform/ios) のセットアップ方法を備忘録として残します。

## パッケージのインストール

Swift Package Manager を使って Maplibre Native のパッケージをインストールします。

Xcodeから　`File` -> `Add Package Dependencies` をクリック

右上の検索欄に `MapLibre` あるいは　`https://github.com/maplibre/maplibre-gl-native-distribution` と入力すると、下記の画像のように検索結果が表示される。

![スクリーンショット 2024-04-15 22.15.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93706/d57da892-3836-0c2a-73f2-a2bd5b8634fe.png)

この状態で `Add Package` をクリック

![スクリーンショット 2024-04-15 22.16.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93706/566f3a67-c675-2803-3cfe-3aec52248d0c.png)

右側の `Add to Target` で、Xcodeプロジェクトのターゲットを選択して `Add Package` をクリック

これで、導入は完了です。

## MapView

::: note info
MapLibre Native for iOSは、デフォルトでSwiftUIには対応していないため、新規に [ UIViewRepresentable ](https://developer.apple.com/documentation/swiftui/uiviewrepresentable) を継承したソースファイルを作成する必要があります。
:::

Xcodeのプロジェクトに `MapView.swift` を作成して、必要なライブラリをインポートしつつ MapView の struct を作成します。

``` MapView.swift
import SwiftUI
import MapLibre
import MapKit

struct MapView: UIViewRepresentable {
    ...
}
```

### MapTiler APIキー

[MapTiler Cloud](https://www.maptiler.com/cloud/) から配信されている地図を表示するので、MapTilerのAPIキーを取得する関数を書きます。

MapTilerのAPIキーを `Info.plist` に追記をして、以下のようにプログラム上で取得します。

``` MapView.swift
/// MapTilerのキーを取得します
/// - Returns: MapTilerのAPIキー
func getMapTilerKey() -> String {
  let key = Bundle.main.object(forInfoDictionaryKey: "MapTilerKey") as? String

  guard let key = key else {
    preconditionFailure("Failed to read MapTiler Key")
  }

  return key
}
```

### makeUIView

Viewが生成されたときに実行される `makeUIView` 関数を定義します。

``` MapView.swift
func makeUIView(context: Context) -> some UIView {
  // MapTilerのキーを取得
  let mapTilerKey = getMapTilerKey()

  // スタイルのURLを定義
  let styleURL = URL(string: "https://api.maptiler.com/maps/streets-v2/style.json?key=\(mapTilerKey)")

  // Viewを定義
  let mapView = MLNMapView(frame: .zero, styleURL: styleURL)
  mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  mapView.logoView.isHidden = true
  mapView.setCenter(
    CLLocationCoordinate2D(latitude: 35.681111, longitude: 139.766667),
    zoomLevel: 15.0,
    animated: false
  )

  // DelegateはCoordinatorを指定します
  mapView.delegate = context.coordinator

  return mapView
}
```

### updateUIView

Viewに更新があったときに実行される `updateUIView` を定義しますが、今回は特に何もしないので中身は空です。

``` MapView.swift
func updateUIView(_ uiView: UIViewType, context: Context) {
  /// Viewがアップデートされたときの処理
  /// 現状は特に何も処理をします
}
```

### Coordinator

マップが読み込まれた時などのイベント処理は、以下のように `Coordinator` を定義して、その中に記述します（この記事では、イベント関数を定義するのみに留めます）。

``` MapView.swift
struct MapView: UIViewRepresentable {
  ...

  class Coordinator: NSObject, MLNMapViewDelegate {
    var control: MapView

    init(control: MapView) {
      self.control = control
    }

    func mapViewDidFinishLoadingMap(_ mapView: MLNMapView) {
      // マップのローディングが終わったときの処理
    }
  }

  ...

}
```

そして `makeCoordinator` 関数を定義して `Coordinator` を生成します。

``` MapView.swift
func makeCoordinator() -> Coordinator {
  Coordinator(control: self)
}
```

### 全体像

MapView.swiftの全体像は、以下のようになります。

``` MapView.swift
import SwiftUI
import MapLibre
import MapKit

struct MapView: UIViewRepresentable {

  func makeUIView(context: Context) -> some UIView {
    // MapTilerのキーを取得
    let mapTilerKey = getMapTilerKey()

    // スタイルのURLを定義
    let styleURL = URL(string: "https://api.maptiler.com/maps/streets-v2/style.json?key=\(mapTilerKey)")

    // Viewを定義
    let mapView = MLNMapView(frame: .zero, styleURL: styleURL)
    mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    mapView.logoView.isHidden = true
    mapView.setCenter(
      CLLocationCoordinate2D(latitude: 35.681111, longitude: 139.766667),
      zoomLevel: 15.0,
      animated: false
    )

    // DelegateはCoordinatorを指定します
    mapView.delegate = context.coordinator

    return mapView
  }

  func updateUIView(_ uiView: UIViewType, context: Context) {
    /// Viewがアップデートされたときの処理
    /// 現状は特に何も処理をします
  }

  func makeCoordinator() -> Coordinator {
    Coordinator(control: self)
  }

  class Coordinator: NSObject, MLNMapViewDelegate {
    var control: MapView

    init(control: MapView) {
      self.control = control
    }

    func mapViewDidFinishLoadingMap(_ mapView: MLNMapView) {
      // マップのローディングが終わったときの処理
    }
  }

  /// MapTilerのキーを取得します
  /// - Returns: MapTilerのAPIキー
  func getMapTilerKey() -> String {
    let key = Bundle.main.object(forInfoDictionaryKey: "MapTilerKey") as? String

    guard let key = key else {
      preconditionFailure("Failed to read MapTiler Key")
    }

    return key
  }

}
```

## ContentView

アプリ起動後に表示される `ContentView` で、上記で作成した `MapView` を呼び出します。

``` ContentView.swift
import SwiftUI

struct ContentView: View {
  var body: some View {
    MapView()
      .ignoresSafeArea()
  }
}

#Preview {
  ContentView()
}

```

## プレビュー

この状態でプレビューを確認すると、以下のように地図が表示されます。

![スクリーンショット 2024-04-16 1.12.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93706/44b1bb5c-558c-36f6-46e0-ddcf34c19218.png)

## 追記情報

MapLibre Native for iOS では v6.0.0 で `Mapbox` としてインポートするのではなく `MapLibre` としてインポートするように変更されたようです。

また、地図表示に関連する関数名も Mapbox GL ベースから　 MapLibre Native ベースの名称に変更となりました。

さらに、このバージョンで地図描画が Metal レンダリングに対応しました。

詳しくは　[v6.0.0 のリリースノート](https://github.com/maplibre/maplibre-native/releases/tag/ios-v6.0.0) を御覧ください。