Upgrade to Pro — share decks privately, control downloads, hide ads and more …

MapLibre SwiftUIを使ってGPSトラッキングアプリを作ってみた

Avatar for Haruki Inoue Haruki Inoue
October 12, 2025
29

MapLibre SwiftUIを使ってGPSトラッキングアプリを作ってみた

この発表はFOSS4G 2025 Japanで発表した内容となります。
https://talks.osgeo.org/foss4g-2025-japan/talk/review/LD33TRNZWYKYCCSXKKBEERXNZ9GKXCLZ

iOSアプリを作る際にSwiftUIを使うことで、宣言的にコードベースでUIを作成しながらアプリを作ることができます。iOSアプリにおいては、MapKitが2023年にSwiftUIで対応したことにより、宣言的にマップを定義することができました。

MapLibreにおいても、MapLibre SwiftUIが登場して宣言的にMapLibreベースのマップが定義できるようになっています。
https://github.com/maplibre/swiftui-dsl

このプレゼンテーションでは、MapLibre SwiftUIを使ってGPSトラッキングアプリケーションを作ってみましたので、それを基にMapLibre SwiftUIのTipsを共有したいと思います。

Avatar for Haruki Inoue

Haruki Inoue

October 12, 2025
Tweet

More Decks by Haruki Inoue

