MapLibre GL Native for iOSを簡単に触ってみた

MapLibre GLがiOSでも実装できると聞きましたので，今回はMapLibre GL Native for iOSを簡単に触ってみました．

## 参考サイト

MapTilerさんが MapLibre GL Native for iOS の [チュートリアル](https://docs.maptiler.com/maplibre-gl-native-ios/ios-swiftui-basic-get-started/) を書いています．これを参考にしながら触ってみます．

## 環境

* Xcode 14.2
* Swift 5.7

今回はSwiftUIで実装していきます．

## 事前準備

Xcodeのプロジェクトの作成は省略します．

MapLibre GL Native for iOSを入れるために，パッケージをインストールします．

画面上部の「File」=>「Add Packages...」を選択します．

<img width="320px" alt="メニュー選択" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93706/1969e234-0ffb-ac53-8ee9-edfd6740cd3b.png">

右上の検索バーにMapLibre GLのGithub URL ( `https://github.com/maplibre/maplibre-gl-native-distribution` ) を入力します．その後，「maplibre-gl-native-distribution」を選択した状態で 「Add Package」 を押します．

<img width="640px" alt="インストール１" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93706/b8438747-4336-1310-a3e1-441badbb717c.png">

途中でmapboxのパッケージをインストールする画面が出てきます．これも「Add Package」を押します．

<img width="640px" alt="インストール１" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93706/ae7a8a23-67b4-9009-fb83-726889267e7b.png">

左のサイドバーの「Package Dependencies」に「MapLibew GL Native」が表示されていれば，インストール完了です．

<img width="320px" alt="サイドバー" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93706/7f095726-e668-9b82-d6be-b98b0031d3ab.png">

また，TARGETS => General => Frameworks, Libraries, and Embedded Content の中に「Mapbox」があるか確認してください．

<img width="320px" alt="TARGETS" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93706/0767e9ca-7884-d484-87ad-197148708c03.png">

<img width="640px" alt="Frameworks" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93706/63f18250-6702-478a-f682-2faf6cd98113.png">

## マップ表示の実装

マップを表示する部分を実装していきます．今回はSwiftUIで実装します．表示に関わるフォルダ構成は下記のとおりです（少し省略しています）．

 `MGLMapView` は，SwiftUIには対応しておらず，UIKitで定義する必要があります．SwiftUIに適用するために， `MapLibreView.swift` に `MGLMapView` の定義をしていきます．

<img width="320px" alt="Frameworks" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93706/d929377a-65e9-a074-e6c4-d54dd0c58cb5.png">

### MapLibreView.swift

UIKitでのみ対応しているライブラリをSwiftUIに適用するためには， `UIViewRepresentable` を継承してViewを定義します．まずは，全体のソースコードです．

:::note warn
API Keyは適切に管理してください．
`.env` ファイルを用いて管理する方法や， `.plist` ファイルを用いて管理する方法などが挙げられます．
:::

```MapLibreView.swift
import Mapbox
import MapKit
import SwiftUI

struct MapLibreView: UIViewRepresentable {
    
    func updateUIView(_ uiView: MGLMapView, context: Context) {
        
    }
    
    func makeUIView(context: Context) -> MGLMapView {
        let mapTilerAPIKey = {YOUR_MapTiler_APIKEY}
        let styleURL = URL(string: "https://api.maptiler.com/maps/basic-v2/style.json?key=\(mapTilerAPIKey)")
        let mapView = MGLMapView(frame: .zero, styleURL: styleURL)
        
        // Mapboxのロゴを消します．
        mapView.logoView.isHidden = true
        
        mapView.setCenter(
            CLLocationCoordinate2D(latitude: 35.6809591, longitude: 139.7673068),
            zoomLevel: 14.0,
            animated: false
        )
        
        mapView.delegate = context.coordinator
        
        return mapView
    }
    
    func makeCoordinator() -> MapLibreView.Coordinator {
        Coordinator(control: self)
    }
    
    class Coordinator: NSObject, MGLMapViewDelegate {
        var control: MapLibreView
        
        init(control: MapLibreView) {
            self.control = control
        }
        
        func mapViewDidFinishLoadingMap(_ mapView: MGLMapView) {
            // マップがロードされた後の処理を記載
        }
    }
}
```

`makeUIView` は，Viewが立ち上がるときにどういった処理をするのか定義します．今回は，マップ自体のスタイルの定義や，中心位置の設定などを記載しています．マップのスタイルは，MapTilerのベクタータイルを利用します．

`updateUIView` は，ビューが何かしらの更新をしたときにどういった処理をするのか定義します．今回は，特に何も処理をしないので空白です． `UIViewRepresentable` プロトコルに準拠する必要があるため，空白でも関数は定義する必要があります．

`Coordinator` クラスは，Viewの中でアクションが発生した場合にどういった処理をするのか定義します．わかりやすい例でいうと，ボタンをタップしたときの処理などは，この `Coordinator` クラスに定義します．

今回，`Coordinator` クラスでは，予めマップがロードされた後に実行される関数を書いていますが，中身は空の状態で定義しています．

### ContentView.swift

`ContentView.swift` は，上記で作成した `MapLibreView` を `body` の中に定義します．

```ContentView.swift
import SwiftUI
import CoreData

struct ContentView: View {

    var body: some View {
        MapLibreView()
            .edgesIgnoringSafeArea(.all)
    }
    
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

```

`.edgesIgnoringSafeArea(.all)` で， `MapLibreView` をフル画面表示にします．

### App.swift

一番最初に表示するビューを定義する App.swift(今回は `MapLibreNativeSampleApp.swift`）には `ContentView` を定義します．このファイル名は，一番最初にXcodeで作成したプロジェクト名が，そのまま反映されます．

```MapLibreNativeSampleApp.swift
import SwiftUI

@main
struct MapLibreNativeSampleApp: App {

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

```

### 動作確認

この状態でエミュレータ or 実機で動作確認をすると，下記のように東京駅を中心としたMapTilerのベクトルマップが表示されます．

<img width="320px" alt="Frameworks" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93706/6cbcf250-9112-c49f-7b6b-f0f16f3f2a4d.png">

## 指定したポイントに画像を表示する

次に，指定した場所にポイントを示す画像を表示してみたいと思います．

### 画像の定義

画像を用意するにあたり，適当に作成した画像を，Xcode内の `Assets` に定義していきます．今回は `PointIcon` という名前で定義します．ちなみに，画像のサイズは 64px x 64px です．

<img width="640px" alt="Frameworks" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93706/34e9951d-6352-57d2-ce8b-ce53b09ba36f.png">

本当はRetinaディスプレイ用に2x，3xも定義する必要がありますが，今回は1xのみ定義しています．

また，右のRender Asは `Default` に設定しておきます（赤枠の部分）． `Template image` に設定すると，マップ上に表示される画像の色定義ができなくなります．

### MapLibreView.swiftを編集する

Assetsの定義が完了したら， 地図上に画像アイコンを表示する処理を `MapLibreView.swift` の `createMarker` 関数で定義していきます．

この関数の中では，主に画像を表示する場所の定義（ `shapeSource` ） と，画像を表示するレイヤーの定義（ `shapeLayer` ） をしています．マップ上に表示する画像の定義は `MGLStyle` の `setImage` で定義します．

```MapLibreView.swift
func createMarker(_ mapView: MGLMapView, _ style: MGLStyle) {
    // ポイントの定義
    let point = MGLPointAnnotation()
    point.coordinate = CLLocationCoordinate2D(latitude: 35.6809591, longitude: 139.7673068)
    
    // ポイントのデータソース定義
    // 上記で定義したポイントをマップ上に表示するデータソースとして定義します
    let shapeSource = MGLShapeSource(identifier: "tokyo-station-source", shape: point)
    
    // シェイプレイヤーの定義
    // ポイントのデータソースをマップ上に表示するレイヤーに定義します
    let shapeLayer = MGLSymbolStyleLayer(identifier: "tokyo-station-style", source: shapeSource)

    // Assetsの画像をマップ表示用に定義
    if let image = UIImage(named: "PointIcon"){
        style.setImage(image, forName: "point-icon")
    }
    
    // マップ上に表示するレイヤーに，styleで設定した画像の名前を定義
    shapeLayer.iconImageName = NSExpression(forConstantValue: "point-icon")
    
    // スタイルにソースとレイヤーを追加
    style.addSource(shapeSource)
    style.addLayer(shapeLayer)
}

```

---

次に，`MapLibreView.swift` の中の `Coordinator` クラスを編集します．

今回は `mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle)` 関数を定義しています．この関数は，マップのローディングが完了したときに呼び出される関数で， `MGLMapView` と `MGLStyle` を引数として受け取って処理をします．

関数の中身は，上記で定義した `createMarker` を呼び出しています．

```MapLibreView.swift
class Coordinator: NSObject, MGLMapViewDelegate {
    var control: MapLibreView
    
    init(control: MapLibreView) {
        self.control = control
    }
    
    func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) {
        self.control.createMarker(mapView, style)
    }

}
```

### 動作確認

この状態で，エミュレータ or 実機で動作確認をすると，東京駅の部分に `Assets` て定義した画像が表示されます．

<img width="320px" alt="Frameworks" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93706/bb43167c-dcd4-5c40-31f4-461b240c41bc.png">

## 最後に

今回は，地図の表示と特定のポイントに画像を表示する処理を実装してみました．ライブラリがUIKitベースであるため，SwiftUIに適用させるためには工夫が必要ですが，そこまで難しくない実装で地図を表示することができました．

他にも，マップ上にLineStringを引いたり，Polygonを描画することもできます．GeoJSONを読み込んで表示する実装もできるそうなので，試してみたいと思います．

なお，私の環境ではMapLibre GLのライブラリをインストールしているのにもかかわらず，ソースコード内でMapLibreに付随しているMapboxライブラリが読み込まれない場合がありました．ただ，もう一度インストールをやり直すと，ライブラリの読み込みができました．

動作確認画面で表示している地図の著作権: [© MapTiler](https://www.maptiler.com/copyright/?_gl=1*1pmapaa*_ga*MTc3MzIzMjcxNS4xNjY4OTk5Mzkw*_ga_K4SXYBF4HT*MTY3MTQxOTIwNC4xLjEuMTY3MTQxOTQ1NS4zMi4wLjA.&_ga=2.123626243.986437545.1671419205-1773232715.1668999390) [© OpenStreetMap contributors](https://www.openstreetmap.org/copyright)