Share
Embed
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を共有したいと思います。
Haruki Inoue
October 12, 2025
Transcript
32 slides
-
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 -
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 -
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 ) // DelegateCoordinatorΛࢦఆ͠·͢ 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 -
ͪͳΈʹ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 -
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 -
ҐஔใΛऔಘ͢Δ 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 -
ҐஔใΛऔಘ͢Δ 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 -
ҐஔใΛऔಘ͢Δ @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 -
يใΛՃ Ґஔใৗʹऔಘ͢ΔΑ͏ʹ͍ͯ͠ΔͨΊτϥοΩϯάதͷΈྻʹՃ @MainActor class MainViewModel: ObservableObject { ... func locationUpdatedAction(location:
CLLocation) { ... self.currentLocation = location // τϥοΩϯάதͰ͋Ε tracks ͷྻʹՃ if isTracking && tracks.count > 0 { tracks[tracks.count - 1].append(location) } } ... } 25 -
يΛ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 -
ελΠϧϨΠϠʔΛఆٛ 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