SwiftUI + MapBox integration messes up navbar - swiftui

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.

Related

Dismiss UIViewRepresentable using onChange method is Not working

I have a UIViewRepresentable and need to dismiss when some value has changed.I used .onChange method and it is not working. But onChange method called successfully.
Main View
class ViewModel:ObservableObject {
#Published var urlHasChanged:Bool = false
#Published var isShowWebView:Bool = false
}
struct MainView : View {
#ObservedObject var viewModel = ViewModel()
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack {
Button {
viewModel.isShowWebView = true
} label: {
Text("show web view")
}
.background(NavigationLink( destination:
WebView(viewModel: viewModel)
.onChange(of: viewModel.urlHasChanged, perform: { newValue in
print("called")
self.presentationMode.wrappedValue.dismiss()
})
,isActive: $viewModel.isShowWebView, label: {
EmptyView()
}).opacity(0))
}
}
}
}
UIViewRepresentable
struct WebView: UIViewRepresentable {
var viewModel : ViewModel
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
context.coordinator.viewModel = viewModel
webView.navigationDelegate = context.coordinator
webView.load(URLRequest(url: URL(string: "https://www.google.com/")!))
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
let request = URLRequest(url: URL(string: "https://www.google.com/")!)
webView.load(request)
}
class Coordinator : NSObject, WKNavigationDelegate {
var viewModel : ViewModel?
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
//print("webview url \(webView.url)")
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let host = navigationAction.request.url?.absoluteString{
if host.contains("google.com") {
decisionHandler(.allow)
return
}else{
viewModel?.urlHasChanged = true
decisionHandler(.cancel)
return
}
}else{
decisionHandler(.cancel)
}
}
}
}
It is not that presentation, instead turn activating flag back, like
.onChange(of: viewModel.urlHasChanged, perform: { newValue in
print("called")
viewModel.isShowWebView = false // << here !!
})

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
}

Customize Mapbox style with layers in 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 ?

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

SwiftUI How can I add an activity indicator in WKWebView?

