FetchRequest and reset in SwiftUI View? - core-data

I tried around with reset() of the context and found a strange behavior. I use the property wrapper for the fetch request in the SwiftUI View. When resetting the context why are there still the objects in the list? Does it only reset all objects which aren't saved? But what is the difference to rollback()? While testing I found a strange behavior which could have to do with the Discussion part of the Docs but how should it be handled with the property wrapper, or is it better to use rollback()?
And after clicking the reset button the list doesn't get updated, also there is one persons name setted to nil, which is strange too.
Link to Video: https://imgur.com/a/tUCPHUl
From Doc:
Summary
Returns the context to its base state. Declaration
func reset()
Discussion
All the receiver's managed objects are “forgotten.” If you use this
method, you should ensure that you also discard references to any
managed objects fetched using the receiver, since they will be invalid
afterwards.
My Code:
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(sortDescriptors: [SortDescriptor(\.name)]) var persons: FetchedResults<Person>
func saveContext(){
do{
try context.save()
}catch{}
}
var body: some View {
VStack{
Text("\(persons.count)")
if persons.isEmpty{
Text("No persons defined")
}else{
ScrollView{
LazyVStack{
ForEach(persons){p in
Text(p.name ?? "NO NAME").background(Color.green)
}
}
}
}
Spacer()
Button("Add Person without save"){
let p = Person(context: context)
p.id = UUID()
p.name = "Test Without save \(persons.count)"
}
Button("Add Person with save"){
let p = Person(context: context)
p.id = UUID()
p.name = "Test with save \(persons.count)"
saveContext()
}
Button("reset"){
context.reset()
}
Button("rollback"){
context.rollback()
}
Button("delete All"){
for p in persons{
context.delete(p)
}
saveContext()
}
Button("save Context"){
saveContext()
}
}
}
}

Related

SwiftUI ForEach force UI update when updating the contents of a core data relationship

