Swift: CoreData Conforming to MKAnnotation - swift

I am attempting to implement my custom CoreData Carpark entity to conform to MKAnnotation like how we could make a class object conform to MKAnnotation.
I adapted my implementation from the following posts: this, this.
My implementation so far:
//At Carpark+CoreDataClass.swift
import Foundation
import CoreData
import Mapkit
#objc(Carpark)
public class Carpark: NSManagedObject, MKAnnotation {
var coordinate: CLLocationCoordinate2D {
return CLLocationCoordinate2D(latitude: Double(self.lat), longitude: Double(self.long))
}
var title: String {
return self.carparkName
}
}
//At Carpark+CoreDataProperties.swift
import Foundation
import CoreData
import Mapkit
extension Carpark {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Carpark> {
return NSFetchRequest<Carpark>(entityName: "Carpark")
}
#NSManaged public var carparkName: String?
#NSManaged public var sundayAndPublicHolRate: String?
#NSManaged public var saturdayRate: String?
#NSManaged public var weekdayRate1: String?
#NSManaged public var weekdayRate2: String?
#NSManaged public var lat: Double
#NSManaged public var long: Double
}
//At VC where I addAnnotations
var allCarparks: [Carpark]?
var nearbyCarparks = [Carpark]()
var searchedCarpark: Carpark!
func filterCarparksWithinRadius() {
var allAnnotations = [Carpark]()
let searchRadius = UserDefaults.standard.object(forKey: Constants.UserDefaults.SearchRadius) as! Double
let searchedCarparkLocation = CLLocation(latitude: searchedCarpark.lat, longitude: searchedCarpark.long)
allCarparks?.forEach({ (carpark) in
let carparkLocation = CLLocation(latitude: carpark.lat, longitude: carpark.long)
let distance = carparkLocation.distance(from: searchedCarparkLocation)
if distance <= searchRadius && distance != 0 {
nearbyCarparks.append(carpark)
allAnnotations.append(carpark)
}
})
allAnnotations.append(searchedCarpark)
mapView.addAnnotations(allAnnotations as! [MKAnnotation]) //Problem here
let zoomCoordinate = CLLocationCoordinate2D(latitude: searchedCarpark.lat, longitude: searchedCarpark.long)
zoomToLocation(coordinate: zoomCoordinate)
}
The above code would throw the error:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Carpark coordinate]:
unrecognized selector sent to instance 0x60400009e690'
Is there a way that we could make CoreData conform to MKAnnotation like how we could do it for a class object? Similar to something like this?:
class Carpark: NSObject, MKAnnotation {
var coordinate: CLLocationCoordinate2D
var title: String?
var carparkName: String?
var lat: Double?
var long: Double?
init(carparkName: String?, lat: Double?, long: Double?) {
self.title = carparkName
self.carparkName = carparkName
self.lat = lat
self.long = long
self.coordinate = CLLocationCoordinate2DMake(lat!, long!)
}
}
Any advice is much appreciated, thanks.

You need to make your coordinate property public to be seen outside your module (outside like in the MapView framework). Maybe this is also the reason you need to cast your array.
This is my definition
public class Location: NSManagedObject, MKAnnotation {
public var coordinate: CLLocationCoordinate2D {
return CLLocationCoordinate2DMake(latitude, longitude)
}
//....more code
}
And in my view controller I have a property
var locations = [Location]()
And I fetch into it and adds to map view like this
private func updateLocations() {
mapView.removeAnnotations(locations)
let entity = Location.entity()
let fetchRequest = NSFetchRequest<Location>()
fetchRequest.entity = entity
do {
locations = try managedObjectContext.fetch(fetchRequest)
} catch {
fatalCoreDataError(error)
}
mapView.addAnnotations(locations)
}

I managed to solve it by setting Codegen to Manual/None, and properties are then accessible.

Related

How to map model for NSManagedObject?

