Swift - MapView duplicate annotations - swift

I'm querying data from FireStore and displaying the elements on a MapView, using a cursor I am able to paginate data.
This is my custom Annotation class:
final class PostAnnotation: NSObject, MKAnnotation {
var title: String?
var coordinate: CLLocationCoordinate2D
var username: String
var post: PostModel
var subtitle: String?
init(address: PostPlaceMark) {
self.title = address.title
self.coordinate = address.coordinate
self.username = address.username
self.post = address.post
self.subtitle = address.subTitle
}
}
I hit a button to load more data, for testing purposes my data pool is quite small around 7-10 elements. However, whenever I fetch/refresh data, I always get a duplicate of the previous group of annotations.
For instance, when my MapView first loads I have batch of annotations, hit my refresh button I have batch of annotations 1 and batch of annotations 2 but also have a duplicate of batch of annotations 1. Hit refresh again I have batch of annotations one x 3, batch of annotations two x 2 and just one instance of all data belonging to batch of annotations three.
I have managed to figure out where the issue lies but I have two extremes here:
One I keep getting duplicate annotations
Two the previous batch is
removed
The reason I am doing this is to combat the fact there may/can be a number of annotations surrounding a particular view. I have limited to where the issue and potential solution may lie.
private static func annotationsByDistributingAnnotationsContestingACoordinate(annotations: [PostAnnotation], constructNewAnnotationWithClosure ctor: annotationRelocator) -> [PostAnnotation] {
var newAnnotations = [PostAnnotation]()
let contestedCoordinates = annotations.map{ $0.coordinate }
let newCoordinates = coordinatesByDistributingCoordinates(coordinates: contestedCoordinates)
newAnnotations.removeAll()
for (i, annotation) in annotations.enumerated() {
let newCoordinate = newCoordinates[i]
let newPost = annotation.post
let newAnnotation = ctor(annotation, newCoordinate, newPost)
print(newAnnotation.post.location)
newAnnotations.forEach { (naac) in
annotations.forEach { (element) in
if newAnnotations.contains(where: {$0.post.postID == naac.post.postID}) {
let index = newAnnotations.firstIndex{$0.post.postID == naac.post.postID}
// newAnnotations.remove(at: index!)
print("removed")
// newAnnotations.removeAll(where: {$0.post.postID == naac.post.postID})
}
}
}
newAnnotations.append(newAnnotation)
}
let withoutDuplicates = Array(Set(newAnnotations))
return withoutDuplicates
}
I can detect if the element is already in the array, however, removing it, even the firstIndex causes me to lose the annotation completely.
This is how I add/update my annotations, in case you was wondering:
private func updateAnnotations(from mapView: MKMapView) {
let annotations = self.address.map(PostAnnotation.init)
let newAnnotations = ContestedAnnotationTool.annotationsByDistributingAnnotations(annotations: annotations) {
(oldAnn: MKAnnotation, newCoordinate:CLLocationCoordinate2D, post: PostModel) -> (PostAnnotation) in
PostAnnotation(address: PostPlaceMark.init(post: post, coordinate: newCoordinate, title: "\(post.manufacturer) \(post.model)", username: post.username, subTitle: "£\(post.price)"))
}
mapView.removeAnnotations(mapView.annotations)
mapView.addAnnotations(newAnnotations)
}

Related

Unexpected behaviour - unable to access new elements in an array driven by an NSFetchedResultController (SwiftUI)

