WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

Core Data - MapKit - Day 78 Question

Forums > 100 Days of SwiftUI

I have completed the Day 77/78 challenges of #100DaysOfSwiftUI but really struggled getting Core Data and MapKit to work.

In my Core Data entity, I have stored an id (UUID), name (string), imagepath (string), latitude (double), and longitude (double). I also have a computed property 'coordinate' in an extension that changes latitude and longitude over to a CLLocationCoordinate2D.

I am able to do everything I want using Core Data with the exception of passing the latitude and longitude into the 'annotationItems' portion of Map. I understand it requires an array of items and I only have one item. The only way I could get it to work was to create a struct for my Location and then use the onAppear to load my one location into a Location array so I could then use it with 'annotationItems'. Keep in mind, this is a detail screen and information is being pass in with only one item.

This seems like I am having to use additional steps as the Location struct is basically the same as my Core Data entity that is being passed into the screen but I couldn't figure out how to make the Core Data Entity be the array for 'annotationItems'

I have listed my DetailView below (it has not been refactored to MVVM yet). Could someone please let me know if I am doing this wrong or if this is the way it has to be done to make it work?

Thanks in advance

The full project is at my Github: [https://github.com/Icemonster13/Milestone-Day77-Pictures]

import SwiftUI
import MapKit

struct Location: Identifiable {
    let id = UUID()
    let name: String
    let coordinate: CLLocationCoordinate2D
}

struct DetailView: View {

    // MARK: - PROPERTIES
    let picture: PictureEntity
    @State private var mapRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 51.5, longitude: -0.12), span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2))
    @State private var locations: [Location] = []

    // MARK: - BODY
    var body: some View {
        VStack {
            if let imageToLoad = loadImage(fileName: picture.notoptImage) {
                ZStack(alignment: .bottomTrailing) {
                    Image(uiImage: imageToLoad)
                        .resizable()
                        .scaledToFill()
                        .frame(height: 200)
                    .clipShape(RoundedRectangle(cornerRadius: 10))

                    Text(picture.notoptName)
                        .font(.headline)
                        .padding(.horizontal)
                        .frame(height: 30)
                        .background(.thinMaterial)
                        .cornerRadius(10)
                        .offset(x: -5, y: -5)
                }
            }

            Section {
                Map(coordinateRegion: $mapRegion, annotationItems: locations) { location in
                    MapMarker(coordinate: location.coordinate)
                }
            } header: {
                Text("Where was this picture taken...")
                    .padding(.top)
            }

            Spacer()
        }
        .padding(.horizontal)
        .navigationTitle("Picture Details")
        .navigationBarTitleDisplayMode(.inline)
        .onAppear {
            mapRegion = MKCoordinateRegion(center: picture.coordinate, span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2))
            locations = [
                Location(name: picture.notoptName, coordinate: .init(latitude: picture.latitude, longitude: picture.longitude))
            ]
        }
    }
}

   

Still hoping someone may be able to provide some assistance with this question...

   

Your approach looks good to me. You store in Core Data the coordinate in its simplest, most universal format, rather than in the format required by a particular view, and in a string datatype which has the advantage of being a datatype that Core Data can store without any transformation. Getting the CLLocationCoordinate2D via a computed property in the PictureEntity class also seems like as good an approach as any.

You can store in Core Data an attribute whose type does not appear in the "type" popup menu in the model editor. You select Transformable as the type, and then you define a method for transforming the desired type into binary Data type. For details, see Donny Wals' book on Core Data. However, this would be no more efficient than your approach because Core Data has to convert between types Data and CLLocationCoordinate2D every time it reads or writes that object. And it has the disadvantage of embedding in your database design the CLLocationCoordinate2D type that may not be relevant to something you want to do in future.

   

Thanks @bobstern.

The confusing part for me is why I can't directly use the computed property 'coordinate' from the PictureEntity class extension instead of having to create a new struct to house the location data. In my mind, Location.coordinate is the same as picture.coordinate.

I'm sure this is me just getting things twisted in my noggin but I really can't wrap my brain around this one.

   

The answer relates to the SwiftUI Map and MapMarker views, not to Core Data.

Yes, PictureEntity.coordinate is the same type as Location.coordinate. However, the Location object is different because it conforms to Identifiable, and that's what Map requires.

As you undoubtedly saw when you consulted the Apple documentation for MapMarker, the annotationItems parameter of Map must be a type that conforms to Identifiable. Creating a struct having an id property, as you've done with Location, is the standard way to create an Identifiable object and is the example shown in the MapMarker documentation.

The reason for this is similar to why ForEach requires an id. Map contemplates that you might allow the user to delete or move multiple MapMarker annotationItems independent of their index in the annotationItems array.

You could simplify your code somewhat.

Location.name is superfluous, so delete it.

You can move the location instance from the DetailView to the PictureEntity extension, which seems more elegant to me because you might want to use an identifiable counterpart of PictureEntity.coordinate in other views.

To do this, add to the PictureEntity extension:

    var location: Location {
        Location(id: self.id!, coordinate: self.coordinate)   
    }

(You could use id:UUID() instead, but why not use the id property of the PictureEntity object.)

Then, in DetailView, replace:

Map(coordinateRegion: $mapRegion, annotationItems: locations) { location in
     MapMarker(coordinate: location.coordinate)
}

with:

Map(coordinateRegion: $mapRegion, annotationItems: [picture.location]) { loc in
     MapMarker(coordinate: loc.coordinate)
}

Now you can delete from DetailView:

@State private var locations

and locations=... in .onAppear

Lastly, since location is merely the coordinate with an added id property, it might be clearer to rename the Location struct as CoordinateIdentifiable or similar.

   

Hacking with Swift is sponsored by Emerge

SPONSORED Why are Swift reference types bad for app startup time, and what’s the performance cost of protocol conformances? That’s just a couple of the topics you can learn about on the Emerge blog — written by the app performance experts behind Emerge’s advanced app optimization and monitoring tools, based on their experience of working at companies like Apple, Airbnb, Snap, and Spotify.

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.