When I try to do this, the model is stored in the NSManagedObjectContext if I use the context, and without it it throws an error, but I'm not expecting the same result.
Is there an easy way to implement this?
class WordDal: NSManagedObject {
#nonobjc public class func fetchRequest() -> NSFetchRequest<WordDal> {
return NSFetchRequest<WordDal>(entityName: "WordDal")
}
#NSManaged public var word: String?
#NSManaged public var uuid: UUID?
}
struct WordPresentation {
let word: String
let uuid: UUID
}
func mappingNSManagedObject(_ wordPresentation: WordPresentation) -> WordDal {
let model = WordDal()
model.uuid = wordPresentation.uuid
model.word = wordPresentation.word
return model
}
Consider to redesign your model to have computed property for the new wrapper type that transforms the property value to and from the wrapper value.
Implementing a computed property in a Swift Core Data model is often a clear, more intuitive way to achieve what you need.
Here is an example implementation:
struct WordPresentation {
let word: String
let uuid: UUID }
class WordDal: NSManagedObject {
#nonobjc public class func fetchRequest() -> NSFetchRequest<WordDal> {
return NSFetchRequest<WordDal>(entityName: "WordDal")
}
#NSManaged public var word: String?
#NSManaged public var uuid: UUID?
var wordPresentation : WordPresentation {
get {
return WordPresentation(word: self.word, uuid: self.uuid)
}
set {
self.word = newValue.name
self.uuid = newValue.id
}
}
}
I solved the problem like this (I don't know why I put it off and didn't understand right away):
class WordDal: NSManagedObject {
#nonobjc public class func fetchRequest() -> NSFetchRequest<WordDal> {
return NSFetchRequest<WordDal>(entityName: "WordDal")
}
#NSManaged public var word: String?
#NSManaged public var uuid: UUID?
}
struct WordPresentation {
let word: String
let uuid: UUID
}
func removeFromStorage(by uuid: UUID) {
getDataFromStorage { [weak self] objects in
guard let self = self else { return }
if let objectForRemove = objects.first(where: { $0.uuid == uuid }) {
self.coreDataStack.mainContext.delete(objectForRemove)
self.coreDataStack.saveContext(self.managedObjectContext)
}
}
}
I'm creating a presentation level model with UUID!
And I delete only on him himself UUID.
Now I can walk both ways.

How to properly use 'init' to prevent the error: "property initializers run before 'self' is available"

