Customize Mapbox style with layers in SwiftUI - swiftui

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 ?

Related

SwiftUI and UIKit Interoperability with displaying multiple views

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
}

Unexpected acting of MapKit in SwiftUI

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
}
}
}

MFMessageComposeViewController + SwiftUI Buggy Behavior

I'm using ViewControllerRepresentable to present a MFMessageComposeViewController so users can send texts from my app.
However, every time the view is presented, it's very buggy - elements randomly disappear, scrolling is off, and the screen flickers. Tested on iOS 14.2 and 14.3.
Here's the code:
import SwiftUI
import MessageUI
struct MessageView: UIViewControllerRepresentable {
var recipient: String
class Coordinator: NSObject, MFMessageComposeViewControllerDelegate {
var completion: () -> Void
init(completion: #escaping ()->Void) {
self.completion = completion
}
// delegate method
func messageComposeViewController(_ controller: MFMessageComposeViewController,
didFinishWith result: MessageComposeResult) {
controller.dismiss(animated: true, completion: nil)
completion()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator() {} // not using completion handler
}
func makeUIViewController(context: Context) -> MFMessageComposeViewController {
let vc = MFMessageComposeViewController()
vc.recipients = [recipient]
vc.messageComposeDelegate = context.coordinator
return vc
}
func updateUIViewController(_ uiViewController: MFMessageComposeViewController, context: Context) {}
typealias UIViewControllerType = MFMessageComposeViewController
}
and my view
struct ContentView: View {
#State private var isShowingMessages = false
#State var result: Result<MFMailComposeResult, Error>? = nil
var body: some View {
VStack {
Button("Show Messages") {
self.isShowingMessages = true
}
.sheet(isPresented: self.$isShowingMessages) {
MessageView(recipient: "+15555555555")
}
.edgesIgnoringSafeArea(.bottom)
}
}
}
Is there something wrong with the way I'm presenting this view? Has anyone else experienced this behavior? Similar behavior happens with MFMailComposeViewController, but it's not as buggy.
5 minutes later, I realized I needed to add this when presenting the sheet:
MessageView(recipient: "+15555555555")
.ignoresSafeArea()
The view looked buggy because it was trying to account for the keyboard safe area and had a hard time doing it.

SwiftUI + MapBox integration messes up navbar

I'm implementing some MapBox functionality in SwiftUi and when using the default NavigationBar everything works as expected:
var body: some View {
NavigationView {
MapView(annotations: $annotations)
.centerCoordinate(.init(latitude: 37.791293, longitude: -122.396324))
.navigationBarTitle("Hello")
}
}
shows:
But when trying to have an .inline style for the navBar, the view behaves weird:
var body: some View {
NavigationView {
MapView(annotations: $annotations)
.centerCoordinate(.init(latitude: 37.791293, longitude: -122.396324))
.navigationBarTitle("Hello", displayMode: .inline)
//.navigationBarColor(.parqGreen)
}
}
And when adding color:
Any idea why this happens ? Is it something in the MapBox framework ?
Update: Added MapView:
struct MapView: UIViewRepresentable {
#Binding var annotations: [MGLPointAnnotation]
private let mapView: MGLMapView = MGLMapView(frame: .zero, styleURL: MGLStyle.streetsStyleURL)
func makeUIView(context: UIViewRepresentableContext<MapView>) -> MGLMapView {
mapView.logoView.isHidden = true
mapView.attributionButton.isHidden = true
mapView.zoomLevel = 13
if let styleURL = URL(string: "mapbox://styles/morreke/cjkz2y4bq0kb12smmigszo70w") {
mapView.styleURL = styleURL
}
return mapView
}
func updateUIView(_ uiView: MGLMapView, context: UIViewRepresentableContext<MapView>) {
updateAnnotations()
}
private func updateAnnotations() {
if let currentAnnotations = mapView.annotations {
mapView.removeAnnotations(currentAnnotations)
}
mapView.addAnnotations(annotations)
}
func centerCoordinate(_ centerCoordinate: CLLocationCoordinate2D) -> MapView {
mapView.centerCoordinate = centerCoordinate
return self
}
}
Update 2: Even the simplest implementation has the same result
struct MapView: UIViewRepresentable {
private let mapView: MGLMapView = MGLMapView()
func makeUIView(context: UIViewRepresentableContext<MapView>) -> MGLMapView {
return mapView
}
func updateUIView(_ uiView: MGLMapView, context: UIViewRepresentableContext<MapView>) {
}
}
Updating Xcode from 11.3.1 to 11.4.1 did the trick.

Implement PencilKit undo functionality using SwiftUI

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
}
}
}