[SwiftUI] Mapbox の地図を Maps SDK for iOS を用いて実装する – 実装編

前回の記事 で環境準備が整いましたので、早速 Mapbox の地図 を SwiftUI で実装していきたいと思います。 Maps SDK for iOS を用いて実装してきますので、 Maps SDK for iOS の インストール ができていない場合は 前回の記事 を参考に インストール した後に本記事を参照ください。 Mapbox 公式 サイト が提供している サンプルコード は そのままでは SwiftUI に組み入れることができないため UIViewRepresentable を活用して実装していますので、 UIViewRepresentable についても簡単に解説しながら進めていきます。

Xcode: 13.1
iOS: 15.0
Swift: 5
Maps SDK for iOS: v10.0.1

公式 サイト で紹介されている Maps SDK for iOS サンプル コード

Mapbox の公式サイト には 簡単な サンプル が紹介されているのですが、 残念ながら SwiftUI を用いた そのものズバリのものは紹介されておらず、 UIViewController を用いたもののみが紹介されています。 UIViewController は iOS 2.0 以降で実装可能な “枯れた” 技術ではありますが、 SwiftUI で実装するためには UIViewRepresentable を用いてひと手間加える必要があります。

UIViewRepresentable について

Apple Developer サイト によると UIViewRepresentable は以下のように紹介されています。

A wrapper for a UIKit view that you use to integrate that view into your SwiftUI view hierarchy.

https://developer.apple.com/documentation/swiftui/uiviewrepresentable

つまり、iOS 2.0 以降 サポート されている “枯れた” UIKit を “新しい” SwiftUI の構造の中に組み込むための「緩衝材 」といった用途として準備されています。

UIViewController も以下の通り UIKit に含まれていますので、 UIViewController を SwiftUI に組み入れるためには UIViewRepresentable を活用する必要があることになります。

UIViewRepresentable の最も シンプル な サンプル

UIViewController を SwiftUI で用いるためには UIViewRepresentable を活用することは分かったのですが、 ではどのように利用すればよいのでしょうか? Apple Developer サイト には SwiftUI から UIViewRepresentable を呼び出す サンプル は紹介されていますが、 肝心の UIViewRepresentable をどのように活用すればよいかについてはあまり情報がありません。 Apple Developer サイト の下の方にある Topics を注意深く見ると、3 つの アクション が紹介されていることを見つけることができます。

  1. Creating and Updating the View
  2. Cleaning Up the View
  3. Providing a Custom Coordinator Object

今回はただ単純に View を作成するだけでよいため、 “1. Creating and Updating the View” の内容のみを参照していきます。

UIViewRepresentable: Creating and Updating the View について

 ”Creating and Updating the View” にはいくつかの要素が紹介されていますが、 以下の項目が Required (必須) として定義されています。 この 3 つの要素を備えた struct を作成することで UIViewRepresentable を活用できることがわかります。

TypeNameDescription
funcmakeUIViewView の作成処理
funcupdateUIViewView の更新処理
associatedtypeUIViewType: UIViewWrap する View の タイプ
Required Elements for the “Creating and Updating the View”

それでは、これらの内容を踏まえつつ、 Maps SDK for iOS の ドキュメント に紹介されている サンプルコード を活用する形で UIViewRepresentable を用いて SwiftUI で Mapbox の地図を View として実装していきます。


SwiftUI で Mapbox 地図 View の実装

今回は MapView.swift という ファイル を作成して Mapbox 地図の View を実装し、 ContentView から呼び出していこうと思います。

SwiftUI と MapboxMaps の Import

MapView.swift に 必要な ライブラリ を インポート していきます。Maps SDK for iOS のドキュメント では SwiftUI ではなく UIKit を インポート していますが、 今回は SwiftUI として実装してくため UIKit の代わりに SwiftUI を インポートしていきます。

import SwiftUI
import MapboxMaps

ViewController の実装

続いて、 地図 の表示部分である ViewController を実装してきます。 ここは Maps SDK for iOS の ドキュメント に紹介されている サンプルコード をそのまま流用していきます。

class ViewController: UIViewController {
 
    internal var mapView: MapView!

