I'm trying to implement a solution with GoogleMapsApi with a map where user can touch the map and do things. For that, I understand delegate must be implemented, but I can't figure out how to achieve that with SwifUI. There's a lot of code samples on web, when in Swift, or even Objective C, but I couldn't find any on SwifUI.
Here's what I did (I'm trying to keep this code as simple as it could be):
struct GoogleMapsHomeView: UIViewRepresentable {
func makeUIView(context: Self.Context) -> GMSMapView {
let mapView = GMSMapView.map()
return mapView
}
func updateUIView(_ mapView: GMSMapView, context: Context) {
}
}
struct HomeView: View {
var body: some View {
GoogleMapsHomeView()
}
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
How to declare GMSMapViewDelegate and related listener for a user map moving detection?
The common pattern is to use coordinator as delegate
struct GoogleMapsHomeView: UIViewRepresentable {
func makeUIView(context: Self.Context) -> GMSMapView {
let mapView = GMSMapView.map()
mapView.delegate = context.coordinator
return mapView
}
func makeCoordinator() -> Coordinator {
Coordinator(owner: self)
}
func updateUIView(_ mapView: GMSMapView, context: Context) {
}
class Coordinator: NSObject, GMSMapViewDelegate {
let owner: GoogleMapsHomeView // access to owner view members,
init(owner: GoogleMapsHomeView) {
self.owner = owner
}
// ... delegate methods here
}
}
Related
Overview: I'm using SwiftUI, but wanted to use UIKit-MapKit. I used UIViewRepresentable to be able to wrap the UIKit feature.
Problem: I'm learning about swiftui-uikit-interoperability and I'm getting stuck on being able to display multiple SwiftUI views.
Code Snippet:
ContentView
struct ContentView: View {
#ObservedObject var viewModel: MapView.PinViewModel
init() {
self.viewModel = MapView.PinViewModel()
}
var body: some View {
NavigationView {
MapView()
.sheet(isPresented: $viewModel.showPinForm) {
PinForm()
}
.navigationTitle("SwiftUI UIKit Interop").scaledToFill()
}
}
}
MapView
struct MapView: UIViewRepresentable {
class PinViewModel: ObservableObject {
#Published var showPinForm: Bool
init() {
self.showPinForm = false
}
func updateShowPinVar() {
self.showPinForm = true
}
}
func showPinForm() {
pinViewModel.updateShowPinVar()
}
func makeCoordinator() -> MapViewCoordinator {
let coordinator = MapViewCoordinator()
coordinator.delegate = self
return coordinator
}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
let coordinate = CLLocationCoordinate2D(latitude: 40.7209, longitude: -74.0007)
let span = MKCoordinateSpan(latitudeDelta: 0.03, longitudeDelta: 0.03)
let mapRegion = MKCoordinateRegion(center: coordinate, span: span)
mapView.setRegion(mapRegion, animated: true)
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) {
}
}
In this I have a #Published var showPinForm that gets toggled in MapView. ContentView is supposed to watch this variable and when it is true it will cause the sheet to pull up. However, I believe when I enter MapView() from ContentView() then I no longer recognize ContentView.
Using the UIViewRepresentable, what is the best way to display another swiftui view? Does not have to use .sheet (Although, it would be nice)
I have tried to simplify the code to show the main problem, so I left out a lot of additional info and took out basic patterns that I used (MVVM)
Please let me know if you need any clarifications
try to follow this pattern, you can toggle the flag both inside and outside your MapView
struct MapView: UIViewRepresentable {
#Binding var switcher: Bool // -> use binding
func makeUIView(context: Context) -> MKMapView { MKMapView() }
func updateUIView(_ uiView: MKMapView, context: Context) { }
}
struct MainView: View {
#ObservedObject var viewModel = MainViewModel()
var body: some View {
MapView(switcher: $viewModel.flag)
.sheet(isPresented: $viewModel.flag) {
Text("Pin pin")
}
}
}
class MainViewModel: ObservableObject {
#Published var flag: Bool = false
}
I'm stuck with the really strange problem. I'm implementing map into my SwiftUI app. It should act like a normal map (drag, scroll and so on). When changing position (that is binding point) the app gets an address via geocoder.
Also user can click "Change" button and enter address manually (with autocompletion). After selecting the address, the map should move to the reverse geocoded point.
Built-in SwiftUI Map() is a good thing, but... it's unreal to make it show buildings. And in the app it's something that matters. So, going with UIViewRepresentable gives me another strange problem.
If I set the center coordinate in UpdateUIView, the map stops any interactivity. Otherwise changing the address manually doesn't work.
What could be wrong with this?
struct MapView: UIViewRepresentable {
#Binding var point: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.showsBuildings = true
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) {
// uiView.setCenter(point, animated: true)
}
func makeCoordinator() -> MapView.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
self.parent.point = mapView.centerCoordinate
}
}
}
I tried wrapping everything into DispatchQueue.main.async {} - not working (and honestly I don't think it could)
I also tried this solution, but it worked neither: https://www.reddit.com/r/SwiftUI/comments/kti9r9/uiviewrepresentable_how_to_update_bindings/
I also had the same problem. I solved this using #state. So every time the mapView changes, the corresponding function of the coordinator is definitely called. Hope it helps.
struct YourView: View {
#State mapView: MKMapView = .init()
#State var point: CLLocationCoordinate2D = [...]
var body: some View {
MapView(mapView: $mapView, point: $point)
...
}
struct MapView: UIViewRepresentable {
#Binding var mapView: MKMapView
#Binding var point: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
self.mapView = MKMapView()
self.mapView.showsBuildings = true
self.mapView.delegate = context.coordinator
return self.mapView
}
...
func updateUIView(_ uiView: MKMapView, context: Context) {
// uiView.setCenter(point, animated: true)
}
func makeCoordinator() -> MapView.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
self.parent.point = self.mapView.centerCoordinate
}
}
}
I've wrapped a MKMapView in a ViewRepresentable
public struct MapView: UIViewRepresentable {
I want to create an action callback that works like this:
MapView().onAnnotationTapped { annotation in
}
In MapView i have defined
#inlinable public func onAnnotationTapped(site: (AnnotationView) -> ()) -> some View {
return self
}
But how to provide the AnnotationView from the coordinator?
public class MapViewCoordinator: NSObject, MKMapViewDelegate {
var mapView: MapView
init(_ control: MapView) {
self.mapView = control
}
public func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
/// How to send from here to the MapView function?
}
}
Here is a solution - introduce callback property for view and inject it in your inalinable modifier. Prepared with Xcode 12.1 / iOS 14.1
struct MapView: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
let view = MKMapView()
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: MKMapView, context: Context) {
}
func makeCoordinator() -> MapViewCoordinator {
MapViewCoordinator(self)
}
var tapCallback: ((MKAnnotationView) -> ())? // << this one !!
#inlinable public func onAnnotationTapped(site: #escaping (MKAnnotationView) -> ()) -> some View {
var newMapView = self
newMapView.tapCallback = site // << here !!
return newMapView
}
public class MapViewCoordinator: NSObject, MKMapViewDelegate {
var mapView: MapView
init(_ control: MapView) {
self.mapView = control
}
public func mapView(_ mkMap: MKMapView, didSelect view: MKAnnotationView) {
self.mapView.tapCallback?(view) // << call !!
}
}
}
I have an issue to customize a Mapbox view's style, like for instance adding some information on the map if a switch is switched on. Not sure if it's important, but the layers I need to add are MGLSymbolStyleLayer and MGLLineStyleLayer.
Let's start with code for the main view containing a switch representing a state used to customize the map's style, and an UIViewRepresentable for the Mapbox view.
struct Test_MapBox: View {
#State private var styleURL: URL = MGLStyle.outdoorsStyleURL
#State private var switchButton: Bool = false
var body: some View {
ZStack(alignment: .bottom) {
MapView(switchButton: switchButton)
.styleURL(styleURL)
.edgesIgnoringSafeArea(.all)
Toggle(isOn: $switchButton, label: {
Text("Switch")
})
}
}
}
struct MapView: UIViewRepresentable {
var switchButton: Bool
var mapView = MGLMapView(frame: .zero)
func makeUIView(context: UIViewRepresentableContext<MapView>) -> MGLMapView {
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ uiView: MGLMapView, context: UIViewRepresentableContext<MapView>) {
print("Style: \(uiView.style)")
print("Update view, switch: \(switchButton)")
}
func makeCoordinator() -> MapView.Coordinator {
Coordinator(self)
}
func styleURL(_ styleURL: URL) -> MapView {
mapView.styleURL = styleURL
return self
}
final class Coordinator: NSObject, MGLMapViewDelegate {
var parent: MapView
init(_ control: MapView) {
self.parent = control
}
func mapViewDidFinishLoadingMap(_ mapView: MGLMapView) {
print("Map loaded, switch: \(parent.switchButton)")
}
func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) {
print("Style loaded, switch: \(parent.switchButton)")
}
}
}
The issue is the following: inside the delegate functions, switchButton is never up to date, always false (and I don't understand why)... And in updateUIView(), switchButton is OK, but the style is not yet loaded, so usually you get a nil when accessing it ...
Have you got a solution ?
Edit: Thanks to some of the feedback, I have been able to get this partially working (updated code to reflect current changes).
Even though the app appears to be working as intended, I am still getting the 'Modifying state...' warning. How can I update the view's drawing in updateUIView and push new drawings onto the stack with the canvasViewDrawingDidChange without causing this issue? I have tried wrapping it in a dispatch call, but that just creates an infinite loop.
I'm trying to implement undo functionality in a UIViewRepresentable (PKCanvasView). I have a parent SwiftUI view called WriterView which holds two buttons and the canvas.
Here's the parent view:
struct WriterView: View {
#State var drawings: [PKDrawing] = [PKDrawing()]
var body: some View {
VStack(spacing: 10) {
Button("Clear") {
self.drawings = []
}
Button("Undo") {
if !self.drawings.isEmpty {
self.drawings.removeLast()
}
}
MyCanvas(drawings: $drawings)
}
}
}
Here is how I've implemented my UIViewRepresentable:
struct MyCanvas: UIViewRepresentable {
#Binding var drawings: [PKDrawing]
func makeUIView(context: Context) -> PKCanvasView {
let canvas = PKCanvasView()
canvas.delegate = context.coordinator
return canvas
}
func updateUIView(_ uiView: PKCanvasView, context: Context) {
uiView.drawing = self.drawings.last ?? PKDrawing()
}
func makeCoordinator() -> Coordinator {
Coordinator(self._drawings)
}
class Coordinator: NSObject, PKCanvasViewDelegate {
#Binding drawings: [PKDrawing]
init(_ drawings: Binding<[PKDrawing]>) {
self._drawings = drawings
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
drawings.append(canvasView.drawing)
}
}
}
I am getting the following error:
[SwiftUI] Modifying state during view update, this will cause undefined behavior.
Presumably it is being caused by my coordinator's did change function, but I'm not sure how to fix this. What is the best way to approach this?
Thanks!
I finally (accidentally) figured out how to do this using UndoManager. I'm still not sure exactly why this works, because I never have to call self.undoManager?.registerUndo(). Please comment if you understand why I never have to register an event.
Here's my working parent view:
struct Writer: View {
#Environment(\.undoManager) private var undoManager
#State private var canvasView = PKCanvasView()
var body: some View {
VStack(spacing: 10) {
Button("Clear") {
canvasView.drawing = PKDrawing()
}
Button("Undo") {
undoManager?.undo()
}
Button("Redo") {
undoManager?.redo()
}
MyCanvas(canvasView: $canvasView)
}
}
}
Here's my working child view:
struct MyCanvas: UIViewRepresentable {
#Binding var canvasView: PKCanvasView
func makeUIView(context: Context) -> PKCanvasView {
canvasView.drawingPolicy = .anyInput
canvasView.tool = PKInkingTool(.pen, color: .black, width: 15)
return canvasView
}
func updateUIView(_ canvasView: PKCanvasView, context: Context) { }
}
This certainly feels more like the intended approach for SwiftUI and is certainly more elegant than the attempts I made earlier.
just for completeness and if you want to show the PKToolPicker, here is my UIViewRepresentable.
import Foundation
import SwiftUI
import PencilKit
struct PKCanvasSwiftUIView : UIViewRepresentable {
let canvasView = PKCanvasView()
#if !targetEnvironment(macCatalyst)
let coordinator = Coordinator()
class Coordinator: NSObject, PKToolPickerObserver {
// initial values
var color = UIColor.black
var thickness = CGFloat(30)
func toolPickerSelectedToolDidChange(_ toolPicker: PKToolPicker) {
if toolPicker.selectedTool is PKInkingTool {
let tool = toolPicker.selectedTool as! PKInkingTool
self.color = tool.color
self.thickness = tool.width
}
}
func toolPickerVisibilityDidChange(_ toolPicker: PKToolPicker) {
if toolPicker.selectedTool is PKInkingTool {
let tool = toolPicker.selectedTool as! PKInkingTool
self.color = tool.color
self.thickness = tool.width
}
}
}
func makeCoordinator() -> PKCanvasSwiftUIView.Coordinator {
return Coordinator()
}
#endif
func makeUIView(context: Context) -> PKCanvasView {
canvasView.isOpaque = false
canvasView.backgroundColor = UIColor.clear
canvasView.becomeFirstResponder()
#if !targetEnvironment(macCatalyst)
if let window = UIApplication.shared.windows.filter({$0.isKeyWindow}).first,
let toolPicker = PKToolPicker.shared(for: window) {
toolPicker.addObserver(canvasView)
toolPicker.addObserver(coordinator)
toolPicker.setVisible(true, forFirstResponder: canvasView)
}
#endif
return canvasView
}
func updateUIView(_ uiView: PKCanvasView, context: Context) {
}
}
I think the error probably comes from the private func clearCanvas()
and private func undoDrawing(). Try this to see if it works:
private func clearCanvas() {
DispatchQueue.main.async {
self.drawings = [PKDrawing()]
}
}
Similarly for undoDrawing().
If it is from canvasViewDrawingDidChange, do same trick.
I have something working with this:
struct MyCanvas: UIViewRepresentable {
#Binding var drawings: [PKDrawing]
func makeUIView(context: Context) -> PKCanvasView {
let canvas = PKCanvasView()
canvas.delegate = context.coordinator
return canvas
}
func updateUIView(_ canvas: PKCanvasView, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self._drawings)
}
class Coordinator: NSObject, PKCanvasViewDelegate {
#Binding var drawings: [PKDrawing]
init(_ drawings: Binding<[PKDrawing]>) {
self._drawings = drawings
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
self.drawings.append(canvasView.drawing)
}
}
}
I think I have something working without the warning using a different approach.
struct ContentView: View {
let pkCntrl = PKCanvasController()
var body: some View {
VStack(spacing: 10) {
Button("Clear") {
self.pkCntrl.clear()
}
Spacer()
Button("Undo") {
self.pkCntrl.undoDrawing()
}
Spacer()
MyCanvas(cntrl: pkCntrl)
}
}
}
struct MyCanvas: UIViewRepresentable {
var cntrl: PKCanvasController
func makeUIView(context: Context) -> PKCanvasView {
cntrl.canvas = PKCanvasView()
cntrl.canvas.delegate = context.coordinator
cntrl.canvas.becomeFirstResponder()
return cntrl.canvas
}
func updateUIView(_ uiView: PKCanvasView, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, PKCanvasViewDelegate {
var parent: MyCanvas
init(_ uiView: MyCanvas) {
self.parent = uiView
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
if !self.parent.cntrl.didRemove {
self.parent.cntrl.drawings.append(canvasView.drawing)
}
}
}
}
class PKCanvasController {
var canvas = PKCanvasView()
var drawings = [PKDrawing]()
var didRemove = false
func clear() {
canvas.drawing = PKDrawing()
drawings = [PKDrawing]()
}
func undoDrawing() {
if !drawings.isEmpty {
didRemove = true
drawings.removeLast()
canvas.drawing = drawings.last ?? PKDrawing()
didRemove = false
}
}
}