How can I add an activity indicator in WKWebView which will display the indicator while the webpage is loading and disappears when loaded ?
I've looked at some of the old posts but could not figure out how to do it in SwiftUI
see link to one of the old solutions below
How to add Activity Indicator to WKWebView (Swift 3)
Use UIViewRepresentable to create a UIActivityIndicatorView:
You control when an activity indicator animates by calling the startAnimating() and stopAnimating() methods. To automatically hide the activity indicator when animation stops, set the hidesWhenStopped property to true.
You can set the color of the activity indicator by using the color property.
struct ActivityIndicatorView: UIViewRepresentable {
#Binding var isAnimating: Bool
let style: UIActivityIndicatorView.Style
func makeUIView(context: UIViewRepresentableContext<ActivityIndicatorView>) -> UIActivityIndicatorView {
return UIActivityIndicatorView(style: style)
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicatorView>) {
isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
}
}
Create a LoadingView to allow you to wrap around your views:
This allows you to style the activity views content.
struct LoadingView<Content>: View where Content: View {
#Binding var isShowing: Bool
var content: () -> Content
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
self.content()
.disabled(self.isShowing)
.blur(radius: self.isShowing ? 3 : 0)
VStack {
Text("Loading...")
ActivityIndicatorView(isAnimating: .constant(true), style: .large)
}
.frame(width: geometry.size.width / 2, height: geometry.size.height / 5)
.background(Color.secondary.colorInvert())
.foregroundColor(Color.red)
.cornerRadius(20)
.opacity(self.isShowing ? 1 : 0)
}
}
}
}
If you want to be able to update the LoadingView(...) status you'll need to introduce a view model that inherits from ObservableObject:
Based on this answer: https://stackoverflow.com/a/58825642/264802
class WebViewModel: ObservableObject {
#Published var url: String
#Published var isLoading: Bool = true
init (url: String) {
self.url = url
}
}
struct WebView: UIViewRepresentable {
#ObservedObject var viewModel: WebViewModel
let webView = WKWebView()
func makeCoordinator() -> Coordinator {
Coordinator(self.viewModel)
}
class Coordinator: NSObject, WKNavigationDelegate {
private var viewModel: WebViewModel
init(_ viewModel: WebViewModel) {
self.viewModel = viewModel
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.viewModel.isLoading = false
}
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<WebView>) { }
func makeUIView(context: Context) -> UIView {
self.webView.navigationDelegate = context.coordinator
if let url = URL(string: self.viewModel.url) {
self.webView.load(URLRequest(url: url))
}
return self.webView
}
}
Then to use it inside your views you would do the following:
struct ContentView: View {
#StateObject var model = WebViewModel(url: "http://www.google.com")
var body: some View {
LoadingView(isShowing: self.$model.isLoading) {
WebView(viewModel: self.model)
}
}
}
Using 3 Steps I do it in my project.
Step 1: Create a Loading View
import SwiftUI
import UIKit
struct ActivityIndicatorView: UIViewRepresentable {
#Binding var isAnimating: Bool
let style: UIActivityIndicatorView.Style
func makeUIView(context: Context) -> UIActivityIndicatorView {
return UIActivityIndicatorView(style: style)
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) {
isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
}
}
// Main View
struct LoadingView<Content>: View where Content: View {
#Binding var isShowing: Bool
let message: String
var content: () -> Content
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
self.content()
.disabled(self.isShowing)
.blur(radius: self.isShowing ? 3 : 0)
VStack {
Text(self.message)
.bold()
ActivityIndicatorView(isAnimating: .constant(true), style: .large)
}
.frame(width: geometry.size.width / 2,
height: geometry.size.height / 5)
.background(Color.secondary.colorInvert())
.foregroundColor(Color.primary)
.cornerRadius(20)
.opacity(self.isShowing ? 1 : 0)
}
}
}
}
// Mark: Testing
struct LoadingIndicator: View {
var body: some View {
LoadingView(isShowing: .constant(true), message: "Loading...") {
NavigationView {
List(["1", "2", "3", "4", "5"], id: \.self) { row in
Text(row)
}.navigationBarTitle(Text("A List"), displayMode: .large)
}
}
}
}
struct ActivityIndicatorView_Previews: PreviewProvider {
static var previews: some View {
LoadingIndicator()
}
}
Step 2: Create a WebView and WebViewModel
import SwiftUI
import WebKit
class WebViewModel: ObservableObject {
#Published var isLoading: Bool = false
}
struct WebView: UIViewRepresentable {
#ObservedObject var webViewModel: WebViewModel
let urlString: String
func makeUIView(context: Context) -> WKWebView {
let wkWebView = WKWebView()
if let url = URL(string: urlString) {
let urlRequest = URLRequest(url: url)
wkWebView.load(urlRequest)
}
return wkWebView
}
func updateUIView(_ wkWebView: WKWebView, context: Context) {
// do nothing
}
class Coordinator: NSObject, WKNavigationDelegate {
let webViewModel: WebViewModel
init(_ webViewModel: WebViewModel) {
self.webViewModel = webViewModel
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
webViewModel.isLoading = true
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webViewModel.isLoading = false
}
}
func makeCoordinator() -> WebView.Coordinator {
Coordinator(webViewModel)
}
}
struct WebView_Previews: PreviewProvider {
static var previews: some View {
WebView(webViewModel: WebViewModel(),
urlString: "https://instagram.com/mahmudahsan/")
}
}
Step 3: In your main view use the following code to show indicator and webview
ZStack {
WebView(webViewModel: webViewModel, urlString: "http://ithinkdiff.net")
.frame(height: 1000)
if webViewModel.isLoading {
LoadingView(isShowing: .constant(true), message: "Loading...") {
EmptyView()
}
}
}