Transcript

  1. MapLibre Native for iOS جຊతʹ͸UIKitͰͷར༻Λ૝ఆ͍ͯ͠Δ class SimpleMap: UIViewController, MLNMapViewDelegate {

    var mapView: MLNMapView! override func viewDidLoad() { super.viewDidLoad() mapView = MLNMapView(frame: view.bounds) mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.addSubview(mapView) mapView.delegate = self } // MLNMapViewDelegate method called when map has finished loading func mapView(_: MLNMapView, didFinishLoading _: MLNStyle) { } } 6
  2. MapLibre Native for iOS SwiftUIͰ͸ UIViewRepresentable Λఆٛ͢Δඞཁ͕͋Δ struct SimpleMap: UIViewRepresentable

    { func makeUIView(context _: Context) -> MLNMapView { let mapView = MLNMapView() return mapView } func updateUIView(_: MLNMapView, context _: Context) {} } struct MyApp: App { var body: some Scene { WindowGroup { SimpleMap().edgesIgnoringSafeArea(.all) } } } 7
  3. UIViewRepresentableͰ͍Ζ͍Ζͱఆٛ͢Δͷ͸େม struct MapView: UIViewRepresentable { func makeUIView(context: Context) -> some

    UIView { // MapTilerͷΩʔΛऔಘ let mapTilerKey = getMapTilerKey() // ελΠϧͷURLΛఆٛ let styleURL = URL(string: "URL") // 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) { // ϚοϓͷϩʔσΟϯά͕ऴΘͬͨͱ͖ͷॲཧ } } ... 9
  4. ͪͳΈʹMapKit͸Ͳ͏ͳ͍ͬͯΔʁ iOS17͔ΒMapKit͕SwiftUIͷAPIͱͯ͠ఏڙ։࢝ struct ContentView: View { var body: some View

    { Map { Marker("San Francisco City Hall", coordinate: cityHallLocation) .tint(.orange) Marker("San Francisco Public Library", coordinate: publicLibraryLocation) .tint(.blue) Annotation("Diller Civic Center Playground", coordinate: playgroundLocation) { ZStack { RoundedRectangle(cornerRadius: 5) .fill(Color.yellow) Text(" ") .padding(5) } } } .mapControlVisibility(.hidden) } } 10
  5. MapLibre SwiftUIͷαϯϓϧίʔυ import MapLibre import MapLibreSwiftDSL import MapLibreSwiftUI import SwiftUI

    import CoreLocation struct PolylineMapView: View { let styleURL: URL = URL(string: "URL")! let waypoints: [CLLocationCoordinate2D] var body: some View { MapView(styleURL: styleURL, camera: .constant(.center(waypoints.first!, zoom: 14))) { // ιʔεΛ௥Ճ let polylineSource = ShapeSource(identifier: "polyline") { MLNPolylineFeature(coordinates: waypoints) } // LineStyleLayerͷఆٛ LineStyleLayer(identifier: "polyline-sample", source: polylineSource) .lineCap(.round) .lineJoin(.round) .lineColor(.blue) .lineWidth(interpolatedBy: .zoomLevel, curveType: .exponential, parameters: NSExpression(forConstantValue: 1.5), stops: NSExpression(forConstantValue: [14: 6, 18: 24])) } } } struct ContentView: View { var body: some View { PolylineMapView(waypoints: [ CLLocationCoordinate2D(latitude: 35.681236, longitude: 139.767125), // ౦ژӺ CLLocationCoordinate2D(latitude: 35.689634, longitude: 139.692101), // ৽॓Ӻ ]) .ignoresSafeArea(.all) } } 16
  6. Ґஔ৘ใΛऔಘ͢Δ import CoreLocation @MainActor class LocationsHandler: ObservableObject { static let

    shared = LocationsHandler() let manager: CLLocationManager var currentLocation: CLLocation? private init () { self.manager = CLLocationManager() // Ґஔ৘ใͷઃఆ self.manager.activityType = .other // ΞΫςΟϏςΟλΠϓ self.manager.desiredAccuracy = kCLLocationAccuracyKilometer // ڐ༰੍౓ self.manager.distanceFilter = kCLDistanceFilterNone // ڑ཭ϑΟϧλ } ... } 22
  7. Ґஔ৘ใΛऔಘ͢Δ import CoreLocation @MainActor class LocationsHandler: ObservableObject { // Ґஔ৘ใͷऔಘ։࢝

    // Ҿ਺ͰҐஔ৘ใ͕ߋ৽͞Εͨͱ͖ͷॲཧΛΫϩʔδϟͰड͚औΔ func startLocationUpdates(action: @escaping (CLLocation) -> Void) async { // Ґஔ৘ใͷݖݶ͕ະ֬ೝͷ৔߹͸ڐՄΛϦΫΤετ͢Δ if self.manager.authorizationStatus == .notDetermined { self.manager.requestAlwaysAuthorization() } do { let updates = CLLocationUpdate.liveUpdates() // Ґஔ৘ใ͸ඇಉظΠςϨʔγϣϯͰड͚औΔ for try await update in updates { if let location = update.location { currentLocation = location action(location) } } } catch { print("Could not start location updates") } } } 23
  8. Ґஔ৘ใΛऔಘ͢Δ @MainActor class MainViewModel: ObservableObject { ... @Published var tracks:

    [[CLLocation]] = [] @Published var isTracking = false private let locationHandler = LocationsHandler.shared // MapͷView্ཱ͕͕ͪͬͨͱ͖ʹҐஔ৘ใ͸Քಇ։࢝ func startLocationUpdates() async { // ઌఔఆٛͨ͠LocationHanderͷstartLocationUpdatesΛݺͼग़͢ await locationHandler.startLocationUpdates { location in self.locationUpdatedAction(location: location) } } /// Ϣʔβʔ͕τϥοΩϯά։࢝ϘλϯΛԡͨ͠ͱ͖ͷॲཧ /// Button { /// if self.vm.isTracking { /// self.vm.stopTrack() /// } else { /// self.vm.startTrack() /// } /// } label {...} func startTrack() { ... tracks.append([]) self.isTracking = true } ... } 24
  9. ي੻৘ใΛ௥Ճ Ґஔ৘ใ͸ৗʹऔಘ͢ΔΑ͏ʹ͍ͯ͠ΔͨΊτϥοΩϯάதͷΈ഑ྻʹ௥Ճ @MainActor class MainViewModel: ObservableObject { ... func locationUpdatedAction(location:

    CLLocation) { ... self.currentLocation = location // τϥοΩϯάதͰ͋Ε͹ tracks ͷ഑ྻʹ௥Ճ if isTracking && tracks.count > 0 { tracks[tracks.count - 1].append(location) } } ... } 25
  10. ي੻ΛMapLibreͰදࣔ import MapLibre import MapLibreSwiftDSL import MapLibreSwiftUI import SwiftUI struct

    MainView: View { @StateObject var vm = MainViewModel() let styleURL: URL = URL(string: "URL")! var body: some View { MapView(styleURL: styleURL, camera: self.$vm.camera) { // औಘͨ͠τϥοΫσʔλΛShapeSourceͱͯ͠௥Ճ let trackSource = ShapeSource(identifier: "track-source") { let polylines = self.vm.tracks.compactMap({ track -> MLNPolylineFeature? in if track.isEmpty { return nil } return MLNPolylineFeature(coordinates: track.map(\.coordinate), count: UInt(track.count)) }) // ෳ਺ͷPolylineFeatureΛؚΊΔͨΊʹMultiPolylineFeatureΛ࢖༻ // ʢτϥοΫσʔλΛҰ࣌ఀࢭ͢ΔՄೳੑ͕͋Γɺ2࣍ݩ഑ྻͰ؅ཧ͍ͯ͠ΔͨΊʣ MLNMultiPolylineFeature(polylines: polylines) } ... } } 26
  11. ελΠϧϨΠϠʔΛఆٛ struct MainView: View { @StateObject var vm = MainViewModel()

    let styleURL: URL = URL(string: "URL")! var body: some View { MapView(styleURL: styleURL, camera: self.$vm.camera) { ... // ϥΠϯͷελΠϧϨΠϠʔΛఆٛ LineStyleLayer(identifier: "track-line", source: trackSource) .lineCap(.round) .lineJoin(.round) .lineColor(.red) .lineWidth( interpolatedBy: .zoomLevel, curveType: .exponential, parameters: NSExpression(forConstantValue: 2.0), stops: NSExpression(forConstantValue: [ 14: 2, 18: 4 ]) ) } } 27