swiftui mapkit polygon overlay - swiftui

I'm trying to show a polygon overlay on the map but I don't find what I'm doing wrong
my MapView file is:
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
#EnvironmentObject var vmHome: HomeViewModel
#State var restrictions: [MKOverlay] = []
func makeCoordinator() -> Coordinator {
return MapView.Coordinator()
}
func makeUIView(context: Context) -> MKMapView {
let view = vmHome.mapView
view.showsUserLocation = true
view.delegate = context.coordinator
vmHome.showRestrictedZones { (restrictions) in
self.restrictions = restrictions
print("dentro mapview \(restrictions)")
view.addOverlays(self.restrictions)
}
return view
}
func updateUIView(_ uiView: MKMapView, context: Context) {
}
class Coordinator: NSObject,MKMapViewDelegate{
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation.isKind(of: MKUserLocation.self){return nil}
else{
let pinAnnotation = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "PIN_VIEW")
pinAnnotation.tintColor = .red
pinAnnotation.animatesDrop = true
pinAnnotation.canShowCallout = true
return pinAnnotation
}
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let polygon = overlay as? MKPolygon {
let renderer = MKPolygonRenderer(polygon: polygon)
renderer.fillColor = UIColor.purple.withAlphaComponent(0.2)
renderer.strokeColor = .purple.withAlphaComponent(0.7)
return renderer
}
return MKOverlayRenderer(overlay: overlay)
}
}
}
then the view model where I want to convert a fixed array of locations in polygon and add them to MKOverlay array (I cut out some come from the view model that is not related to overlay)
import Foundation
import MapKit
import CoreLocation
class HomeViewModel: NSObject, ObservableObject, CLLocationManagerDelegate{
#Published var mapView = MKMapView()
var overlays: [MKOverlay] = []
func showRestrictedZones(completion: #escaping ([MKOverlay]) -> ()) {
let locations = [CLLocation(latitude: 11.3844028, longitude: 45.6174815), CLLocation(latitude: 11.5608707,longitude: 45.3305094), CLLocation(latitude: 11.8533817, longitude: 45.4447992), CLLocation(latitude: 11.8382755, longitude: 45.6314077), CLLocation(latitude: 11.6624943, longitude: 45.6942722), CLLocation(latitude: 11.3844028, longitude: 45.6174815)]
var coordinates = locations.map({(location: CLLocation) -> CLLocationCoordinate2D in return location.coordinate})
let polygon = MKPolygon(coordinates: &coordinates, count: locations.count)
print(locations.count)
overlays.append(polygon)
print(overlays)
DispatchQueue.main.async {
completion(self.overlays)
}
}
}
ad finally the home view
import SwiftUI
import CoreLocation
struct Home: View {
#EnvironmentObject var vmHome: HomeViewModel
#State var locationManager = CLLocationManager()
var body: some View {
VStack{
HStack{
Text("Hi,")
.font(.title)
.foregroundColor(.theme.primary)
.padding(.horizontal)
Spacer()
VStack(alignment: .trailing) {
HStack {
Image(systemName: "mappin.and.ellipse")
.font(.largeTitle)
.foregroundColor(.blue)
Text("O1")
.font(.title)
.foregroundColor(.theme.primary)
}
Text(vmHome.currentAddress)
.font(.callout)
.foregroundColor(.theme.primary)
}
.padding(.horizontal)
}
ZStack(alignment: .bottom) {
MapView()
.environmentObject(vmHome)
.ignoresSafeArea(.all, edges: .bottom)
//VStack{
Button(action: vmHome.focusLocation, label: {
Image(systemName: "location.fill")
.font(.title2)
.padding(10)
.background(Color.primary)
.clipShape(Circle())
})
.frame(maxWidth: .infinity, alignment: .trailing)
.padding()
.padding(.bottom)
//}
}
}
.background(Color.theme.backgroud)
.onAppear {
locationManager.delegate = vmHome
locationManager.requestWhenInUseAuthorization()
}
.alert(isPresented: $vmHome.permissionDenied, content: {
Alert(title: Text("Permission Denied"), message: Text("Please Enable Permission In App Settings"), dismissButton: .default(Text("Goto Settings"), action: {
// Redireting User To Settings...
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
}))
})
}
}
struct Home_Previews: PreviewProvider {
static var previews: some View {
Home()
.environmentObject(HomeViewModel())
}
}
when I debug the array of MKOverlay I have a value like this [<MKPolygon: 0x282200f30>]
so I suppose that inside there's something
thanks