I have a SwiftUI app that uses the MVVM design pattern in places where the underlying logic driving the View is either verbose or unit testing is advisable. In certain places I have taken to using a NSFetchedResultsController in conjunction with #Published properties and, early in development, this behaved as I would expect.
However, I have now encountered a situation where an addition to the CoreData store triggers controllerDidChangeContent and the array populated by controller.fetchedObjects has an appropriate number of elements but, for reasons I cannot fathom, I am unable to access the newest elements.
There is a certain amount of data processing which, as I'm working with an array by this point, I didn't think would cause a problem. I'm more suspicious that relationships may be responsible in some way and/or faulting is responsible (although adjusting faulting behaviour on the underlying fetch request failed to resolve the issue).
Interestingly, some similar code elsewhere in the app that uses #FetchRequest (because the View is simpler and so a ViewModel wasn't considered necessary) doesn't seem to suffer from the same problem.
Normally scattering debugging around has put me back on track but not today! I've included the console output - as you can see, as new entries (timestamped) are added, the total observation count increases but the most property which should reflect the most recent observation does not change. Any pointers would be gratefully received as always.
I can't really prune the code on this without losing context - apologies in advance for the verbosity ;-)
ViewModel:
extension ParameterGridView {
final class ViewModel: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
#Published var parameters: [Parameter] = []
#Published var lastObservation: [Parameter : Double] = [:]
#Published var recentObservation: [Parameter : Double] = [:]
let patient: Patient
private let dataController: DataController
private let viewContext: NSManagedObjectContext
private let frc: NSFetchedResultsController<Observation>
var observations: [Observation] = []
init(patient: Patient, dataController: DataController) {
self.patient = patient
self.dataController = dataController
self.viewContext = dataController.container.viewContext
let parameterFetch = Parameter.fetchAll
self.parameters = try! dataController.container.viewContext.fetch(parameterFetch)
let observationFetch = Observation.fetchAllDateSorted(for: patient)
self.frc = NSFetchedResultsController(
fetchRequest: observationFetch,
managedObjectContext: dataController.container.viewContext,
sectionNameKeyPath: nil,
cacheName: nil)
try! self.frc.performFetch()
observations = self.frc.fetchedObjects ?? []
super.init()
frc.delegate = self
updateHistoricalObservations()
}
// MARK: - METHODS
/// UI controls for entering new Observations default to the last value entered
/// This function calculates the median value for the Parameter's reference range to be used in the event no historical observations are available
/// - Parameter parameter: Parameter used to derive start value
/// - Returns: median value for the Parameter's reference range
func medianReferenceRangeFor(_ parameter: Parameter) -> Double {
let rangeMagnitude = parameter.referenceRange.upperBound - parameter.referenceRange.lowerBound
return parameter.referenceRange.lowerBound + (rangeMagnitude / 2)
}
/// Adds a new Observation to the Core Data store
/// - Parameters:
/// - parameter: Parameter for the observation
/// - value: Observation value
func addObservationFor(_ parameter: Parameter, with value: Double) {
_ = Observation.create(in: viewContext,
patient: patient,
parameter: parameter,
numericValue: value)
try! viewContext.save()
}
/// Obtains clinically relevant historical observations from the dataset for each Parameter
/// lastObservation = an observation within the last 15 minutes
/// recentObservation= an observation obtained within the last 4 hours
/// There may be better names for these!
private func updateHistoricalObservations() {
let lastObservationTimeLimit = Date.now.offset(.minute, value: -15)!.offset(.second, value: -1)!
let recentObservationTimeLimit = Date.now.offset(.hour, value: -4)!.offset(.second, value: -1)!
Logger.coreData.debug("New Observations.count = \(self.observations.count)")
let sortedObs = observations.sorted(by: { $0.timestamp < $1.timestamp })
let newestObs = sortedObs.first!
let oldestObs = sortedObs.last!
Logger.coreData.debug("Newest obs: \(newestObs.timestamp) || \(newestObs.numericValue)")
Logger.coreData.debug("Oldest obs: \(oldestObs.timestamp) || \(oldestObs.numericValue)")
for parameter in parameters {
var twoMostRecentObservatonsForParameter = observations
.filter { $0.cd_Parameter == parameter }
.prefix(2)
if let last = twoMostRecentObservatonsForParameter
.first(where: { $0.timestamp > lastObservationTimeLimit }) {
lastObservation[parameter] = last.numericValue
twoMostRecentObservatonsForParameter.removeAll(where: { $0.objectID == last.objectID })
} else {
lastObservation[parameter] = nil
}
recentObservation[parameter] = twoMostRecentObservatonsForParameter
.first(where: { $0.timestamp > recentObservationTimeLimit })?.numericValue
}
}
// MARK: - NSFetchedResultsControllerDelegate conformance
internal func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
let newObservations = controller.fetchedObjects as? [Observation] ?? []
observations = newObservations
updateHistoricalObservations()
}
}
}
NSManagedObject subclass:
extension Observation {
// Computed properties excluded to aid clarity
class func create(in context: NSManagedObjectContext,
patient: Patient,
parameter: Parameter,
numericValue: Double? = nil,
stringValue: String? = nil) -> Observation {
precondition(!((numericValue != nil) && (stringValue != nil)), "No values sent to initialiser")
let observation = Observation(context: context)
observation.cd_Patient = patient
observation.timestamp = Date.now
observation.parameter = parameter
if let value = numericValue {
observation.numericValue = value
} else {
observation.stringValue = stringValue!
}
try! context.save()
return observation
}
static var fetchAll: NSFetchRequest<Observation> {
let request = Observation.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \Observation.cd_timestamp, ascending: true)]
return request
}
static func fetchAllDateSorted(for patient: Patient) -> NSFetchRequest<Observation> {
let request = fetchAll
request.sortDescriptors = [NSSortDescriptor(keyPath: \Observation.cd_timestamp, ascending: true)]
request.predicate = NSPredicate(format: "%K == %#", #keyPath(Observation.cd_Patient), patient)
return request
}
static func fetchDateSorted(for patient: Patient, and parameter: Parameter) -> NSFetchRequest<Observation> {
let patientPredicate = NSPredicate(format: "%K == %#", #keyPath(Observation.cd_Patient), patient)
let parameterPredicate = NSPredicate(format: "%K == %#", #keyPath(Observation.cd_Parameter), parameter)
let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [patientPredicate, parameterPredicate])
let request = fetchAll
request.predicate = compoundPredicate
return request
}
}
Console output: (note observation count increments but the most recent observation does not change)
There is something wrong with your timestamps and/or sorting, the oldest observation is 4 days newer than the newest one (and it is in the future!)
Joakim was on the money - the timestamps are indeed incorrect; the problem was not in the logic but an error in the code (maths error relating to the TimeInterval between datapoints) that generated data for testing purposes. Garbage in, garbage out...
A lesson to me to be more careful - precondition now added to the function that generated the time series data (and a unit test!).
static func placeholderTimeSeries(for parameter: Parameter, startDate: Date, numberOfValues: Int) -> [(Date, Double)] {
let observationTimeInterval: TimeInterval = (60*5) // 5 minute intervals, not 5 hours! Test next time!!
let observationPeriodDuration: TimeInterval = observationTimeInterval * Double(numberOfValues)
let observationEndDate = startDate.advanced(by: observationPeriodDuration)
precondition(observationEndDate < Date.now, "Observation period end date is in the future")
return placeholderTimeSeries(valueRange: parameter.referenceRange,
valueDelta: parameter.controlStep...(3 * parameter.controlStep),
numberOfValues: numberOfValues,
startDate: startDate,
dataTimeInterval: observationTimeInterval)
}

How to use NSSet created from Core Data

I have the following core data model:
where Person to Codes is a one-to-many relationship.
I have a function which returns a Person record and if the code person.codes returns an NSSet of all the codes associated with that Person. The issue that I am having is how to use the NSSet.
person.codes.allObjects.first returns this data:
<Codes: 0x60000213cb40> (entity: Codes; id: 0xb978dbf34ddb849 <x-coredata://A2B634E4-E136-48E1-B2C5-82B6B68FBE44/Codes/p1> ; data: {
code = 4LQ;
number = 1;
whosAccount = "0xb978dbf34ddb869 <x-coredata://A2B634E4-E136-48E1-B2C5-82B6B68FBE44/Person/p1>";
})
I thought if I made person.codes.allObjects.first of type Codes, I would be able to access the code and number elements but I get an error: error: value of type 'Any?' has no member 'number'
Also, how can I search this data set for a particular code or number.
I appreciate that this is proabably a simple question but have searched and read the documentation to no avail. I suspect that may base knowledge is not sufficient.
Update
I have a CoreDataHandler class which contains the following code:
class CoreDataHandler: NSObject {
//static let sharedInstance = CoreDataHandler()
private static func getContext() -> NSManagedObjectContext {
let appDelegate = NSApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext
}
static func fetchPerson() -> [Person]? {
let context = getContext()
do {
let persons: [Person] = try context.fetch(Person.fetchRequest())
return persons
} catch {
return nil
}
}
I can fetch a person using:
let row = personTableView.selectedRow
let person = CoreDataHandler.fetchPerson()?[row]
Core Data supports widely native Swift types.
Declare codes as Set<Codes> in the Person class.
It's much more convenient than typeless NSSet.
You get a strong type and you can apply all native functions like filter, sort, etc. without type cast.
let codes = person.codes as! Set<Code>
Once that is done you can access the properties. Searching can be done by filtering for instance
let filteredCodes = codes.filter({ $0.code == "XYZ" })
will return all objects that has the code "XYZ". Or to get only one you can use
let code = codes.first(where: {$0.id == 1})
which will return the first object that has id = 1
A simple example getting all Person objects that has a given code
func findWithCode(_ code: String) -> [Person] {
guard let persons = CoreDataHandler.fetchPerson() else {
return []
}
var result = [Person]()
for person in persons {
let codes = person.codes as! Set<Code>
if codes.contains(where: { $0.code == code }) {
result.append(person)
}
}
return persons
}

Allocating the results of Reverse Geocoding to a global variable

I am using Swift 4 and Xcode 9.3.1. I'm including screenshots of the code.
I am new to mobile development/ programming in general and have been thinking about how to phrase this. So this is the best I can explain it:
I am building an app that gets the user's location in order to send assistance through to them. The app gets the user's coordinates, and displays a TextView with their address information. So pretty straight forward mapKit/coreLocation functionality. So far, so good: Getting the coordinates with startUpdatingLocation() works fine, and I've used Reverse Geocoder to get the street name & locality. But they-- meaning the decoded street and locality strings-- only print out if I call them within the closure, not outside it. I've understood (correctly or incorrectly?) that variables that need to be available for multiple functions within a class should to be declared globally at the top. However I can't figure out how to extract the information from this closure in order to use it elsewhere.
I've been googling and reading through questions in stackOverflow and I feel like something really simple is missing but can't figure out what. Things I've tried unsuccessfully so far:
1_ Defining global variables as empty strings at the beginning of the class
and using the variable names inside the closure where the geocoding reverse method happens, in an attempt to store the resulting strings, but when I try to print the variables outside the closure, the global variable is still and empty string ("").
[global variables declared at the top][1]
2_Defining an empty, global array of strings and appending the information from inside the closure to the global array. Still got an empty array outside the closure. (so same as 1)
3_Create a function --func decodedString()-- to return the data as a String, so I can use it by declaring
*let streetLocation : String = decodedString()*
However when I declare that function like this :
var street = ""
var locality = ""
// Get the street address from the coordinates
func deocodedString() -> String {
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { placemarks, error in
if let placemark = placemarks?.first {
self.street = placemark.name!
self.locality = placemark.locality!
let string = "\(self.street), \(self.locality)"
return string
}
}
}
I get an error of: Unexpected non-void return value in void function
unexpected no void return value in void function
Lastly, if I pass the information straight into a TextView within the closure by using the code below, my textView updates successfully-- but I can't format the strings, which I need to do in order to make them look like the design instructions I'm following (aka some bold text, some regular text, and some different sizes of text):
CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in
if let placemark = placemarks?.first {
self.street = placemark.name!
self.locality = placemark.locality!
let string = "\(self.street), \(self.locality)"
self.addressTextView.text = string
}
}
So that's why I can't just pass it through with the textView.text = string approach.
I'd appreciate some help...I have been looking though StackOverFlow, youtube and other tutorial places but I can't figure out what I'm missing, or why my function declaration generates an error. I have already destroyed and reversed my code several times over last 24 hs without getting an independent string that I can apply formatting to before passing it into the textView and I'm at a loss as to how else to approach it.
When you call this function the reverseGeocodeLocation runs in the background thread. So if you want to return the address in this method you should use escaping closure.
func getaddress(_ position:CLLocationCoordinate2D,completion:#escaping (String)->()) {
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { placemarks, error in
if let placemark = placemarks?.first {
let street = placemark.name!
let locality = placemark.locality!
let string = "\(street), \(locality)"
completion(string)
}
}
}
self.getaddress(position.target) { address in
print(address)
self.addressTextView.text = address
}
I had a problem with google geocoder to update the label on the map screen.
So I did this, first, create
swift file name: GoogleAPI just call it as you like.
class GoogleAPI {
static let sharedInstance = GoogleAPI()
private init() { }
var addressValue = ""
public func geocoding(lat: Double, long: Double) {
Alamofire.request("https://maps.googleapis.com/maps/api/geocode/json?latlng=\(lat),\(long)&key=YOUR_GOOGLE_KEY").responseJSON { (response) in
if response.result.isSuccess {
let dataJSON : JSON = JSON(response.result.value!)
self.geocoding(json: dataJSON)
} else {
print("Error \(response.result.error!)")
}
}
}
fileprivate func geocoding(json: JSON) {
let json = json["results"]
let address = json[1]["formatted_address"].stringValue
addressValue = address
print("pin address \(addressValue)")
}
}
This is an API call to Google to fetch all from a response and parse the only street.
After that go to your View Controller with a map where is the pin, map etc..
Set up a pin, marker to be draggable. [marker1.isDraggable = true]
Then add this function
mapView(_ mapView: GMSMapView, didEndDragging marker: GMSMarker)
and add call from above like this :
func mapView(_ mapView: GMSMapView, didEndDragging marker: GMSMarker) {
GoogleAPI.sharedInstance.geocoding(lat: marker.position.latitude, long: marker.position.longitude)
DispatchQueue.main.async {
self.txtSearch.text = GoogleAPI.sharedInstance.addressValue
}
}
txtSearch is my search text field.
yea I know, that can be done better, but no time. this is working.
Swift 4.2

