I am following the SwiftUI watchOS tutorial but I keep getting the following error when trying to preview WatchMapView in Section 4 Step 6:
WatchLandmarks.app crashed: communication with the app was interrupted
The error starts to occur only after adding this in WatchLandmarkDetail:
WatchMapView(landmark: self.landmark)
.scaledToFit()
.padding()
The WatchMapView code is as follows
import SwiftUI
struct WatchMapView: WKInterfaceObjectRepresentable {
var landmark: Landmark
func makeWKInterfaceObject(context: WKInterfaceObjectRepresentableContext<WatchMapView>) -> WKInterfaceMap {
return WKInterfaceMap()
}
func updateWKInterfaceObject(_ map: WKInterfaceMap, context: WKInterfaceObjectRepresentableContext<WatchMapView>) {
let span = MKCoordinateSpan(latitudeDelta: 0.02,
longitudeDelta: 0.02)
let region = MKCoordinateRegion(
center: landmark.locationCoordinate,
span: span)
map.setRegion(region)
}
}
struct WatchMapView_Previews: PreviewProvider {
static var previews: some View {
WatchMapView(landmark: UserData().landmarks[0])
.previewDevice("Apple Watch Series 5 - 44mm")
}
}
Related
Setup:
My app uses a SwiftUI Map, essentially as
struct MapViewSWUI: View {
#Binding private var show_map_modal: Bool
#State private var region: MKCoordinateRegion
//…
init(show_map_modal: Binding<Bool>) {
self._show_map_modal = show_map_modal
self.region = // Some computed region
//…
var body: some View {
//…
Map(coordinateRegion: $region)
.frame(width: 400, height: 300) // Some frame for testing
}
}
Using this code, I can show the map modally without problems.
Problem:
If I out comment the .frame view modifier, I get the runtime error
Modifying state during view update, this will cause undefined behavior.
with the following stack frame:
Question:
Why is it in my case required to set a frame for the Map? This tutorial does it, but Apple's docs don't.
How to do it right?
PS:
I have read this answer to a similar question and tried to catch the error with a runtime breakpoint, but it does not show anything interesting:
I found an answer to another questions related to the same error, but it doesn't apply here.
EDIT:
Workaround found, but not understood:
My map is presented modally from another view. This view has a state var that controls the presentation:
#State private var show_map_modal = false
The body of the view consists of a HStack with some views, and a fullScreenCover view modifier is applied to the HStack:
var body: some View {
HStack {
// …
}
.fullScreenCover(isPresented: $show_map_modal) {
MapViewSWUI(show_map_modal: $show_map_modal, itemToBeDisplayed: viewItem)
.ignoresSafeArea(edges: [.leading, .trailing])
}
}
If the map is presented in this way, no run time error is raised.
However, if I include (as it was done up to now) .top or .bottom in the edge set, the run time error Modifying state during view update is raised.
I would be glad for any hint to the reason.
My guess is that the error is not related to the frame at all, but to the update of the region once the sheet is presented.
As you can see in my code, I update the region 3 seconds after presenting the seet. Then, the error shows up.
Could the be happening in your code?
struct ContentView: View {
#State private var show_map_modal = false
var body: some View {
Button {
show_map_modal.toggle()
} label: {
Text("Show me the map!")
}
.sheet(isPresented: $show_map_modal) {
MapViewSWUI(show_map_modal: $show_map_modal)
}
}
}
struct MapViewSWUI: View {
#Binding private var show_map_modal: Bool
#State private var region: MKCoordinateRegion
init(show_map_modal: Binding<Bool>) {
self._show_map_modal = show_map_modal
self.region = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 51.507222,
longitude: -0.1275),
span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)
)
}
var body: some View {
VStack(alignment: .trailing) {
Button("Done") {
show_map_modal.toggle()
}
.padding(10)
Map(coordinateRegion: $region)
}
// .frame(width: 400, height: 300) // Some frame for testing
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.region = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 31.507222,
longitude: -1.1275),
span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)
)
}
}
}
}
I want a SwiftUI LazyGrid that can change its GridItem size with an animation, but a crash occurs while scrolling up after the size is made smaller.
Steps to Reproduce:
1 - scroll to bottom of 'xLarge' size grid
2 - change to 'large' size using Picker
3 - scroll up after size change animation has finished
Crash Error: Thread 1: EXC_BREAKPOINT (code=1, subcode=0x18d9f3b64)
Code Snippets:
import SwiftUI
#main
struct GridScrollDefectApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
import SwiftUI
struct ContentView: View {
#State var cellSize: CellSize = .xLarge
var body: some View {
let sizes = cellSize.adaptiveSizes
let objects = Array(0..<30).map { _ in return TestObject() }
VStack {
Picker("Size", selection: $cellSize.animation()) {
ForEach(CellSize.allCases) { cellSize in
Text(cellSize.rawValue.localizedCapitalized)
.tag(cellSize)
}
}
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: sizes.min, maximum: sizes.max))]) {
ForEach(objects) { _ in
Image(systemName: "multiply")
.resizable()
.aspectRatio(contentMode: .fit)
}
}
}
}
}
}
import Foundation
class TestObject: Identifiable {
var id: UUID = UUID()
}
enum CellSize: String, CaseIterable, Identifiable {
case xLarge, large
var id: RawValue { return rawValue }
var adaptiveSizes: (min: CGFloat, max: CGFloat) {
switch self {
case .xLarge:
return (330,400)
case .large:
return (150,200)
}
}
}
I've tried various combinations of storing the cellSize and the objects array in #State, #Binding, and a #ObservedObject #Published var, and all exhibit this issue. The only half-work-around I've found is to refresh the views using a .id() modifier on the grid, which avoids the crash, but also removes the resizing animation.
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.
UIViewRepresentable:
import SwiftUI
import MapKit
import CoreData
struct CustomView: UIViewRepresentable {
var coordinate: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
func updateUIView(_ uiView: MKMapView, context: Context) {
let span = MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
let region = MKCoordinateRegion(center: coordinate, span: span)
uiView.setRegion(region, animated: true)
}
}
ContentView:
import SwiftUI
import CoreLocation
struct ContentView: View {
var body: some View {
GeometryReader { geo in
CustomView(coordinate: CLLocationCoordinate2D(latitude: 37.33182, longitude: -122.03118))
.frame(width: geo.size.width, height: 300)
.cornerRadius(25)
.contextMenu(/*#START_MENU_TOKEN#*/ContextMenu(menuItems: {
Text("Menu Item 1")
Text("Menu Item 2")
Text("Menu Item 3")
})/*#END_MENU_TOKEN#*/)
}
.padding(.horizontal)
.frame(height: 300)
}
}
The above code works fine in the SwiftUI Canvas and the Simulator, however on my physical testing device (an iPhone 7 - iOS 14 Beta 5), when I long press the CustomView, it becomes black. The app also sometimes crashes with the following error which may be related:
CGImageCreate: invalid image alphaInfo: kCGImageAlphaNone. It should be kCGImageAlphaNoneSkipLast
If I replace the CustomView with an Image like below, everything works as expected:
import SwiftUI
struct ContentView: View {
var body: some View {
Image("imageName")
.resizable()
.frame(width: 350, height: 300)
.cornerRadius(25)
.contextMenu(/*#START_MENU_TOKEN#*/ContextMenu(menuItems: {
Text("Menu Item 1")
Text("Menu Item 2")
Text("Menu Item 3")
})/*#END_MENU_TOKEN#*/)
}
}
How can I fix it? Thanks!
I have integrated Mapbox with SwiftUI using the following example:
https://github.com/mapbox/mapbox-maps-swiftui-demo
It works fine. However when trying to display other #State variables on the View Stack, the UI Refresh propagation stops going down to the Mapbox call updateUIView()
For example, you can replicate the problem by replacing ContentView.swift from the above repository with the following code:
import SwiftUI
import Mapbox
struct ContentView: View {
#State var annotations: [MGLPointAnnotation] = [
MGLPointAnnotation(title: "Mapbox", coordinate: .init(latitude: 37.791434, longitude: -122.396267))
]
var body: some View {
ZStack {
VStack {
MapView(annotations: $annotations).centerCoordinate(.init(latitude: 37.791293, longitude: -122.396324)).zoomLevel(16)
Button(action: {
let rand = Float.random(in: 37.79...37.80)
self.annotations.append(MGLPointAnnotation(title: "Mapbox", coordinate: .init(latitude: CLLocationDegrees(rand), longitude: -122.396261)))
}) {
Text("Button")
Text("\(self.annotations.count)")
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Running the above code indicates that the Text("\(self.annotations.count)") UI gets updated - however, the annotations are not refreshed (hence updateUIView() is not called).
If I comment // Text("\(self.annotations.count)") then annotations are refreshed (and updateUIView() is called)
Does anybody have any ideas of what might be the issue? Or am I missing something here?
Thanks!
Answering my own question here thanks to this post
https://github.com/mapbox/mapbox-maps-swiftui-demo/issues/3#issuecomment-623905509
In order for this to work it is necessary to update the UIView being rendered inside Mapview:
func updateUIView(_ uiView: MGLMapView, context: Context) {
updateAnnotations(uiView)
trackUser()
}
private func updateAnnotations(_ view: MGLMapView) {
if let currentAnnotations = view.annotations {
view.removeAnnotations(currentAnnotations)
}
view.addAnnotations(annotations)
}