I recommend watching WWDC 2019 Integrating SwiftUI to learn the correct design, from 12:41.
Notice their UIViewRepresentable struct has a #Binding var, which in your case should be the array of polygons or overlays. updateView is called when that value changes and that is where you need to update the MKMapView with the differences in the array from last time. Also you should create the MKMapView in makeUIView do not fetch one from somewhere else.
I would also suggest removing the view model objects and instead learning SwiftUI View structs and property wrappers (which make the efficient structs have view model object semantics). WWDC 2019 Data Flow Through SwiftUI is a great starting point. You'll notice they never use objects for view data.

Related

SwiftUI Web View unable to go back and forward

I am using WebView for loading the html into view . I added the forward and back button to go back and forward with require code but the problem is when I enter the url and click more link , I do not see the back button or forward button is enable ..
Here is the content view ..
import SwiftUI
struct ContentView: View {
#State private var selection = 0
var body: some View {
TabView(selection: $selection) {
NavigationView {
WebListView().navigationBarTitle("Web View ", displayMode: .inline)
.toolbarBackground(Color.white,for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
.accentColor(.red)
.onAppear() {
UITabBar.appearance().barTintColor = .white
}
}.tabItem {
Image(systemName: "person.crop.circle")
Text("Web View")
}.tag(2)
}
}
}
Here is the code ListView ..
import SwiftUI
struct WebListView: View {
#StateObject var model = WebViewModel()
var body: some View {
WebContentView()
.font(.system(size: 30, weight: .bold, design: .rounded))
.toolbar {
ToolbarItemGroup(placement: .automatic) {
Button(action: {
model.goBack()
}, label: {
Image(systemName: "chevron.left")
})
.disabled(!model.canGoBack)
.font(.system(size: 20))
Button(action: {
model.goForward()
}, label: {
Image(systemName: "chevron.right")
})
.disabled(!model.canGoForward)
.font(.system(size: 20))
Spacer()
}
}
}
}
Code for UIViewRepresentable..
struct WebView: UIViewRepresentable {
typealias UIViewType = WKWebView
let webView: WKWebView
func makeUIView(context: Context) -> WKWebView {
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) { }
}
Here is the WebContent view code ..
import Combine
import WebKit
import SwiftUI
#MainActor
struct WebContentView: View {
#StateObject var model = WebViewModel()
var body: some View {
ZStack(alignment: .bottom) {
Color.blue
VStack(spacing: 0) {
HStack(spacing: 10) {
HStack {
TextField("Enter url",
text: $model.urlString)
.keyboardType(.URL)
.autocapitalization(.none)
.disableAutocorrection(true)
.padding(8)
.font(.system(size: 15))
Spacer()
}
.background(Color.white)
.cornerRadius(30)
Button("GO", action: {
model.loadUrl()
})
.foregroundColor(.white)
.padding(10)
.font(.system(size: 15))
.background(.blue)
}.padding(10)
ZStack {
WebView(webView: model.webView)
if model.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
}
}
}
}
}
}
struct WebContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here is the view Model code ..
#MainActor
class WebViewModel: ObservableObject {
let webView: WKWebView
private let navigationDelegate: WebViewNavigationDelegate
init() {
let configuration = WKWebViewConfiguration()
configuration.websiteDataStore = .nonPersistent()
webView = WKWebView(frame: .zero, configuration: configuration)
navigationDelegate = WebViewNavigationDelegate()
webView.navigationDelegate = navigationDelegate
setupBindings()
}
#Published var urlString: String = ""
#Published var canGoBack: Bool = false
#Published var canGoForward: Bool = false
#Published var isLoading: Bool = false
private func setupBindings() {
webView.publisher(for: \.canGoBack)
.assign(to: &$canGoBack)
webView.publisher(for: \.canGoForward)
.assign(to: &$canGoForward)
webView.publisher(for: \.isLoading)
.assign(to: &$isLoading)
}
func loadUrl() {
guard let url = URL(string: urlString) else {
return
}
webView.load(URLRequest(url: url))
}
func goForward() {
webView.goForward()
}
func goBack() {
webView.goBack()
}
}
here is code for delegate ..
import WebKit
class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
// TODO
decisionHandler(.allow)
}
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
// TODO
decisionHandler(.allow)
}
}
Here is the screenshot ..
The problem is that you're creating a new WebViewModel in WebContentView
Change
struct WebContentView: View {
#StateObject var model = WebViewModel()
//etc
}
to
struct WebContentView: View {
#EnvironmentObject var model: WebViewModel
//etc
}
Then update here…
struct WebListView: View {
#StateObject var model = WebViewModel()
var body: some View {
WebContentView()
.environmentObject(model) // add environmentObject

Can I hide default points of interests in MapKit with SwiftUI like parks, restaurants etc?

I want to use my own map annotations and I am trying to hide default annotations from Map.
I found this to remove every default annotation from the map view
let configuration = MKStandardMapConfiguration()
configuration.pointOfInterestFilter = MKPointOfInterestFilter(including: [])
But how do I apply this configuration to my map view in SwiftUI?
import SwiftUI
import MapKit
#available(iOS 16.0, *)
struct MyMapView: View {
init (){
let configuration = MKStandardMapConfiguration()
configuration.pointOfInterestFilter = MKPointOfInterestFilter(including: [])
}
#State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 41.59710,
longitude: -74.14976),
span: MKCoordinateSpan(
latitudeDelta: 0.0125,
longitudeDelta: 0.0125)
)
var body: some View {
Map(coordinateRegion: $region)
.edgesIgnoringSafeArea(.all)
.disabled(true)
}
}
The SwiftUI Map view doesn't support this functionality. To get a map that can do this you'd need to use an MKMapView which is in UIKit. Here's an example of how to use it in SwiftUI
import SwiftUI
import MapKit
struct ContentView: View {
var body: some View {
MapView()
.edgesIgnoringSafeArea(.all)
.disabled(true)
}
}
struct MapView: UIViewRepresentable {
let configuration: MKStandardMapConfiguration
private var center: CLLocationCoordinate2D {
CLLocationCoordinate2D(latitude: 41.59710, longitude: -74.14976)
}
private var coordinateSpan: MKCoordinateSpan {
MKCoordinateSpan(latitudeDelta: 0.0125, longitudeDelta: 0.0125)
}
init() {
configuration = MKStandardMapConfiguration()
configuration.pointOfInterestFilter = MKPointOfInterestFilter(including: [])
}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.region = MKCoordinateRegion(center: center, span: coordinateSpan)
mapView.preferredConfiguration = configuration
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) { }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