Swift mapView load more pins

I have a method that showing me multiple Annotations on the map. Its About 10 000 annotation so i have to show only annotation that are visible on the map in current time. When I move with map i execute method that return me objects with coordinates that will be visible. But my method will delete all of them and start to adding them to map one by one. My goal is when I present Annotations and i move with map i want to keep the ones that are visible and the others remove. I am using this and FBAnnotationClusteringSwift framework.
this is my method for adding pins.
private var arrOfPinsOnMap:[FBAnnotation] = []
private var clusteringManager = FBClusteringManager()
func addMapPoints(){
self.arrOfPinsOnMap.removeAll()
self.clusteringManager = FBClusteringManager() //everytime recreate instance otherwise there will be duplicates.
for i in 0..<sortedObjectsByDistance_ids.count {
var id_pobor = String()
let pin = FBAnnotation()
guard let tempUniqueE21 = get_sortedObjectsByDistance(i) else {
continue
}
let temp = tempUniqueE21
pin.coordinate = CLLocationCoordinate2D(latitude: tempUniqueE21.lat, longitude: tempUniqueE21.lng)
pin.title = temp.provoz
pin.subtitle = temp.ulice
self.clusteringManager.addAnnotations([pin])
self.arrOfPinsOnMap.append(pin)
}
}
I am calling this method overtime when user move with map. The problem is not with FB framework but with stored Annotation values.

