How to store CloudKit zone change tokens? - cloudkit

So I currently use the following methods to get and set zone change tokens:
private func zoneChangeToken(for zoneID: CKRecordZoneID) -> CKServerChangeToken? {
let cacheKey = self.name(for: self.database) + "." + zoneID.zoneName + "." + zoneID.ownerName
if let data = UserDefaults.standard.data(forKey: cacheKey) {
return NSKeyedUnarchiver.unarchiveObject(with: data) as? CKServerChangeToken
} else {
return nil
}
}
private func setZoneChangeToken(changeToken: CKServerChangeToken?, for zoneID: CKRecordZoneID) {
let cacheKey = self.name(for: self.database) + "." + zoneID.zoneName + "." + zoneID.ownerName
if changeToken == nil {
UserDefaults.standard.removeObject(forKey: cacheKey)
} else {
let data = NSKeyedArchiver.archivedData(withRootObject: changeToken!)
UserDefaults.standard.set(data, forKey: cacheKey)
}
}
I try to uniquely target the zone by creating a path from database to zone. However since the shared database might have multiple zones with the same zone name, I appended the zone owner name.
The question is, is this a proper way to store the zone change tokens or is there a better, more efficient, way?

Related

How to empty an appended array after a widget timeline update

I am transitioning from UIKit to SwiftUI in my app and am updating network calls to async await as well. I created a Large size widget that displays the weather for 7 airports. The airports identifiers are stored in an app group shared userdefatults container. I update the timeline every minute (just for testing, normally it would be every 20 minutes). Initially when the widget is selected and appears, all data is there and correct. After a timeline update the data updates, but not all the airports are returned and after two updates or so (not consistent), the screen goes blank. The userdefaults airports are updated from the main app and saved in the shared user defaults container and it calls WidgetCenter.shared.reloadAllTimelines. This is all working fine as I have another process that uses the same container for a small widget, but with only one airport returning data without the need for an appended array. If I remove the calls to empty the array, the data remains and doesn't go blank, but of course the array keeps appending. I've tried the removeAll() and [] to empty the array at different places in the code, but same result. I am trying to understand the flow in the async/await calls, but seem to be missing something here? Any help would be greatly appreciated. I've been googling and searching stack overflow for a month and don't really know how to solve this issue. Thanks in advance!
actor MetarService: NSObject, XMLParserDelegate, URLSessionDelegate, ObservableObject {
enum MetarFetcherError: Error {
case invalidServerResponse
case missingData
}
#Published var metarArray = [String]()
#Published var metarDataModel: [MetarDataModel] = []
var tempDataModel: [MetarDataModel] = []
func fetchMetars(metarAPTs: String) async throws -> [MetarDataModel] {
let wxUrl = URL(string: "https://www.aviationweather.gov/adds/dataserver_current/httpparam?dataSource=metars&requestType=retrieve&format=xml&hoursBeforeNow=3&mostRecent=true&stationString=" + metarAPTs)!
let (data, response) = try await URLSession.shared.data(from: wxUrl)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw MetarFetcherError.invalidServerResponse
}
guard let xml = SWXMLHash.parse(data) as XMLIndexer? else {
throw MetarFetcherError.missingData
}
noDataResponse = (xml["response"]["data"].element?.attribute(by: "num_results")?.text) ?? "0"
if noDataResponse == "1" && (xml["response"]["data"]["METAR"]["observation_time"].element?.text) != nil {
if (xml["response"]["data"]["METAR"]["station_id"].element?.text) != nil {
myairport = xml["response"]["data"]["METAR"]["station_id"].element!.text
} else {
myairport = "MSNG"
}
if (xml["response"]["data"]["METAR"]["flight_category"].element?.text) != nil {
myfltcat = xml["response"]["data"]["METAR"]["flight_category"].element!.text
} else {
myfltcat = "MISNG"
}
switch myfltcat {
case "VFR":
mymetarImage = "sun.max.circle.fill"
case "MVFR":
mymetarImage = "cloud.sun.circle.fill"
case "IFR":
mymetarImage = "cloud.fog.circle.fill"
case "LIFR":
mymetarImage = "smoke.circle.fill"
default:
mymetarImage = "person.crop.circle.badge.questionmark"
}
if (xml["response"]["data"]["METAR"]["observation_time"].element?.text) != nil {
myobstime = xml["response"]["data"]["METAR"]["observation_time"].element!.text as NSString
if myobstime.length < 16 {
myobstime = "MISNG"
} else {
myobstime = myobstime.substring(with: NSRange(location: 11, length: 5)) as NSString
}
}
if (xml["response"]["data"]["METAR"]["visibility_statute_mi"].element?.text) != nil {
myvis = xml["response"]["data"]["METAR"]["visibility_statute_mi"].element!.text
let intVis = (myvis as NSString) .integerValue
myvis = String(intVis) + "SM"
} else {
myvis = "0"
}
if (xml["response"]["data"]["METAR"]["wind_dir_degrees"].element?.text) != nil {
mywinddir = xml["response"]["data"]["METAR"]["wind_dir_degrees"].element!.text
if mywinddir.contains("VRB") {
mywinddir = "VRB"
} else
if mywinddir.count <= 2 && mywinddir.count > 0 {
mywinddir = "0" + mywinddir
}
} else {
mywinddir = "MISNG"
}
if (xml["response"]["data"]["METAR"]["wind_speed_kt"].element?.text) != nil {
mywindspd = xml["response"]["data"]["METAR"]["wind_speed_kt"].element!.text
if mywindspd == "0" {
mywind = "Calm"
} else if mywindspd.count == 1 {
mywindspd = "0" + mywindspd
mywind = mywinddir + "/" + mywindspd + "KT"
} else if mywindspd.count > 1 {
mywind = mywinddir + "/" + mywindspd + "KT"
}
} else {
mywind = "MISNG"
}
}
self.tempDataModel.append(MetarDataModel(metarImage: mymetarImage, mairport: myairport, mobstime: myobstime as String, mfltcat: myfltcat, mvis: myvis, mwind: mywind))
self.metarDataModel = self.tempDataModel
tempDataModel = []
return metarDataModel
}
func readMetarApts() -> [String] {
let defaults = UserDefaults(suiteName: "group.userdefaults.shared.FRAT")!
if ((defaults.value(forKey: "icaoIdent") as! String).isEmpty) {
defaultairport = "KSFO"
} else {
defaultairport = defaults.value(forKey: "icaoIdent") as! String
}
wxAirport1 = defaults.value(forKey: "wxAirport1") as! String
wxAirport2 = defaults.value(forKey: "wxAirport2") as! String
wxAirport3 = defaults.value(forKey: "wxAirport3") as! String
wxAirport4 = defaults.value(forKey: "wxAirport4") as! String
wxAirport5 = defaults.value(forKey: "wxAirport5") as! String
wxAirport6 = defaults.value(forKey: "wxAirport6") as! String
metarArray.append(defaultairport)
metarArray.append(wxAirport1)
metarArray.append(wxAirport2)
metarArray.append(wxAirport3)
metarArray.append(wxAirport4)
metarArray.append(wxAirport5)
metarArray.append(wxAirport6)
metarArray = metarArray.sorted()
let returnArray = metarArray
metarArray = []
return returnArray
}// end of readAirports function
nonisolated func getAirports() -> ([MetarDataModel] ){
// transData = []
Task{
let tempArray = await readMetarApts()
for apts in tempArray {
let zData = try await self.fetchMetars(metarAPTs: apts)
if zData .isEmpty {
let errorData = MetarDataModel(metarImage: "sun.max.circle.fill", mairport: "DATA", mobstime: "CHK", mfltcat: "MSNG", mvis: "WiFi", mwind: "")
tempData = [errorData]
} else {
transData.append(contentsOf: zData)
tempData = transData
} // else Closure
} //Task Closure
//transData.removeAll()
} // apts in tempArray Closure
tempData = transData
// transData = []
return tempData.sorted()
} // end of getAirports function
} // end of MetarService Class
I have tried different solutions found on stack overflow, reddit, medium and others. But no matter what approach I take, if I try and empty the appended array in preparation for the next timeline update, it will always eventually return without data. At first I thought it was the app group shared container losing reference as I got the much debated 'kCFPreferencesAnyUser, ByHost: Yes, Container: (null)): Using kCFPreferencesAnyUser with a container is only allowed for System Containers, detaching from cfprefsd' in the log, but Apple says this does not indicate that particular condition? And, I use the container elsewhere with no issues. I am new to the async await and the new URLSession.shared.data(from: ) and maybe I'm not understanding the flow and timing of how the data is fetched and returned? I just need to append the array, display the data, then empty the array and wait for the next timeline to fetch new data. I've put the removeAll() and tried [] as an alternative in many different places in my code (at the start of the function and at the end of the function). Stumped!!