My app is meant to have a bunch of workouts in core data, each with a relationship to many exercises. A view should display the data in each workout (name, description etc.) and then iterate and display each exercise belonging to that workout.
Adding exercises and displaying them works fine. If an exercise is deleted, however it:
deletes from coredata no worries
the information seems to delete from iterableExercises
however, the Text line does not disappear. it goes from, for example "Squat, Description" to simply " , "
If I close the app entirely and reopen, then the " , " lines do completely disappear.
The problem code:
if let iterableExercises = workout.exercises?.array as? [ExerciseEntity] {
ForEach(iterableExercises) {exercise in
Text("\(exercise.name ?? ""), \(exercise.desc ?? "")")
}
}
I've got the entity relationship set as ordered, but I've also tried unordered with .allObjects instead of .array. This clearly isn't the problem as it's the array iterableExercises that's not correctly being reset?
EDIT: to reproduce, here's all the code you need and some screenshots of the CoreData model.
import SwiftUI
import CoreData
class ViewModel: ObservableObject {
let container: NSPersistentCloudKitContainer
#Published var savedWorkouts: [WorkoutEntity] = []
#Published var savedExercises: [ExerciseEntity] = []
// MARK: INIT
init() {
container = NSPersistentCloudKitContainer(name: "mre")
container.loadPersistentStores { description, error in
if let error = error {
print("Error loading CoreData: \(error)")
}
}
fetchWorkoutEntities()
fetchExerciseEntities()
}
// MARK: FETCHERS
func fetchWorkoutEntities() {
let request = NSFetchRequest<WorkoutEntity>(entityName: "WorkoutEntity")
do {
savedWorkouts = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching WorkoutEntity: \(error)")
}
}
func fetchExerciseEntities() {
let request = NSFetchRequest<ExerciseEntity>(entityName: "ExerciseEntity")
do {
savedExercises = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching ExerciseEntity: \(error)")
}
}
// MARK: SAVE
func saveData() {
do {
try container.viewContext.save()
fetchWorkoutEntities()
fetchExerciseEntities()
} catch let error {
print("Error saving: \(error)")
}
}
// MARK: ADDERS
func addWorkout(name: String) {
let _ = WorkoutEntity(context: container.viewContext)
saveData()
}
func addExerciseToWorkout(workout: WorkoutEntity, name: String) {
let newExercise = ExerciseEntity(context: container.viewContext)
newExercise.name = name
workout.addToExercises(newExercise)
saveData()
}
// MARK: DELETERS
func deleteWorkout(workout: WorkoutEntity) {
container.viewContext.delete(workout)
saveData()
}
func deleteExercise(exercise: ExerciseEntity) {
container.viewContext.delete(exercise)
saveData()
}
// MARK: TODO: UPDATERS
}
struct ContentView: View {
#StateObject var data = ViewModel()
var body: some View {
VStack {
Button {
data.addWorkout(name: "workout")
data.addExerciseToWorkout(workout: data.savedWorkouts[0], name: "[exercisename]")
} label: {
Text("Click ONCE to add workout to work with")
}
Spacer()
if let iterableExercises = data.savedWorkouts[0].exercises?.array as? [ExerciseEntity] {
ForEach(iterableExercises) { exercise in
Button {
data.deleteExercise(exercise: exercise)
} label: {
Text("Click to delete \(exercise.name ?? "") AFTER DELETING IF THIS STILL SHOWS BUT DOESN'T SHOW THE EXERCISE NAME THEN IT'S BROKEN")
}
}
}
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
screenshots of model
I’m not sure if this is the ONLY solution as #malhal gave quite an extensive and seemingly useful response.
But I came across a much easier and immediate fix, within my original solution. The inverse relationships must be specified. Doing this resolved all issues.
We don't use view model objects in SwiftUI. You need to learn the View struct and property wrappers which gives the consistency and efficiency of value types with the benefits of reference types. The property wrapper for core data is #FetchRequest which invalidates the View when the results change. It's also a DynamicProperty (which is how it gets the context from the environment) that you can use it directly without the property wrapper syntax which allows you to use a param in a predicate, in your case to do fetch the one-to-many relation, e.g.
struct WorkoutView: View {
private var fetchRequest: FetchRequest<Exercise>
private var exercices: FetchedResults<Exercise> {
fetchRequest.wrappedValue
}
init(workout: Workout) {
let sortAscending = true
let sortDescriptors = [SortDescriptor(\Exercise.timestamp, order: sortAscending ? .forward : .reverse)]
fetchRequest = FetchRequest(sortDescriptors: sortDescriptors, predicate: NSPredicate(format: "workout = %#", workout), animation: .default)
}
var body: some View {
List(exercises) { exercise in
ExerciseView(exercise: exercise)
}
}
}
For creating the NSPersistentContainer check out the Xcode App template with Core Data checked. Looks like this:
#main
struct TestApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
The reason it is not an #StateObject is we don't want to invalidate this body when it changes and we need it to be init for previewing which is a different singleton.
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
... see template
That other code in your view model class can be moved to NSManagedObject and NSManagedObjectContext extensions. Use the Editor menu to generate the NSManagedObject extension for the model, the files need tidying up though and make sure use extension is selected for the entity.

SwiftUI: unable to access #EnvironmentObject in view model

I'm building an app using SwiftUI / Combine and trying to do so in an MVVM pattern. I'm getting a little confused as to how best to expose certain properties and in particular, in relation to the Core Data implementation.
In the main app file, I have set up an environmnt object as follows (I'll come to why later):
struct Football_GuruApp: App {
let persistenceController = PersistenceController.shared
#StateObject var favouritePlayers = FavouritePlayersViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(favouritePlayers)
}
}
}
The app has 3 main views:
ContentView: this contains a TabView with 2 subviews: FetchedResultsView() and FavouritesView()
FetchedResultsView: this contains a subview FetchedPlayers() which looks like this:
struct FetchedPlayersView: View {
#EnvironmentObject var fetchedResultsVM: FetchedResultsViewModel
var body: some View {
Section(header: Text("Players")) {
ForEach(fetchedResultsVM.fetchedPlayers, content: { player in
PlayerView(player: player)
})
if fetchedResultsVM.playersExpandable {
MoreResultsButton(action: fetchedResultsVM.getMorePlayers, buttonTitle: "More players")
}
}
}
}
And finally FavouritesView:
struct FavouritesView: View {
#EnvironmentObject var favouritePlayersVM: FavouritePlayersViewModel
var context = PersistenceController.shared.container.viewContext
var body: some View {
List {
ForEach(context.fetchAll(PlayerCD.self)) { player in
PlayerView(player: PlayerViewModel.mapFromCoreData(player: player))
}
}
}
}
Within the PlayerView() (subview of FetchedPlayersView) we have a button:
FavouritesButton(playerViewModel: player)
When tapped we set a property on the PlayerViewModel to true:
playerViewModel.favourite = true
And then a didSet method on PlayerViewModel triggers the player to be stored to core data:
var favourite = false {
didSet {
self.mapToCoreData()
}
}
func mapToCoreData() {
let storedPlayer = PlayerCD(context: context)
storedPlayer.id = self.id
storedPlayer.firstName = self.firstName
storedPlayer.secondName = self.secondName
try? PersistenceController.shared.container.viewContext.save()
favouritePlayersVM.updateFavourites()
}
We have the following env object on the PlayerViewModel
#EnvironmentObject var favouritePlayersVM: FavouritePlayersViewModel
Finally, FavouritePlayersViewModel looks like this:
class FavouritePlayersViewModel: ObservableObject {
#Published var players = [PlayerViewModel]()
func updateFavourites() {
let context = PersistenceController.shared.container.viewContext
let savedPlayers = context.fetchAll(PlayerCD.self)
self.players = [PlayersViewModel]()
savedPlayers.forEach {
players.append(PlayerViewModel.mapFromCoreData(player: $0))
}
}
}
So the idea is that when the button is tapped, we store to core data and then at the same time we update the players list on FavouritePlayersViewModel which should then update the FavouritesView with the latest players. However, clearly I am struggling with the implementation of the environment object. I had thought that by exposing this at the root of the app, I would be able to access everywhere, but I guess that as PlayerViewModel, for example, is not a direct descendent, I can't access (as I'm getting a crash).
Perhaps using the env object is not the best fit here, but I'm struggling to work out how best to have it so that I can update the FavouritesViewModel players list from the PlayerViewModel whilst using the same instance of this FavouritesViewModel to update the FavouritesView.
By the same token, I'm also not able to access the NSManagedObjectContext that I set as #Environment in the root file in the view models either which is why I'm using the singleton of the persistent container to store my core data, which is not what I really wanted to do.

How to use a picker on CoreData relationships in SwiftUI

G'day everyone,
I'm trying to work out how CoreData relationships can work with UI elements like pickers.
At the moment I have a 3 view app (based on the Xcode boilerplate code) which displays a list of parent entities, which have children which have children. I want a picker to select which grandchild a child entity should refer to.
At the moment I have two funny side effects:
When I run the app as a preview (so there is pre-populated data... this sample code will break without the data in place),
the selected grandchild in the picker is the grandchild of the first
child, irrespective of which child you're dropped into in the first
view.
When I drop back and pick another child, now the picked grabs the correct initial selection from the child entity
When I select a child and "save" that, the value in the child summary does not change, until I click another child at which point the value changes before the transition to the modal view.
I am clearly missing something in my understanding of the sequence of events when presenting modals in SwiftUI... can any what shed any light on what I've done wrong?
Here's a video to make this more clear:
https://github.com/andrewjdavison/Test31/blob/main/Test31%20-%20first%20click%20issue.mov?raw=true
Git repository of the sample is https://github.com/andrewjdavison/Test31.git, but in summary:
Data Model:
View Source:
import SwiftUI
import CoreData
struct LicenceView : View {
#Environment(\.managedObjectContext) private var viewContext
#Binding var licence: Licence
#Binding var showModal: Bool
#State var selectedElement: Element
#FetchRequest private var elements: FetchedResults<Element>
init(currentLicence: Binding<Licence>, showModal: Binding<Bool>, context: NSManagedObjectContext) {
self._licence = currentLicence
self._showModal = showModal
let fetchRequest: NSFetchRequest<Element> = Element.fetchRequest()
fetchRequest.sortDescriptors = []
self._elements = FetchRequest(fetchRequest: fetchRequest)
_selectedElement = State(initialValue: currentLicence.wrappedValue.licenced!)
}
func save() {
licence.licenced = selectedElement
try! viewContext.save()
showModal = false
}
var body: some View {
VStack {
Button(action: {showModal = false}) {
Text("Close")
}
Picker(selection: $selectedElement, label: Text("Element")) {
ForEach(elements, id: \.self) { element in
Text("\(element.desc!)")
}
}
Text("Selected: \(selectedElement.desc!)")
Button(action: {save()}) {
Text("Save")
}
}
}
}
struct RegisterView : View {
#Environment(\.managedObjectContext) private var viewContext
#State var showModal: Bool = false
var currentRegister: Register
#State var currentLicence: Licence
init(currentRegister: Register) {
currentLicence = Array(currentRegister.licencedUsers! as! Set<Licence>)[0]
self.currentRegister = currentRegister
}
var body: some View {
VStack {
List {
ForEach (Array(currentRegister.licencedUsers! as! Set<Licence>), id: \.self) { licence in
Button(action: {currentLicence = licence; showModal = true}) {
HStack {
Text("\(licence.leasee!) : ")
Text("\(licence.licenced!.desc!)")
}
}
}
}
}
.sheet(isPresented: $showModal) {
LicenceView(currentLicence: $currentLicence, showModal: $showModal, context: viewContext )
}
}
}
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Register.id, ascending: true)],
animation: .default)
private var registers: FetchedResults<Register>
var body: some View {
NavigationView {
List {
ForEach(registers) { register in
NavigationLink(destination: RegisterView(currentRegister: register)) {
Text("Register id \(register.id!)")
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
[1]: https://i.stack.imgur.com/AfaNb.png
I didn't really understand this
• selected grandchild in the picker is the grandchild of the first child, irrespective of which child you're dropped into in the first view.
• When I drop back and pick another child, now the picked grabs the correct initial selection from the child entity
Could you attach a video that represents a problem?
But I can give you a solution to the preview problem and the second one.
Preview
If you use preview with Core Data, you need to use a viewContextcreated with MockData and pass it to your View. Here I provide a generic code, that can be modified for each of your views:
In your Persistance struct (CoreData Manager) declare a variable preview with your preview Items:
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
// Here you create your Mock Data
let newItem = Item(context: viewContext)
newItem.yourProperty = yourValue
do {
try viewContext.save()
} catch {
// error handling
}
return result
}()
Make sure it has inMemory: Bool in its init, as it is responsible for separating real viewContext and previewContext:
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "TestCD")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
Create Mock Item from your viewContext and pass it to preview:
struct YourView_Previews: PreviewProvider {
static var previews: some View {
let context = PersistenceController.preview.container.viewContext
let request: NSFetchRequest<Item> = Item.fetchRequest()
let fetchedItems = try! context.fetch(request)
YourView(item: fetchedItems)
}
}
If you use #FetchRequest and #FetchedResults it makes it easier, as they will do creating and fetching objects for you. Just implement a preview like this:
struct YourView_Previews: PreviewProvider {
static var previews: some View {
YourView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
Here is Persistence struct created by Xcode at the moment of the project initialization:
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
let item = Item(context: viewContext)
item.property = yourProperty
do {
try viewContext.save()
} catch {
}
return result
}()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "TestCD")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
Second problem
Core Data objects are built with classes, so their type is a reference. When you change a property is a class it doesn't notifiy the view struct to redraw with a new value. (exception is classes, that are created to notify about changes.)
You need to explicitly tell your RegisterView struct to redraw itself after you dismiss your LicenceView. You can do it by creating one more variable in your RegisterView - #State var id = UUID(). Then attach an .id(id) modifier at the end of your VStack
VStack {
//your code
}.id(id)
Finally, create a function viewDismissed which will change the id property in your struct:
func viewDismissed() {
id = UUID()
}
Now, attach this function to your sheet with an optional parameter onDismiss
.sheet(isPresented: $showModal, onDismiss: viewDismissed) {
LicenceView(currentLicence: $currentLicence, showModal: $showModal, context: viewContext )
}
OK. Huge vote of thanks to Lorem for getting me to the answer. Thanks too for Roma, but it does turn out that his solution, whilst it worked to resolve one of my key problems, does introduce inefficiencies - and didn't resolve the second one.
If others are hitting the same issue I'll leave the Github repo up, but the crux of it all was that #State shouldn't be used when you're sharing CoreData objects around. #ObservedObject is the way to go here.
So the resolution to the problems I encountered were:
Use #ObservedObject instead of #State for passing around the CoreData objects
Make sure that the picker has a tag defined. The documentation I head read implied that this gets generated automatically if you use ".self" as the id for the objects in ForEach, but it seems this is not always reliable. so adding ".tag(element as Element?)" to my picker helped here.
Note: It needed to be an optional type because CoreData makes all the attribute types optional.
Those two alone fixed the problems.
The revised "LicenceView" struct is here, but the whole solution is in the repo.
Cheers!
struct LicenceView : View {
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var licence: Licence
#Binding var showModal: Bool
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Element.desc, ascending: true)],
animation: .default)
private var elements: FetchedResults<Element>
func save() {
try! viewContext.save()
showModal = false
}
var body: some View {
VStack {
Button(action: {showModal = false}) {
Text("Close")
}
Picker(selection: $licence.licenced, label: Text("Element")) {
ForEach(elements, id: \.self) { element in
Text("\(element.desc!)")
.tag(element as Element?)
}
}
Text("Selected: \(licence.licenced!.desc!)")
Button(action: {save()}) {
Text("Save")
}
}
}
}