I am creating an application that receives the localization information from an Apple Watch and with the coordinates that I receive, I want to create a MapKit annotation on a, I also show the location of the Phone.
But when I try to create the array to hold the information for the annotation the error
Cannot use instance member 'connectivity' within property initializer; property initializers run before 'self' is available
The class that is giving me the error is the following:
MapView.swift
import MapKit
import SwiftUI
// Struct to display devices location
struct Place: Identifiable {
let id = UUID()
let name: String?
let coordinate: CLLocationCoordinate2D?
init(name: String? = nil,
coordinate: CLLocationCoordinate2D? = nil) {
self.name = name
self.coordinate = coordinate
}
}
struct MapView: View {
// Loads the ViewModels for the map
#StateObject private var viewModel = MapViewModel()
// Loads location of apple watch
#StateObject private var connectivity = Connectivity()
// Start tracking
#State var toggleTracking = true
#State var showTemporaryPopUp = true
// contains the location coordinates of device, to draw where is located on the map
var annotations = [
Place(name: "Apple Watch", coordinate: CLLocationCoordinate2D(latitude: connectivity.receivedLat, longitude: connectivity.receivedLong))
]
var body: some View {
ZStack(alignment: .top) {
// Displays Map
Map(coordinateRegion: $viewModel.region, showsUserLocation: true)
.ignoresSafeArea()
.accentColor(Color(.systemPink))
}
The part that is giving me the error is this one:
// contains the location coordinates of device, to draw where is located on the map
var annotations = [
Place(name: "Apple Watch", coordinate: CLLocationCoordinate2D(latitude: connectivity.receivedLat, longitude: connectivity.receivedLong))
]
And here is the class that contains the latitude and longitude, and the source of the error.
Connectivity.swift
import Foundation
import WatchConnectivity
class Connectivity: NSObject, ObservableObject, WCSessionDelegate {
#Published var receivedText = ""
#Published var receivedLat: Double = 0.0
#Published var receivedLong: Double = 0.0
override init() {
super.init()
if WCSession.isSupported() {
let session = WCSession.default
session.delegate = self
session.activate()
}
}
// This function receives the information sent from the watch,
//and then sets the published vars with the information
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Void) {
DispatchQueue.main.async {
if let lat = message["latitude"] as? Double, let lon = message["longitud"] as? Double {
self.receivedText = "\(lat),\(lon)"
self.receivedLat = lat
self.receivedLong = lon
replyHandler(["Response": "Be excellent to each other"])
}
}
}
}
The first thing that I tried is using the 'lazy' var, like this:
// contains the location coordinates of device, to draw where is located on the map
lazy var annotations = [
Place(name: "Apple Watch", coordinate: CLLocationCoordinate2D(latitude: connectivity.receivedLat, longitude: connectivity.receivedLong))
]
But then the next error appears when I try to get the coordinates:
Cannot use mutating getter on immutable value: 'self' is immutable
So this solution is out of the question.
And the other common solution is to use the init, the way I am doing it is the following way, on my Connectivity.swift file:
override init() {
super.init()
if WCSession.isSupported() {
let session = WCSession.default
session.delegate = self
session.activate()
}
self.receivedLat = 0.0
self.receivedLong = 0.0
}
But the error still exist, and I don't know some other way to change the init.
Is there other way to do it?
Sources:
Hudson, P. (2020). Hacking with watchOS
Make annotations a computed property -- that way, it has access to connectivity and it will get re-computed with the View re-renders because a #Published property of Connectivity has changed:
var annotations : [Place] {
[
Place(name: "Apple Watch", coordinate: CLLocationCoordinate2D(latitude: connectivity.receivedLat, longitude: connectivity.receivedLong))
]
}

How do Views relate/loosely couple to Annotations? Annotationsarray I got, Views I need

SryImNew:
(lets say I want to Display gliphXYZ if title == "home")
"Annotation views are loosely coupled to a corresponding annotation object" https://developer.apple.com/documentation/mapkit/mkannotationview
How to couple them loosely, and where?
Or do I already have multiple MKAnnotationView automatically, if so how to address them & their properties?
My Case
I have running: An app that shows multiple pins for locations on a map
by
rawArrayWithFormat=[[namestring,lat,long][namestring,lat...]]
class Place: NSObject, MKAnnotation {
var title: String?
var subtitle: String?
var latitude: Double
var longitude:Double
var coordinate: CLLocationCoordinate2D {
return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
init(latitude: Double, longitude: Double) {
self.latitude = latitude
self.longitude = longitude
}
Function that returns Array of [Place] by iterating through rawArray
func getMapAnnotations() -> [Place] {
var annotations:Array = [Place]()
for item in rawArray {
let lat = (item[1] as AnyObject) as! Double
let long = (item[2] as AnyObject) as! Double
let annotation = Place(latitude: lat, longitude: long)
annotation.title = (item[0] as AnyObject) as? String
annotations.append(annotation)
}
return annotations
And then in ViewController
#IBOutlet weak var mapView: MKMapView!
...
finally I call in override func viewDidLoad{
mapView.addAnnotations(getMapAnnotations())

Realm map clustering

To create your own data to present in realm map you need Model which looks like from example :
public class ABFRestaurantObject: Object {
public dynamic var businessId: String?
public dynamic var name: String?
public dynamic var address: String?
public dynamic var city: String?
public dynamic var state: String?
public dynamic var postalCode: String?
public dynamic var latitude: Double = 37.7859547
public dynamic var longitude: Double = -122.4024658
public dynamic var phoneNumber: String?
public let violations = List<ABFViolationObject>()
public let inspections = List<ABFInspectionObject>()
override public static func primaryKey() -> String? {
return "businessId"
}
}
What means this primaryKey ? Then how could I load my own data into it ? Of course omitting fact that you have to create your own model with custom propertiese ?
Thanks in advance!
EDIT
I am using Realm map kit clustering
I've created model :
class Test: Object {
public dynamic var id = 0
public dynamic var latitude = 45.0889
public dynamic var longitude = 54.1565
override static func primaryKey() -> String? {
return "id"
}
}
Then in my map controller I've added following functions and declarations :
var positions = try! Realm().objects(Test.self)
var position:Test!
override func viewDidLoad() {
super.viewDidLoad()
addNewVehicle()
populateMap()
}
func addNewPosition() {
let realm = try! Realm() // 1
try! realm.write { // 2
let newPos = Test() // 3
newPos.latitude = 50.060363
newPos.longitude = 19.939983
realm.add(newPos) // 5
self.position = newPos // 6
}
}
func populateMap() {
mapView.removeAnnotations(mapView.annotations) // 1
let positions = try! Realm().objects(Test.self) // 2
// Create annotations for each one
for pos in positions { // 3
let coord = CLLocationCoordinate2D(latitude: pos.latitude, longitude: pos.longitude);
let pin = MapPin(coordinate: coord, title: "asd", subtitle: "asd")
mapView.addAnnotation(pin) // 4
}
}
And at the end simple class fro creating pins :
class MapPin : NSObject, MKAnnotation {
var coordinate: CLLocationCoordinate2D
var title: String?
var subtitle: String?
init(coordinate: CLLocationCoordinate2D, title: String, subtitle: String) {
self.coordinate = coordinate
self.title = title
self.subtitle = subtitle
}
}
I would like to create few random pins and see if clustring works.
Atm i am getting an error in line :
safeObject->_coordinate = coordinate;
Thread 9: EXC_BAD_ACCESS(code=1,address=0x40)
po positions
Results<Test> (
[0] Test {
latitude = 50.060363;
longitude = 19.939983;
},
[1] Test {
latitude = 50.060363;
longitude = 19.939983;
},
[2] Test {
latitude = 50.060363;
longitude = 19.939983;
}
)
Then I've added as well to my AppDelegate.swift to the method didFinishLaunchingWithOptions :
let config = RLMRealmConfiguration.default()
config.schemaVersion = 1
config.migrationBlock = { (migration, oldSchemaVersion) in
}
RLMRealmConfiguration.setDefault(config)
Error: "Primary key property 'Test.id' has duplicate values after migration."
From the REALM documentation.
Override Object.primaryKey() to set the model’s primary key. Declaring
a primary key allows objects to be looked up and updated efficiently
and enforces uniqueness for each value. Once an object with a primary
key is added to a Realm, the primary key cannot be changed.
If you would like to create own Realm model simply create class with Object extension.
Example:
class User: Object {
dynamic var id = 0
dynamic var name = ""
dynamic var email = ""
}

Saving custom object as core data property

I am trying to save a custom class as a property of a core data object.
The Core Data object:
#objc(Safer)
class Safer: NSObject
{
#NSManaged var perakimFiles: [prayer]? //This line gives me an error saying that it cannot save a type that is not Obj. C
#NSManaged var titleEnglish: String?
#NSManaged var titleHebrew: String?
#NSManaged var parshaPerakim: [Int]?
#NSManaged var saferCode: String?
var titles: [[String]]
{
get
{
var ret = [[String]]()
var i = 0
for file in perakimFiles!
{
ret.append([file.title, "\(i+1)"])
i++
}
return ret
}
}
init(_ _saferCode: SefarimCodes)
{
super.init()
self.saferCode = _saferCode.rawValue
}
init(_perakimFiles: [prayer], _titleEnglish: String, _titleHebrew: String)
{
super.init()
self.perakimFiles = _perakimFiles
self.titleEnglish = _titleEnglish
self.titleHebrew = _titleHebrew
}
init(_perakimFiles: [prayer], _titleEnglish: String, _titleHebrew: String, _parshaPerakim: [Int])
{
super.init()
self.perakimFiles = _perakimFiles
self.titleEnglish = _titleEnglish
self.titleHebrew = _titleHebrew
self.parshaPerakim = _parshaPerakim
self.saferCode = setTorahSaferCode()!.rawValue
let config = self.configFromCode()
self.perakimFiles = config.perakimFiles
}
}
Here is the prayer class that I am trying to save in the core data object:
class prayer
{
var url: NSURL
var title: String
var detail: String?
init(initURL: NSURL, initTitle: String)
{
print("1")
print("2")
self.title = initTitle
print("3")
self.url = initURL
print("4")
}
init(initURL: NSURL, initTitle: String, initDetail: String)
{
self.url = initURL
self.title = initTitle
self.detail = initTitle
}
}
So what can I do to the prayer class to make it savable by the core data object? I need to also be able to use instances of the prayer class in other places of the code.
As mentioned, have your prayer class subclass NSObject and conform to NSCoding which requires two methods : -initWithCoder: and encodeWithCoder:
Once those are implemented, you can use NSKeyedArchiver/NSKeyedUnarchiver class methods to transform your objects into NSData objects and back, thus allowing you to store your objects as CoreData properties under the supported NSData type.
Let class prayer conforms to NSObject and NSCoding and see whether that addresses the error.