How to query multiple fields with one value in Firebase?

I'm a newbie at firebase I have implemented a sample app that able to transfer point to each other after transfer success I also added two fields called "sender_name" and "receiver_name" but it's too difficult to get all transitions based on user login I found sample ways to do just add multiple where to it, its work fine if true both but that's not what I want I want whereOr like SQL as an example below
SELECT column1, column2, ...
FROM table_name
WHERE condition1 OR condition2 OR condition3 ...;
any solution help, please
func getUserTransition(){
// process
/*
1.get all transition from tm_members sender and receiver by current user login
2.
*/
guard let username = self.userSession?.username else {
return
}
print("username in user session : \(username)")
COLLECTION_TM_TRANSITIONS_UAT
.whereField("sender_name", isEqualTo: username)
.whereField("receiver_name", isEqualTo: username)
.getDocuments { documentSnapshot, error in
if error == nil {
guard let value = documentSnapshot?.documents else { return }
self.tmTransitions = value.map { (queryDocumentSnapshot) -> TmTransition in
let data = queryDocumentSnapshot.data()
let email = data["email"] as? String ?? ""
let is_sender = data["is_sender"] as? Bool ?? false
let point = data["point"] as? Int ?? 0
let username = data["username"] as? String ?? ""
let sender_id = data["sender_id"] as? String ?? ""
let receiver_id = data["receiver_id"] as? String ?? ""
let created_at = data["created_at"] as? Timestamp
let sender_name = data["sender_name"] as? String ?? ""
let receiver_name = data["receiver_name"] as? String ?? ""
print("username : \(email)")
return TmTransition(id: queryDocumentSnapshot.documentID, sender_id: sender_id, receiver_id: receiver_id, username: username, is_sender: is_sender, point: point, email: email,created_at: created_at,sender_name: sender_name,receiver_name: receiver_name)
}
}
else{
print("error during fetch data ")
}
}
}