Saving Item in CoreData Initiates Navigation

In my ContentView I have a FetchRequest<Project>. I navigate to ProjectView using a NavigationLink. From ProjectView I navigate to AddItemView using another NavigationLink. In AddItemView when I add an Item to the Project and call container.viewContext.save() the AddItemView automatically dismisses back to the ContentView.
My guess is that saving to CoreData updates the FetchRequest<Project> list which in turn updates the views, but I am not sure.
How can I save a new Item to the Project in CoreData and only navigate back to ProjectView and not ContentView?
To reproduce:
Create new Single View App and check Core Data and Host in CloudKit
In the .xcdatamodel delete the default Entity and replace it with an Entity called Project which has attributes date: Date and title: String and an Entity called Item which has an attribute name: String. Give the Project a relationship called items (to type Item) and choose “to many” on the right. Give the Item a relationship called project that is the inverse of items.
Replace the code in Persistence.swift with this:
// Persistence.swift
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "CoreDataBug")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
Copy ContentView
// ContentView.swift
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
let projects: FetchRequest<Project>
init() {
projects = FetchRequest<Project>(entity: Project.entity(), sortDescriptors: [
NSSortDescriptor(keyPath: \Project.date, ascending: false)
])
}
var body: some View {
NavigationView {
List {
ForEach(projects.wrappedValue) { project in
NavigationLink(destination: ProjectView(project: project)) {
Text(project.title ?? "Title")
}
}
}
.navigationTitle("Projects")
.toolbar {
ToolbarItem(placement: ToolbarItemPlacement.navigationBarTrailing) {
Button {
withAnimation {
let project = Project(context: moc)
let now = Date()
project.date = now
project.title = now.description
try? moc.save()
}
} label: {
Label("Add Project", systemImage: "plus")
}
}
}
}
}
}
Create ProjectView.swift and copy this:
// ProjectView.swift
import SwiftUI
struct ProjectView: View {
#ObservedObject var project: Project
var items: [Item] {
project.items?.allObjects as? [Item] ?? []
}
var body: some View {
List {
ForEach(items) { item in
Text(item.name ?? "")
}
}
.toolbar {
ToolbarItem(placement: ToolbarItemPlacement.navigationBarTrailing) {
NavigationLink(destination: AddItemView(project: project)) {
Label("Add Item", systemImage: "plus")
}
}
}
}
}
Create AddItemView.swift and copy this:
import SwiftUI
// AddItemView.swift
import SwiftUI
struct AddItemView: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var moc
let project: Project
#State private var selectedName: String = ""
var body: some View {
TextField("Type name here", text: $selectedName)
.navigationTitle("Add Item")
.navigationBarItems(trailing: Button("Add") {
let ingestion = Item(context: moc)
ingestion.project = project
ingestion.name = selectedName
try? moc.save()
presentationMode.wrappedValue.dismiss()
})
}
}
Run the app. Click the plus on the top right. Click the project that just slid in. In the ProjectView click the plus on the top right again. Type a name in the TextField and click add on the top right. When the AddItemView is dismissed it probably went back to ContentView. If not add another item to the project.