swiftUI Mapkit and Corelocation causing problems

I'm reposting my question of yesterday and now adding a clean code example to demonstrate the problem
I have a MyCustomMapView, embedding a MKMApView and it starts at a fixed location. I have a function called gotoCoordinate, which accepts a coordinate and then navigates the mapview's center to that coordinate.
In the sample code that can be simulated by clicking on the red button labelleing "Click here to change map position".
This all works great. Until....
in the app I'm working on I also need to have a user location so I have a LocationViewModel handling the request. Once you have given request to access your location, click the button no longer moves the center of the map to that new coordinate.
Once you comment the #StateObject var locationViewModel = LocationViewModel() it is working again.
So it seems that once you are using a location manager with a delegate the map no longer moves when changing it's region
Is this a bug or am I doing something wrong?
import SwiftUI
struct ContentView: View {
#StateObject var locationViewModel = LocationViewModel()
var body: some View {
switch locationViewModel.authorizationStatus {
case .notDetermined:
AnyView(RequestLocationView())
.environmentObject(locationViewModel)
case .restricted:
ErrorView(errorText: "Location use is restricted.")
case .denied:
ErrorView(errorText: "The app does not have location permissions. Please enable them in settings.")
default:
EmptyView()
}
GeometryReader { geometry in
DisplayMapView(size:geometry.size)
}
}
}
import SwiftUI
import CoreLocation
import MapKit
struct MyCustomMapView: UIViewRepresentable {
var map = MKMapView() // << constructor contract !!
let coordinate: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude:31,longitude: -86 )
func makeUIView(context: Context) -> MKMapView {
map.delegate = context.coordinator
map.showsUserLocation = true
map.showsCompass = true
let region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: coordinate.latitude,longitude: coordinate.longitude),
span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
map.setRegion(region, animated: true)
return map
}
func gotoCoordinate(_ newCoordinate: CLLocationCoordinate2D ){
let region = MKCoordinateRegion(center: newCoordinate, span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2))
map.setRegion(region, animated: true)
}
func updateUIView(_ uiView: MKMapView, context: Context) {
}
func makeCoordinator() -> MyCustomMapView.Coordinator {
return MyCustomMapView.Coordinator(parent1: self)
}
final class Coordinator: NSObject, MKMapViewDelegate {
var parent:MyCustomMapView
init(parent1:MyCustomMapView){
parent = parent1
}
}//class Coordinator
}
import SwiftUI
import CoreLocation
import MapKit
struct DisplayMapView: View {
#Environment(\.presentationMode) var presentationMode
var size: CGSize
var startCoordinate: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude:40.741895,longitude: -73.989308)
var map = MyCustomMapView()
var body: some View {
ZStack(alignment:.top){
map
VStack(alignment:.leading){
HStack {
HStack {
Text("Click here to change map position")
.onTapGesture(){
map.gotoCoordinate(startCoordinate)
}
}
.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.foregroundColor(.black)
.background(Color(.red))
.cornerRadius(10.0)
}
}.padding(.top,50).padding(.leading,20).padding(.trailing,20)
}.ignoresSafeArea()
}
}
import Foundation
import SwiftUI
import CoreLocation
class LocationViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var authorizationStatus: CLAuthorizationStatus
#Published var lastSeenLocation: CLLocation?
#Published var currentPlacemark: CLPlacemark?
private let locationManager: CLLocationManager
static let shared = LocationViewModel()
override init() {
locationManager = CLLocationManager()
authorizationStatus = locationManager.authorizationStatus
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = 0.4
locationManager.startUpdatingLocation()
}
func requestPermission() {
locationManager.requestWhenInUseAuthorization()
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
authorizationStatus = manager.authorizationStatus
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
lastSeenLocation = locations.first
}
}
struct RequestLocationView: View {
#EnvironmentObject var locationViewModel: LocationViewModel
var body: some View {
VStack(spacing:50) {
Image(systemName: "location.circle")
.resizable()
.frame(width: 100, height: 100, alignment: .center)
.foregroundColor(Color.init(red: 0.258, green: 0.442, blue: 0.254))
Button(action: {
locationViewModel.requestPermission()
}, label: {
Label(LocalizedStringKey("allowLocationAccess"), systemImage: "location")
})
.padding(10)
.foregroundColor(.white)
.background(.green)
.clipShape(RoundedRectangle(cornerRadius: 8))
Text("We need your permission to give you the best experience.")
.foregroundColor(.gray)
.font(.caption)
}
}
}
struct ErrorView: View {
var errorText: String
var body: some View {
VStack {
Image(systemName: "xmark.octagon")
.resizable()
.frame(width: 100, height: 100, alignment: .center)
Text(errorText)
}
.padding()
.foregroundColor(.white)
.background(Color.red)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
Declare your coordinates as a stateful variable, either as #State or as #Published within an observable object:
struct DisplayMapView: View {
#State var coordinates = CLLocationCoordinate2D(latitude:40.741895,longitude: -73.989308)
Then pass the coordinates in as an argument to your view - no need to store your view as a variable:
ZStack(alignment: .top) {
MyMapView(coordinates: coordinates)
VStack(alignment: .leading) {
// etc.
Then you’ll need to do some rejigging in your UIViewRepresentable. You mustn't retain map as a separate instance outside makeUIView and updateUIView - SwiftUI structs can be recreated at will, so that would release your MKMapView instance and create a new one. Instead, the object returned by makeUIView is retained for you by the system. You do need to declare a variable that will accept the coordinates argument above, and then respond to any changes in it in updateUIView.
struct MyMapView: UIViewRepresentable {
var coordinates: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
let map = MKMapView()
map.delegate = context.coordinator
// etc.
return map
}
func updateUIView(_ uiView: MKMapView, context: Coordinator) {
let region = MKCoordinateRegion(center: coordinates, span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2))
uiView.setRegion(region, animated: true)
}
}
Now, when the user taps, instead of calling a function inside your view, you update the DisplayMapView’s coordinates variable and the UIViewRepresentable’s update logic should redraw the map in the correct position.

Change the mapType to .satellite etc with a picker

I want to be able to change the mapType from .standard to .satellite and .hybrid in xCode 13.3 Can anybody tell me if it is at all possible with this code? I implemented a picker to do the job but unfortunately I could not make it work. I succeeded making it change with different code but then buttons map + and map - would not work anymore
import Foundation
import SwiftUI
import MapKit
struct QuakeDetail: View {
var quake: Quake
#State private var region : MKCoordinateRegion
init(quake : Quake) {
self.quake = quake
_region = State(wrappedValue: MKCoordinateRegion(center: quake.coordinate,
span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)))
}
#State private var mapType: MKMapType = .standard
var body: some View {
VStack {
Map(coordinateRegion: $region, annotationItems: [quake]) { item in
MapMarker(coordinate: item.coordinate, tint: .red)
} .ignoresSafeArea()
HStack {
Button {
region.span.latitudeDelta *= 0.5
region.span.longitudeDelta *= 0.5
} label: {
HStack {
Text("map")
Image(systemName: "plus")
}
}.padding(5)//.border(Color.blue, width: 1)
Spacer()
QuakeMagnitude(quake: quake)
Spacer()
Button {
region.span.latitudeDelta /= 0.5
region.span.longitudeDelta /= 0.5
} label: {
HStack {
Text("map")
Image(systemName: "minus")
}
}
}.padding(.horizontal)
Text(quake.place)
.font(.headline)
.bold()
Text("\(quake.time.formatted())")
.foregroundStyle(Color.secondary)
Text("\(quake.latitude) \(quake.longitude)")
VStack {
Picker("", selection: $mapType) {
Text("Standard").tag(MKMapType.standard)
Text("Satellite").tag(MKMapType.satellite)
Text("Hybrid").tag(MKMapType.hybrid)
}
.pickerStyle(SegmentedPickerStyle())
.font(.largeTitle)
}
}
}
}
Here is the code that changes the mapType but the buttons do not work anymore:
import Foundation
import SwiftUI
import MapKit
struct QuakeDetail: View {
var quake: Quake
#State private var region : MKCoordinateRegion
init(quake : Quake) {
self.quake = quake
_region = State(wrappedValue: MKCoordinateRegion(center: quake.coordinate,
span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)))
}
#State private var mapType: MKMapType = .standard
var body: some View {
VStack {
MapViewUIKit(region: region, mapType: mapType)
.edgesIgnoringSafeArea(.all)
HStack {
Button {
region.span.latitudeDelta *= 0.5
region.span.longitudeDelta *= 0.5
} label: {
HStack {
Text("map")
Image(systemName: "plus")
}
}.padding(5)//.border(Color.blue, width: 1)
Spacer()
QuakeMagnitude(quake: quake)
Spacer()
Button {
region.span.latitudeDelta /= 0.5
region.span.longitudeDelta /= 0.5
} label: {
HStack {
Text("map")
Image(systemName: "minus")
}
}
}.padding(.horizontal)
Text(quake.place)
.font(.headline)
.bold()
Text("\(quake.time.formatted())")
.foregroundStyle(Color.secondary)
Text("\(quake.latitude) \(quake.longitude)")
Picker("", selection: $mapType) {
Text("Standard").tag(MKMapType.standard)
Text("Satellite").tag(MKMapType.satellite)
Text("Hybrid").tag(MKMapType.hybrid)
//Text("Hybrid flyover").tag(MKMapType.hybridFlyover)
}
.pickerStyle(SegmentedPickerStyle())
.font(.largeTitle)
}
}
}
struct MapViewUIKit: UIViewRepresentable {
let region: MKCoordinateRegion
let mapType : MKMapType
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.setRegion(region, animated: false)
mapView.mapType = mapType
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.mapType = mapType
}
}
I implemented the code and now the buttons work and the mapType changes correctly, thank you very much also for the pointers to the documentation. Unfortunately the annotations do not display the pin at the earthquake location. I changed the title from London to quake.place and the coordinate to coordinate: CLLocationCoordinate2D(latitude: region.span.latitudeDelta, longitude: region.span.longitudeDelta) but it made no difference. Here are my changes:
import SwiftUI
import MapKit
struct QuakeDetail: View {
var quake: Quake
#State private var region : MKCoordinateRegion
init(quake : Quake) {
self.quake = quake
_region = State(wrappedValue: MKCoordinateRegion(center: quake.coordinate,
span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)))
}
#State private var mapType: MKMapType = .standard
var body: some View {
VStack {
MapViewUIKit(
region: $region,
mapType: mapType,
annotation: Annotation(
title: quake.place,
coordinate: CLLocationCoordinate2D(latitude: region.span.latitudeDelta, longitude: region.span.longitudeDelta)
) // annotation
).ignoresSafeArea() // MapViewUIKit
Spacer()
HStack {
Button {
region.span.latitudeDelta *= 0.5
region.span.longitudeDelta *= 0.5
} label: {
HStack {
Image(systemName: "plus")
}
}//.padding(5)
Spacer()
QuakeMagnitude(quake: quake)
Spacer()
Button {
region.span.latitudeDelta /= 0.5
region.span.longitudeDelta /= 0.5
} label: {
HStack {
Image(systemName: "minus")
}
}
}.padding(.horizontal) // HStack + - buttons and quake magnitude
Text(quake.place)
Text("\(quake.time.formatted())")
.foregroundStyle(Color.secondary)
Text("\(quake.latitude) \(quake.longitude)")
.padding(.bottom, -5)
Picker("", selection: $mapType) {
Text("Standard").tag(MKMapType.standard)
Text("Satellite").tag(MKMapType.satellite)
Text("Hybrid").tag(MKMapType.hybrid)
}
.pickerStyle(SegmentedPickerStyle())
}
}
}
struct Annotation {
let pointAnnotation: MKPointAnnotation
init(title: String, coordinate: CLLocationCoordinate2D) {
pointAnnotation = MKPointAnnotation()
pointAnnotation.title = title
pointAnnotation.coordinate = coordinate
}
}
struct MapViewUIKit: UIViewRepresentable {
#Binding var region: MKCoordinateRegion
let mapType : MKMapType
let annotation: Annotation
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.setRegion(region, animated: false)
mapView.mapType = mapType
// Set the delegate so that we can listen for changes and
// act appropriately
mapView.delegate = context.coordinator
// Add the annotation to the map
mapView.addAnnotation(annotation.pointAnnotation)
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.mapType = mapType
// Update your region so that it is now your new region
mapView.setRegion(region, animated: true)
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapViewUIKit
init(_ parent: MapViewUIKit) {
self.parent = parent
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
// We should handle dequeue of annotation view's properly so we have to write this boiler plate.
// This basically dequeues an MKAnnotationView if it exists, otherwise it creates a new
// MKAnnotationView from our annotation.
guard annotation is MKPointAnnotation else { return nil }
let identifier = "Annotation"
guard let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) else {
let annotationView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
annotationView.canShowCallout = true
return annotationView
}
annotationView.annotation = annotation
return annotationView
}
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
// We need to update the region when the user changes it
// otherwise when we zoom the mapview will return to its original region
DispatchQueue.main.async {
self.parent.region = mapView.region
}
}
}
}
So to use MKMapView we need to set up the UIViewRepresentable properly. MKMapView has a delegate and as such we need to set the delegate for our mapView. We do this by adding a Coordinator to our UIViewRepresentable
So here is a full working example, it may not be 100% perfect but it shows the general idea of what you can do.
I created my own ContentView because your code was missing several things (such as Quake).
MapViewUIKit takes three parameters.
A binding for MKCoordinateRegion, it needs to be a binding as we will be passing data back to the ContentView
The mapType which is a MKMapType, this is for changing the map type
An annotation, this is a custom Annotation type that is used to hold the information about the annotation we wish to show on the map.
struct ContentView: View {
#State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 51.507222,
longitude: -0.1275),
span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)
)
#State private var mapType: MKMapType = .standard
var body: some View {
VStack {
Picker("", selection: $mapType) {
Text("Standard").tag(MKMapType.standard)
Text("Satellite").tag(MKMapType.satellite)
Text("Hybrid").tag(MKMapType.hybrid)
}
.pickerStyle(SegmentedPickerStyle())
MapViewUIKit(
region: $region,
mapType: mapType,
annotation: Annotation(
title: "London",
coordinate: CLLocationCoordinate2D(latitude: 51.507222, longitude: -0.1275)
)
)
HStack {
Button {
region.span.latitudeDelta *= 0.5
region.span.longitudeDelta *= 0.5
} label: {
HStack {
Image(systemName: "plus")
}
}.padding(5)
Button {
region.span.latitudeDelta /= 0.5
region.span.longitudeDelta /= 0.5
} label: {
HStack {
Image(systemName: "minus")
}
}.padding(5)
}
}
}
}
This is the Annotation struct that I created to hold the information about the annotation that we wish to display.
struct Annotation {
let pointAnnotation: MKPointAnnotation
init(title: String, coordinate: CLLocationCoordinate2D) {
pointAnnotation = MKPointAnnotation()
pointAnnotation.title = title
pointAnnotation.coordinate = coordinate
}
}
Finally we need the UIViewRepresentable to tie it all together. I've commented in the code to show what it does.
struct MapViewUIKit: UIViewRepresentable {
#Binding var region: MKCoordinateRegion
let mapType : MKMapType
let annotation: Annotation
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.setRegion(region, animated: false)
mapView.mapType = mapType
// Set the delegate so that we can listen for changes and
// act appropriately
mapView.delegate = context.coordinator
// Add the annotation to the map
mapView.addAnnotation(annotation.pointAnnotation)
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.mapType = mapType
// Update your region so that it is now your new region
mapView.setRegion(region, animated: true)
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapViewUIKit
init(_ parent: MapViewUIKit) {
self.parent = parent
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
// We should handle dequeue of annotation view's properly so we have to write this boiler plate.
// This basically dequeues an MKAnnotationView if it exists, otherwise it creates a new
// MKAnnotationView from our annotation.
guard annotation is MKPointAnnotation else { return nil }
let identifier = "Annotation"
guard let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) else {
let annotationView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
annotationView.canShowCallout = true
return annotationView
}
annotationView.annotation = annotation
return annotationView
}
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
// We need to update the region when the user changes it
// otherwise when we zoom the mapview will return to its original region
DispatchQueue.main.async {
self.parent.region = mapView.region
}
}
}
}
This gives the following output
https://imgur.com/a/gH42UED
I would suggest that you familiarise yourself with Apple's documentation and there is a wealth of tutorials out there that can help you.
https://developer.apple.com/documentation/mapkit/mkmapview
https://developer.apple.com/documentation/mapkit/mkmapviewdelegate
https://www.raywenderlich.com/7738344-mapkit-tutorial-getting-started
https://www.hackingwithswift.com/example-code/location/how-to-add-annotations-to-mkmapview-using-mkpointannotation-and-mkpinannotationview
https://talk.objc.io/episodes/S01E195-wrapping-map-view