    override public func viewDidLoad() {
        super.viewDidLoad()

        let myResourceOptions = ResourceOptions(accessToken: "your_public_access_token")
        let myMapInitOptions = MapInitOptions(resourceOptions: myResourceOptions)
        mapView = MapView(frame: view.bounds, mapInitOptions: myMapInitOptions)
        mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

        self.view.addSubview(mapView)
    }
}

 accessToken の “your_public_access_token” には Mapbox アカウント に ログインし、画面下部に表示されている  Public Token 値 を入力していきます。Default public token でも問題ありませんが、 今回は 環境準備編 で作成した 最小限である 2 つの Public Scopes を持つ simple public token の値を指定していきます。

UIViewControllerRepresentable で ViewController を SwiftUI に組み込み可能に

前述したとおり UIViewController 型 で定義した ViewController を SwiftUI で用いるためには UIViewControllerRepresentable で ラップ する必要がありますので その部分を実装してきます。

今回は 極力 シンプル にするために 以下のように実装しています。

TypeName実装内容
funcmakeUIViewUIViewController 型 の ViewController を返すだけ
funcupdateUIView何もしない
associatedtypeUIViewTypeViewController を指定

実装する UIViewControllerRepresentable は以下のようになります。

struct MapViewWrapper : UIViewControllerRepresentable {
    
    func makeUIViewController(context: Context) -> ViewController {
        return ViewController()
    }
    
    func updateUIViewController(_ uiViewController: ViewController, context: Context) {
        
    }
}

ContentView からの呼び出し部分の実装

最後に ContentView からの呼び出し部分を実装してきます。 といっても、 UIViewControllerRepresentable のおかげで 今回定義した MapViewWrapper を SwiftUI の View のように呼び出すだけになります。

//  ContentView.swift

import SwiftUI

struct ContentView: View {
    var body: some View {
        MapViewWrapper()
    }
}

ビルド & 実行

それでは、これまでの内容を ビルド し 実行してみます。今回は 緯度経度 を特に指定していないため、 緯度0度, 経度0度 の地点 ( 通称 Null Island )を中心とした地図が表示されています。 

なお、 地図を表示すると アカウント に紐付いた 使用量 が以下のように加算されていく様が Mapbox アカウント ログインページ の 下部 に表示されてきます。個人で使用する分には 25,000 回 というのは十分過ぎる 無料枠 ですが Token が漏洩した場合にはその限りではありませんので Token の取り扱いには十分留意するようにしてください。


まとめ

  • Mapbox 地図表示用の サンプル はUIViewController として提供されているため、そのままでは SwiftUI に組み込むことができない
  • UIViewController を含む UIKit を SwiftUI に組み込むためには UIViewControllerRepresentable を活用することで SwiftUI に組み込み可能
  • パラメータ 指定なしで地図を描画すると 緯度経度 ゼロ地点 が表示される

関連記事

参照情報

Wrapping a UIViewController in a SwiftUI view – a free Hacking with iOS: SwiftUI Edition tutorial

今回使用した サンプルコード

ContentView.swift

//  ContentView.swift

import SwiftUI

struct ContentView: View {
    var body: some View {
        MapViewWrapper()
    }
}

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

MapView.swift

//  MapView.swift

import SwiftUI
import MapboxMaps

struct MapViewWrapper : UIViewControllerRepresentable {
    
    func makeUIViewController(context: Context) -> ViewController {
        return ViewController()
    }
    
    func updateUIViewController(_ uiViewController: ViewController, context: Context) {
        
    }
}


class ViewController: UIViewController {
 
    internal var mapView: MapView!

    override public func viewDidLoad() {
        super.viewDidLoad()

        let myResourceOptions = ResourceOptions(accessToken: "pk.eyJ1IjoiaGFydWJlYXJzIiwiYSI6ImNrdzFuNDh6NzAydGoybm1wZGdudm5ybTkifQ.mnv9PQFHVGZz0kDWZ2mcRg")
        let myMapInitOptions = MapInitOptions(resourceOptions: myResourceOptions)
        mapView = MapView(frame: view.bounds, mapInitOptions: myMapInitOptions)
        mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

        self.view.addSubview(mapView)
    }
}