How to get a specific attribute in a core data entity swift

I know there are similiar questions but I have spent hours of trying to find relevant result to my situation without success.
I have got one entity: EntityDate.
The entity has got 2 attributes: date and time
I have a variable "date" that I save as a String to core data like this:
addToCoreData(name: getDateFormatted(), entityName: "EntityDate", key: "date")
func addToCoreData(name: String, entityName: String, key: String) {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let entity = NSEntityDescription.entity(forEntityName: entityName, in: context)
let newEntity = NSManagedObject(entity: entity!, insertInto: context)
newEntity.setValue(name, forKey: key)
do {
try context.save()
print("saved")
} catch {
print("Failed saving")
}
}
Later in my code I retrieve the data like this:
var dateFromCD = getCoreData(Entity: "EntityDate")
func getCoreData(Entity: String) -> Array<NSManagedObject>{
var coreData= [NSManagedObject]()
coreData= []
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let request = NSFetchRequest<NSFetchRequestResult>(entityName: Entity)
request.returnsObjectsAsFaults = false
do {
let result = try context.fetch(request)
for data in result as! [NSManagedObject]
{
coreData.append(data)
}
} catch {
print("Failed")
}
return coreData
}
Now I thought I would add a variable time to the seperate attribute "time", and naturally I proceeded by doing like this:
addToCoreData(name: getFormattedTime(), entityName: "EntityDate", key: "time")
The problem I have is that when I call the function
"getCoreData(Entity: "EntityDate")" it gives me this output
[<EntityDate: 0x6000034fc2d0> (entity: EntityDate; id: 0xc229f3dcff76efb1 <x-coredata://2E6F1D46-F49D-436C-9608-FEC59EEB21E6/EntityDate/p18>; data: {
date = "2/21/20";
time = nil;
}), <EntityDate: 0x6000034fc320> (entity: EntityDate; id: 0xc229f3dcff72efb1 <x-coredata://2E6F1D46-F49D-436C-9608-FEC59EEB21E6/EntityDate/p19>; data: {
date = nil;
time = "13:46";
})]
I dont understand the output and I would like to be able to retrieve only date and only time as two seperate variables from the array.
The problem is that you are creating two records, one with the date and the other with the time information instead of one with both date and time.
If you have only one entity anyway take advantage of the generic behavior and write the method more specific like
func createEntityDate(date: String, time: String) {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let newEntity = EntityDate(context: context)
newEntity.date = date
newEntity.time = time
do {
try context.save()
print("saved")
} catch {
print("Failed saving", error)
}
}
If you need to modify attributes of an existing record you have to fetch it, modify and save it back.
In any case it's highly recommended to save the whole date as Date and use computed properties to extract the date and time portion.