SiwftUI: update model in UIViewRepresentable Coordinator when View updates?

So I am using a WKWebView within UIViewRepresentable so I can show a web view in my SwiftUI view.
For a while I could not figure out why my SwiftUI view would not update when the Coordinator would set #Publsihed properties that affect the SwiftUI view.
In the process I finally understood better how UIViewRepresentable works and realized what the problem was.
This is the UIViewRepresentable:
struct SwiftUIWebView : UIViewRepresentable {
#ObservedObject var viewModel: WebViewModel
func makeCoordinator() -> Coordinator {
Coordinator(self, viewModel: viewModel)
}
let webView = WKWebView()
func makeUIView(context: Context) -> WKWebView {
....
return self.webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
// This made my SwiftUI view update properly when the web view would report loading progress etc..
context.coordinator.viewModel = viewModel
}
}
The SwiftUI view would pass in the viewModel, then makeCoordinator would be called (only the first time at init...), then the Coordinator would be returned with that viewModel.
However, subsequently when a new viewModel was passed in on updates and not on coordinator init, the coordinator would just keep the old viewModel and things would stop working.
So I added this in the updateUIView... call, which did fix the problem:
context.coordinator.viewModel = viewModel
Question:
Is there a way to pass in the viewModel to the Coordinator during the func makeUIView(context: Context) -> WKWebView { ... } so that if a new viewModel is passed in to SwiftUIWebView the coordinator would also get the change automatically instead of me having to add:
context.coordinator.viewModel = viewModel
in updateUIView...?
EDIT:
Here is the entire code. The root content view:
struct ContentView: View {
#State var showTestModal = false
#State var redrawTest = false
var body: some View {
NavigationView {
VStack {
Button(action: {
showTestModal.toggle()
}) {
Text("Show modal")
}
if redrawTest {
Text("REDRAW")
}
}
}
.fullScreenCover(isPresented: $showTestModal) {
WebContentViewTest(redraw: $redrawTest)
}
}
}
And what the Content view presents:
struct SwiftUIProgressBar: View {
#Binding var progress: Double
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle()
.foregroundColor(Color.gray)
.opacity(0.3)
.frame(width: geometry.size.width, height: geometry.size.height)
Rectangle()
.foregroundColor(Color.blue)
.frame(width: geometry.size.width * CGFloat((self.progress)),
height: geometry.size.height)
.animation(.linear(duration: 0.5))
}
}
}
}
struct SwiftUIWebView : UIViewRepresentable {
#ObservedObject var viewModel: WebViewModel
func makeCoordinator() -> Coordinator {
Coordinator(self, viewModel: viewModel)
}
let webView = WKWebView()
func makeUIView(context: Context) -> WKWebView {
print("SwiftUIWebView MAKE")
if let url = URL(string: viewModel.link) {
self.webView.load(URLRequest(url: url))
}
return self.webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
//add your code here...
}
}
class Coordinator: NSObject {
private var viewModel: WebViewModel
var parent: SwiftUIWebView
private var estimatedProgressObserver: NSKeyValueObservation?
init(_ parent: SwiftUIWebView, viewModel: WebViewModel) {
print("Coordinator init")
self.parent = parent
self.viewModel = viewModel
super.init()
estimatedProgressObserver = self.parent.webView.observe(\.estimatedProgress, options: [.new]) { [weak self] webView, _ in
print(Float(webView.estimatedProgress))
guard let weakSelf = self else{return}
print("in progress observer: model is: \(Unmanaged.passUnretained(weakSelf.parent.viewModel).toOpaque())")
weakSelf.parent.viewModel.progress = webView.estimatedProgress
}
}
deinit {
estimatedProgressObserver = nil
}
}
class WebViewModel: ObservableObject {
#Published var progress: Double = 0.0
#Published var link : String
init (progress: Double, link : String) {
self.progress = progress
self.link = link
print("model init: \(Unmanaged.passUnretained(self).toOpaque())")
}
}
struct WebViewContainer: View {
#ObservedObject var model: WebViewModel
var body: some View {
ZStack {
SwiftUIWebView(viewModel: model)
VStack {
if model.progress >= 0.0 && model.progress < 1.0 {
SwiftUIProgressBar(progress: .constant(model.progress))
.frame(height: 15.0)
.foregroundColor(.accentColor)
}
Spacer()
}
}
}
}
struct WebContentViewTest : View {
#Binding var redraw:Bool
var body: some View {
let _ = print("WebContentViewTest body")
NavigationView {
ZStack(alignment: .topLeading) {
if redraw {
WebViewContainer(model: WebViewModel(progress: 0.0, link: "https://www.google.com"))
}
VStack {
Button(action: {
redraw.toggle()
}) {
Text("redraw")
}
Spacer()
}
}
.navigationBarTitle("Test Modal", displayMode: .inline)
}
}
}
If you run this you will see that while WebViewModel can get initialized multiple times, the coordinator will only get initialized once and the viewModel in it does not get updated. Because of that, things break after the first redraw.