MapLibre GLがiOSでも実装できると聞きましたので,今回はMapLibre GL Native for iOSを簡単に触ってみました.
参考サイト
MapTilerさんが MapLibre GL Native for iOS の チュートリアル を書いています.これを参考にしながら触ってみます.
環境
- Xcode 14.2
- Swift 5.7
今回はSwiftUIで実装していきます.
事前準備
Xcodeのプロジェクトの作成は省略します.
MapLibre GL Native for iOSを入れるために,パッケージをインストールします.
画面上部の「File」=>「Add Packages...」を選択します.
 
右上の検索バーにMapLibre GLのGithub URL ( https://github.com/maplibre/maplibre-gl-native-distribution ) を入力します.その後,「maplibre-gl-native-distribution」を選択した状態で 「Add Package」 を押します.
 
途中でmapboxのパッケージをインストールする画面が出てきます.これも「Add Package」を押します.
 
左のサイドバーの「Package Dependencies」に「MapLibew GL Native」が表示されていれば,インストール完了です.
 
また,TARGETS => General => Frameworks, Libraries, and Embedded Content の中に「Mapbox」があるか確認してください.
 
 
マップ表示の実装
マップを表示する部分を実装していきます.今回はSwiftUIで実装します.表示に関わるフォルダ構成は下記のとおりです(少し省略しています).
MGLMapView は,SwiftUIには対応しておらず,UIKitで定義する必要があります.SwiftUIに適用するために, MapLibreView.swift に MGLMapView の定義をしていきます.
 
MapLibreView.swift
UIKitでのみ対応しているライブラリをSwiftUIに適用するためには, UIViewRepresentable を継承してViewを定義します.まずは,全体のソースコードです.
API Keyは適切に管理してください.
.env ファイルを用いて管理する方法や, .plist ファイルを用いて管理する方法などが挙げられます.
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 の中に定義します.
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で作成したプロジェクト名が,そのまま反映されます.
import SwiftUI
@main
struct MapLibreNativeSampleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
動作確認
この状態でエミュレータ or 実機で動作確認をすると,下記のように東京駅を中心としたMapTilerのベクトルマップが表示されます.
 
指定したポイントに画像を表示する
次に,指定した場所にポイントを示す画像を表示してみたいと思います.
画像の定義
画像を用意するにあたり,適当に作成した画像を,Xcode内の Assets に定義していきます.今回は PointIcon という名前で定義します.ちなみに,画像のサイズは 64px x 64px です.
 
本当はRetinaディスプレイ用に2x,3xも定義する必要がありますが,今回は1xのみ定義しています.
また,右のRender Asは Default に設定しておきます(赤枠の部分). Template image に設定すると,マップ上に表示される画像の色定義ができなくなります.
MapLibreView.swiftを編集する
Assetsの定義が完了したら, 地図上に画像アイコンを表示する処理を MapLibreView.swift の createMarker 関数で定義していきます.
この関数の中では,主に画像を表示する場所の定義( shapeSource ) と,画像を表示するレイヤーの定義( shapeLayer ) をしています.マップ上に表示する画像の定義は MGLStyle の setImage で定義します.
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 を呼び出しています.
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 て定義した画像が表示されます.
 
最後に
今回は,地図の表示と特定のポイントに画像を表示する処理を実装してみました.ライブラリがUIKitベースであるため,SwiftUIに適用させるためには工夫が必要ですが,そこまで難しくない実装で地図を表示することができました.
他にも,マップ上にLineStringを引いたり,Polygonを描画することもできます.GeoJSONを読み込んで表示する実装もできるそうなので,試してみたいと思います.
なお,私の環境ではMapLibre GLのライブラリをインストールしているのにもかかわらず,ソースコード内でMapLibreに付随しているMapboxライブラリが読み込まれない場合がありました.ただ,もう一度インストールをやり直すと,ライブラリの読み込みができました.
動作確認画面で表示している地図の著作権: © MapTiler © OpenStreetMap contributors