Swift REALM - sort by distance, filter and store only unique value

Hi I am making an app that show me interesting places. Its showing places in radius. I am using REALM to store values.
However realm don't know how to make Unique values. I am using this for Unique rows.
let result:[String] = realm.objects(E21).sorted("name").uniqueValue("Id_prov", type: String.self)
This for finding things in region around me
var datasourceE21Distance:Results<E21> = realm.findInRegion(E21.self, region: curentRegion).filter(tempRequest)
But i don't know how to combine these things to one and then sort it from closes one to me to the most far.
I will be glad for any help here.
EDIT i am using these two extensions found:
func findInRegion<T: Object>(type: T.Type, region: MKCoordinateRegion, latitudeKey: String = "lat", longitudeKey: String = "lng") -> Results<T> {
// Query
return self
.objects(type)
.filterGeoBox(region.geoBox, latitudeKey: latitudeKey, longitudeKey: longitudeKey)
}
func uniqueValue<U : Equatable>(paramKey: String, type: U.Type)->[U]{
var uniqueValues : [U] = [U]()
for obj in self {
if let val = obj.valueForKeyPath(paramKey) {
if (!uniqueValues.contains(val as! U)) {
uniqueValues.append(val as! U)
}
}
}
return uniqueValues
}
RealmGeoQueries, the library you're using for filtering your entities by a bounding box, supports sorting objects by distance via sortByDistance. This returns an array as this operation has to be done in memory with cached distances.
You would need to make sure that you're uniqueValue method is defined in an extension on Array.
I came up with something like this. But it can't filter right away :-/
let currentLocation = CLLocationCoordinate2D(latitude: currentLat!, longitude: currentLng!)
sortedObjectsByDistance = realm.findNearby(E21.self, origin: currentLocation, radius: 50000.0, sortAscending: true, latitudeKey: "lat", longitudeKey: "lng")
var seenIdProv:[String:Bool] = [:]
sortedObjectsByDistance = sortedObjectsByDistance.filter {
seenIdProv.updateValue(false, forKey: $0.Id_prov) ?? true
}