Getting old value of field in Firestore with Swift 5

I'm trying to get the old value of a field when it is changed in Firestore. Is there any way to access what the previous value of a field is after it is changed? Here is my current code, I want to access the old nickName under .modified and print out what the new nickName is and also the old one.
db.collection("cities").whereField("state", isEqualTo: "CA").addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
snapshot.documentChanges.forEach { diff in
if (diff.type == .added) {
print("New city: \(diff.document.data())")
let nickName = myData["nickName"] as? String ?? ""
}
if (diff.type == .modified) {
let nickName = myData["nickName"] as? String ?? ""
}
if (diff.type == .removed) {
let nickName = myData["nickName"] as? String ?? ""
}
}
}
Unfortunately, that is not a feature of Firestore. What you can do is have another field oldNickName and using Firebase Functions, automatically update that when the nickName field is changed.
The best solution is storing nickName locally, so you can refer back to your local variable when nickName changes, accessing the newly updated one in the database and the previous nickName locally. Here is the updated code:
var nickNames = [String : String]()
db.collection("cities").whereField("state", isEqualTo: "CA").addSnapshotListener { snapshot, error in
guard error == nil, let snapshot = snapshot?.documentChanges else { return }
snapshot.forEach {
let document = $0.document
let documentID = document.documentID
let nickName = document.get("nickName") as? String ?? "Error"
switch $0.type {
case .added:
print("New city: \(document.data())")
nickNames[documentID] = nickName
case .modified:
print("Nickname changed from \(nickNames[documentID]) to \(nickName)")
nickNames[documentID] = nickName
case .removed:
print("City removed with nickname \(nickNames[documentID])")
nickNames.removeValue(forKey: documentID)
}
}
}
nickNames is a dictionary with key cityID and value nickName. This code is written in Swift 5.

How to parse data from firebase and then save it within a model. Swift3 MVC format

I have a user class within my firebaseDB. Its what you would expect a flat file DB would look like: users->UID->profile->key/values. I am trying to implement best practice within my code so I want the data to be handled by the User model in my Xcode project. How would I go about parsing the data and saving it as class variables within my model.
class User {
private var _username: String
private var _uid: String
var uid: String {
return _uid
}
var username: String {
return _username
}
init (uid: String, username:String) {
self._uid = uid
self._username = username
}
func getUserData() {
DataService.ds.REF_USERS.observeSingleEvent(of: .value, with: { (snapshot) in
if let users = snapshot.value as? Dictionary<String, Any>{
for (key, value) in users { // We use the for in loop to iterate through each key/value pair in the database which is stored as a dictionary.
if let dict = value as? Dictionary<String, Any> { // This gets us inside the user ID
if let profile = dict["profile"] as? Dictionary<String, Any> { // This gets us inside the profile
if let username = profile["username"] as? String {
self._uid = key // Stores the UID of the user as the key.
self._username = username
} else {
let username = profile["name"] as? String
self._uid = key
self._username = username!
}
}
}
}
}
This is what I am trying to do but I am lost as to how I would actually store the data values within the class. FYI: username is an attribute under profile.
You are currently looping through the entire list of users in the database and assigning the User properties repeatedly, overwriting the value from the previous loop cycle.
In order to get the correct user values assigned, you will need a way of knowing the correct path to the values. I would suggest saving the users using their assigned Firebase UID.
If you know the correct UID to look up you can then implement the following code:
func getUserData() {
DataService.ds.REF_USERS.child(_uid).child("profile").observeSingleEvent(of: .value, with: { (snapshot) in
guard let profileDict = snapshot.value as? [String: Any] else {
print("Error fetching user")
return
}
self._username = profileDict["username"] as? String ?? profileDict["name"] as! String
})
}
The way that "username" is force unwrapped in your original code doesn't appear safe but I'm assuming you are certain that it will work out. Otherwise the following line should be changed to safely unwrap the optional:
self._username = profileDict["username"] as? String ?? profileDict["name"] as! String