What is the proper way of adding a "show user location" button for an MKMapView in SwiftUI?

I'm trying to create a SwiftUI view with an MKMapView and a button to navigate to the user location. Below is the entirety of my code. I created a view called MapView which is conforms to UIViewRepresentable to hold my MKMapView. I have a location button that, on tapping, sets shouldNavigateToUserLocation to true. This causes the UI to reload, and my MapView navigates to the user location if shouldNavigateToUserLocation is true. Then, it sets shouldNavigateToUserLocation to false, so that the MapView is not constantly moving to the user location on ever state change.
This approach seems to work when running on real devices, but I get the warning "Modifying state during view update, this will cause undefined behavior." on line 87, which is shouldNavigateToUserLocation = false. This is understandable, but my question is, how can I avoid this? I can't seem to find a way to restructure my code so I'm not violating the rule of not modifying state during a view update, while still having the map navigate to the user location when and only when the user presses the button.
I have tried a couple of different approaches, but I mainly get stuck with the problem of neither my MapView nor my Controller class actually having direct access to the MKMapView. I understand why that is in SwiftUI, but it really limits what I can do.
Here is the entirety of my code:
import SwiftUI
import MapKit
struct ContentView: View {
#State var currentlyDisplayingLocationAuthorizationRequest = false
#State var shouldNavigateToUserLocation = false
let locationManager = CLLocationManager()
var body: some View {
ZStack {
MapView(currentlyDisplayingLocationAuthorizationRequest: $currentlyDisplayingLocationAuthorizationRequest,
shouldNavigateToUserLocation: $shouldNavigateToUserLocation)
HStack {
Spacer()
VStack {
Spacer()
Button(action: {
self.checkForLocationAuthorizationAndNavigateToUserLocation()
}) {
Image(systemName: "location")
.imageScale(.large)
.accessibility(label: Text("Locate Me"))
.padding()
}
.background(Color.gray)
.cornerRadius(10)
.padding()
}
}
}
.onAppear {
self.shouldNavigateToUserLocation = true
}
}
func checkForLocationAuthorizationAndNavigateToUserLocation() {
currentlyDisplayingLocationAuthorizationRequest = false
if CLLocationManager.authorizationStatus() == .notDetermined {
print("location authorization not determined")
currentlyDisplayingLocationAuthorizationRequest = true
locationManager.requestWhenInUseAuthorization()
return
}
shouldNavigateToUserLocation = true
}
}
struct MapView: UIViewRepresentable {
#Binding var currentlyDisplayingLocationAuthorizationRequest: Bool
#Binding var shouldNavigateToUserLocation: Bool
let locationManager = CLLocationManager()
func makeUIView(context: Context) -> MKMapView {
let map = MKMapView()
map.delegate = context.coordinator
map.showsUserLocation = true
return map
}
func updateUIView(_ uiView: MKMapView, context: Context) {
if !currentlyDisplayingLocationAuthorizationRequest && shouldNavigateToUserLocation {
moveToUserLocation(map: uiView)
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// MARK: Location -
private func moveToUserLocation(map: MKMapView) {
guard let location = locationManager.location else { return }
let region = MKCoordinateRegion(center: location.coordinate,
span: MKCoordinateSpan(latitudeDelta: 0.02,
longitudeDelta: 0.02))
map.setRegion(region, animated: true)
shouldNavigateToUserLocation = false
}
// MARK: Coordinator -
final class Coordinator: NSObject, MKMapViewDelegate {
var control: MapView
init(_ control: MapView) {
self.control = control
}
}
}
It's not exactly what you are searching for, but you should take a look at my recent answer to another related question (How to add a move back to user location button in swiftUI?). I think it can help you, or others if they are facing the same problem.

Resources