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

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

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for Haruki Inoue Haruki Inoue
October 12, 2025
51

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