How would one update the position of a SwiftUI Map without impacting the zoom (or distance from a MapCamera point of view). So want:
a) map position being updated by incoming GPS co-ordinates
b) user may then on the Map zoom in/out
c) on subsequent GPS position changes I want to to keep the zoom/distance changes from the User and not reset these
From the code below the the issue seems to be when getting the current "distance" (i.e. mapCamPost.camera?distance) that this value seems to go to "nil" after the User zooms in the map.
struct GCMap: View {
@StateObject var locationMgr = GcFlightState()
@State private var mapCamPos: MapCameraPosition = .automatic
var body: some View {
ZStack {
Map(position: $mapCamPos) {
Annotation("UserLocation", coordinate: self.locationMgr.location.coordinate) {
Image(systemName: "airplane.circle").rotationEffect(.degrees(270))
}
}
.onMapCameraChange() {
print("onMapCameraChange \(mapCamPos.camera?.distance)")
}
.onReceive(locationMgr.$location) { location in
mapCamPos = .camera(MapCamera(
centerCoordinate: location.coordinate,
distance: mapCamPos.camera?.distance ?? 1000, // <<===
heading: location.course
))
}
}
}
}
Post
Replies
Boosts
Views
Activity
Anyone able to see how to use the new SwiftUI Map and WWDC @Observable concept to dynamically update my SwiftUI Map position and rotation based on the dynamic changes it picks up from my @Observable object.
Note the updates are coming through as the Text labels show this. But how to get the Map position referencing the same values and updating them? The "onAppear" approach doesn't seem to work.
import SwiftUI
import MapKit
@Observable
final class NewLocationManager : NSObject, CLLocationManagerDelegate {
var location: CLLocation? = nil
var direction: CLLocationDirection = 0
private let locationManager = CLLocationManager()
func startCurrentLocationUpdates() async throws {
if locationManager.authorizationStatus == .notDetermined {
locationManager.requestWhenInUseAuthorization()
}
for try await locationUpdate in CLLocationUpdate.liveUpdates() {
guard let location = locationUpdate.location else { return }
print("NewLocationManager: \(location.coordinate.latitude), \(location.coordinate.longitude)")
self.location = location
self.direction = self.direction + 1
}
}
}
struct ContentView: View {
var locationMgr = NewLocationManager()
@State private var mapCamPos: MapCameraPosition = .automatic
private let bigBen = CLLocationCoordinate2D(latitude: 51.500685, longitude: -0.124570)
var body: some View {
ZStack {
Map(position: $mapCamPos)
.onAppear { // Does NOT work - how to get position/direction updates working to Map (map should be moving/rotating)
mapCamPos = .camera(MapCamera(
centerCoordinate: self.locationMgr.location?.coordinate ?? bigBen,
distance: 800,
heading: self.locationMgr.direction
))
}
VStack (alignment: .leading) {
Text("Location from observable: \(locationMgr.location?.description ?? "NIL")") // This works (they get updates regularly)
Text("Direction from observable: \(locationMgr.direction)") // This works (they get updates regularly)
Spacer()
}
}
.task {
try? await locationMgr.startCurrentLocationUpdates()
}
}
}
#Preview {
ContentView()
}
Tag: wwdc2023-10043
Trying to use new Swift @Observable to monitor GPS position within SwiftUI content view. But how do I tie the latest locations to the SwiftUI Map's mapCameraPosition?
Well ideally the answer could cover:
How to fix this error - So get map tracking along with the User Position, but also
How to include facility to turn on/off the map moving to track the user position (which I'll need to do next). So could be tracking, then disable, move map around and have a look at things, then click button to start syncing the mapcameraposition to the GPS location again
Refer to error I'm embedded in the code below.
import SwiftUI
import MapKit
@Observable
final class NewLocationManager : NSObject, CLLocationManagerDelegate {
var location: CLLocation? = nil
private let locationManager = CLLocationManager()
func startCurrentLocationUpdates() async throws {
if locationManager.authorizationStatus == .notDetermined {
locationManager.requestWhenInUseAuthorization()
}
for try await locationUpdate in CLLocationUpdate.liveUpdates() {
guard let location = locationUpdate.location else { return }
self.location = location
}
}
}
struct ContentView: View {
var newlocationManager = NewLocationManager()
@State private var cameraPosition: MapCameraPosition = .region(MKCoordinateRegion(
center: newlocationManager.location?.coordinate ?? <#default value#>,
span: MKCoordinateSpan(latitudeDelta: 0.25, longitudeDelta: 0.25)
))
// GET ERROR: Cannot use instance member 'newlocationManager' within property initializer; property initializers run before 'self' is available
var body: some View {
ZStack {
Map(position: $cameraPosition)
Text("New location manager: \(newlocationManager.location?.description ?? "NIL" )") // works
}
.task {
try? await newlocationManager.startCurrentLocationUpdates()
}
}
}
#Preview {
ContentView()
}
How would remove the delete button in SwiftUI List rows when in Edit Mode? Note the hamburger button on the right of the row that allows rows to be re-ordered needs to continue to function.Background - Want a list that has the "re-order" rows functions always enabled. Edit mode seems to enable this (i.e. leave List in edit mode) however do not want the red delete button on each row.
How should my code be modified to ensure that when an exception happens at the Core Data layer when adding a new item, that SwiftUI does NOT continue to show a new item? Background: When I run the below code I get an exception when adding a new item doing the "context.save()", HOWEVER whilst the new item request really failed (did not save to Core Data), the UI does show a new item. It is as if the "lists" variable in the @FetchRequest line is not behaving dynamically. Question - How do I fix code so that the application works properly?Code: import SwiftUI
struct ContentView: View {
@Environment(\.managedObjectContext) var context
@FetchRequest(fetchRequest: List.allListFetchRequest()) var lists: FetchedResults
private func addListItem() {
let newList = List(context: context)
newList.id = 1
newList.title = "Testing 123"
do {
try context.save()
} catch let e as NSError {
print("Could not save new List. \(e.debugDescription)")
return
}
}
var body: some View {
NavigationView {
VStack {
ForEach(lists) { list in
Text("List = \(list.title)")
}
}
.navigationBarTitle( Text("My Todo Lists") )
.navigationBarItems(
trailing: Button(action: {self.addListItem()} ) {
Image(systemName: "plus.square")
}
)
}
}
}Example Output: Could not save new List. Error Domain=NSCocoaErrorDomain Code=133021 "(null)" UserInfo={NSExceptionOmitCallstacks=true, conflictList=(
"NSConstraintConflict (0x600001fcccc0) for constraint (\n id\n): database: (null), conflictedObjects: (\n \"0x600000a7e360 \",\n \"0xfb1bb528bb57810c \"\n)"
)}
How can one prevent auto-zooming with IOS MapKit when using userTrackingMode = .followWithHeading? That is I am setting the current user location to the centre of the screen, and have "view.userTrackingMode = .followWithHeading" so that the map orientates to north, but when you zoom in/out manually the MapView automatically overrides this and zooms back to the level it seems to prefer being at.
I was to be able to zoom in, then the zoom level stays like this, whilst it keeps the map centred to user location, and keeps auto-rotating to keep map aligned to north.
I am using SwiftUI so have effectively do have the location being passed into GCMapView as a parameter (as the means to keep the SwiftUI GCMapView up to date with latest user location). So not sure if this is causing an issue?
Some key bits (have pulled some code out to show relevant lines) of the MapKit call backs I'm using:
struct FlightView: View {
@EnvironmentObject var locationManager : GCLocationManager
@State var centreUserLocation : Bool = false
var body: some View {
GCMapView(
flight: flight,
userLocation: locationManager.userLocation,
centreUserLocation: centreUserLocation,
initalZoomDone: initalZoomDone
)
}
}
struct GCMapView : UIViewRepresentable {
let map = MKMapView()
func makeUIView(context: Context) -> MKMapView {
map.delegate = context.coordinator
map.isRotateEnabled = true
map.userTrackingMode = .followWithHeading
map.showsUserLocation = true
return map
}
func updateUIView(_ view: MKMapView, context: Context) {
if let userLocation = userLocation {
view.centerCoordinate = userLocation
view.userTrackingMode = .followWithHeading // Needed to keep map rotating to align to North
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, MKMapViewDelegate {
var parent: GCMapView
init(_ parent: GCMapView) {
self.parent = parent
super.init()
}
}
}
Can I parse a file on a USB drive from IOS? (Eg OTG cable to iPhone). Is USB device that mounts as file system normally, e.g. on MacBook
Which library/framework to use?
Or is it possible from iPad if not from iPhone?
When moving a row in a SwiftUI List I note that due to my "save Core Data" call the UI transition is jerky. I call save against core data as the list items orderNum has changed, so I'm saving that change that. Video and code below. If I remove the "GCCoreData.save()" there is no stutter.Animated GIF: https://i.stack.imgur.com/lsmtV.gifCode - Extract from my SwiftUI View private var tasksFetchRequest : FetchRequest
private var gcTasks : FetchedResults { tasksFetchRequest.wrappedValue }
init(withGcList gcList:GCList) {
self.currentList = gcList
self.tasksFetchRequest = FetchRequest(
entity: GCTask.entity(),
sortDescriptors: [NSSortDescriptor(key: "orderNum", ascending: true)],
predicate: NSPredicate(format: "gcList == %@", currentList)
)
} private func updateOrderNums(startIndex: Int, destIndex: Int, inc: Int) {
var currIndex = startIndex
while currIndex <= destIndex {
gcTasks[currIndex].orderNum = gcTasks[currIndex].orderNum + Int64(inc)
currIndex += 1
}
}
fileprivate func moveRow(fromStartIndex taskStartIndex: IndexSet.Element, toDestIndex taskDestIndex: Int, movingDown moveDown: Bool) {
gcTasks[taskStartIndex].orderNum = gcTasks[taskDestIndex].orderNum
updateOrderNums(
startIndex: moveDown ? taskStartIndex + 1 : taskDestIndex,
destIndex: moveDown ? taskDestIndex : taskStartIndex - 1,
inc: moveDown ? -1 : 1
)
// Save
GCCoreData.save() // <<== JERKINESS OCCURS DUE TO THIS
}Code - Library functions called class GCCoreData: NSObject {
static func save() {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error performing Core Data save: \(nserror), \(nserror.userInfo)")
}
}
}
}Note that change the "GCCoreData.save()" line to the below also does not assist. DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
GCCoreData.save()
} [1]: https://i.stack.imgur.com/lsmtV.gif
When using a List which I want to be in edit mode, but do NOT want to use "onDelete" (only want to use "onMove") there is an indentation occuring for the row (a bug I think). See below for example. Any work arounds until this is fixed/addressed?Snapshot Image https://i.stack.imgur.com/UAIIN.png Playground Codeimport SwiftUI
import PlaygroundSupport
let modelData: [String] = ["Eggs", "Milk", "Bread"]
struct ListTestNoDelete: View {
private func move(from uiStartIndexSet: IndexSet, to uiDestIndex: Int) {
print("On Move")
}
var body: some View {
NavigationView {
VStack {
List {
ForEach(modelData, id: \.self) { str in
HStack {
Image(systemName: "square")
Text(str)
}
}
.onMove(perform: self.move)
}
.environment(\.editMode, .constant(.active))
.navigationBarTitle( Text("Test") )
}
}
}
}
let listTest = ListTestNoDelete()
PlaygroundPage.current.liveView = UIHostingController(rootView: listTest)
Is it possible to "re-order"/"move" items in a ScrollView as one can in a List? I have playground below which compiles/run fine, however the onDelete / onMove does does visualise any delete button or hamburger lines for dragging/moving?Is there a way to get my code working here so at least the "onMove" concept works?Note - I note that both List and ScrollView (I think) support protocol DynamicViewContent, so was assuming it should work(?)Image after running code: https://i.stack.imgur.com/c5R8g.pngCode (Playgrounds)import SwiftUI
import PlaygroundSupport
let modelData: [String] = ["Eggs", "Milk", "Bread"]
struct ListTestNoDelete: View {
private func move(from uiStartIndexSet: IndexSet, to uiDestIndex: Int) {
print("On Move")
}
private func delete(at offsets : IndexSet) {
print("On Delete")
}
var body: some View {
NavigationView {
VStack {
ScrollView {
ForEach(modelData, id: \.self) { str in
HStack {
Image(systemName: "square")
Text(str)
Spacer()
}.padding(.horizontal, 3)
}
.onMove(perform: self.move)
.onDelete(perform: self.delete)
}
.environment(\.editMode, .constant(.active))
.navigationBarTitle( Text("Test") )
}
}
}
}
let listTest = ListTestNoDelete()
PlaygroundPage.current.liveView = UIHostingController(rootView: listTest)
ould the below be the most appropriate way to handle updating the orderNum of items in a List when the manual order is stored in core data (as the orderNum field).Specifically I note when I try to cycle through my fetchedResults list after I have deleted an item, that item is in fact still there. So what I'm doing is doing a core data save, and then after this updating the orderNum and then resaving. OK approach?Key code sections: @Environment(\.managedObjectContext) var context
@FetchRequest(fetchRequest: GCList.allListFetchRequest()) var gcLists: FetchedResults
private func save() {
// Save
do {
try self.context.save()
} catch let e as NSError {
print("ERROR : \(e.description)")
}
}
private func deleteList(at offsets: IndexSet) {
// Remove Items
for index in offsets {
let list = self.gcLists[index]
self.context.delete(list)
}
self.save()
// Redo orderNum *** NEED TO DO ANOTHER UPDATE/RESAVE LIKE THIS ***
var i = 0
self.gcLists.forEach { gcItem in
gcItem.orderNum = Int64(i)
i += 1
}
self.save()
}