I'm trying to understand CloudKit, but I'm having an issue figuring out a real-world issue if I eventually turned it into an app.
I've got a tableviewcontroller that's populated by CloudKit, which works fine. I've got a detailviewcontroller that is pushed when you tap one of the entries in the tableview controller. The detailviewcontroller pulls additional data from CloudKit that wasn't in the initial tableviewcontroller. All of this works perfectly fine.
Now, the detailviewcontroller allows changing of an image and saving it back to a public database. That's working fine as well.
The scenario I'm dealing with is thinking about this core function being used in an app by multiple people. If one person uploads a photo (overwriting an existing photo for one of the records), what happens if another user currently has the app in the background? If they open the app, they'll see the last photo they saw on that page, and not the new photo. So I want to reload the data from CloudKit when the app comes back to the foreground, but I must be doing something wrong, because it's not working. Using the simulator and my actual phone, I can change a photo on one device and the other device (simulator or phone) doesn't update the data when it comes into the foreground.
Here's the code I'm using: First in ViewController.swift:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
println("viewDidLoad")
NSNotificationCenter.defaultCenter().addObserver(self, selector: "activeAgain", name: UIApplicationWillEnterForegroundNotification, object: nil)
and then the activeAgain function:
func activeAgain() {
println("Active again")
fetchItems()
self.tblItems.reloadData()
println("Table reloaded")
}
and then the fetchItems function:
func fetchItems() {
let container = CKContainer.defaultContainer()
let publicDatabase = container.publicCloudDatabase
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Items", predicate: predicate)
publicDatabase.performQuery(query, inZoneWithID: nil) { (results, error) -> Void in
if error != nil {
println(error)
}
else {
println(results)
for result in results {
self.items.append(result as! CKRecord)
}
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
self.tblItems.reloadData()
self.tblItems.hidden = false
})
}
}
}
I tried adding a reload function to viewWillAppear, but that didn't really help:
override func viewWillAppear(animated: Bool) {
tblItems.reloadData()
}
Now here's the code in DetailViewController.swift:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
NSNotificationCenter.defaultCenter().addObserver(self, selector: "activeAgain", name: UIApplicationWillEnterForegroundNotification, object: nil)
self.imageScrollView.contentSize = CGSizeMake(1000, 1000)
self.imageScrollView.frame = CGRectMake(0, 0, 1000, 1000)
self.imageScrollView.minimumZoomScale = 1.0
self.pinBoardScrollView.maximumZoomScale = 8.0
showImage()
and then the showImage function:
func showImage() {
imageView.hidden = true
btnRemoveImage.hidden = true
viewWait.hidden = true
if let editedImage = editedImageRecord {
if let imageAsset: CKAsset = editedImage.valueForKey("image") as? CKAsset {
imageView.image = UIImage(contentsOfFile: imageAsset.fileURL.path!)
imageURL = imageAsset.fileURL
self.title = "Image"
imageView.hidden = false
btnRemoveImage.hidden = false
btnSelectPhoto.hidden = true
}
}
}
and the activeAgain function:
func activeAgain() {
showImage()
println("Active again")
}
Neither of these ViewControllers reload the data from CloudKit when they return. It's like it's cached the table data for the first ViewController and the image for the DetailViewController and it's using that instead of downloading the actual CloudKit data. I'm stumped, so I figure I better try this forum (my first post!). Hopefully I included enough required info to be useful.
Your showImage function is using a editedImage CKRecord. Did you also reload that record? Otherwise the Image field in that record would then still contain the old CKAsset.
Besides just reloading on a moment you feel right, you could also let the changes be pushed by creating a CloudKit subscription. Then your data will even change when it's active and changed by someone else.
Related
I saved a UIImage to UserDefaults via .data with this code, where the key equals "petPhoto1":
#IBAction func addPhotoButton(_ sender: Any) {
let picker = UIImagePickerController()
picker.allowsEditing = false
picker.delegate = self
picker.mediaTypes = ["public.image"]
present(picker, animated: true)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
image.storeInUserDefaults(for: "petPhoto\(activePet)")
UserDefaults.standard.set("yes", forKey: "doesImageExist\(activePet)")
}
dismiss(animated: true, completion: nil)
}
(unrelated stuff in between)
extension UIImage {
func storeInUserDefaults(with compressionQuality: CGFloat = 0.8, for key: String) {
guard let data = self.jpegData(compressionQuality: compressionQuality) else { return }
let encodedImage = try! PropertyListEncoder().encode(data)
UserDefaults.standard.set(encodedImage, forKey: key)
}
}
Now when I erase it like this:
UserDefaults.standard.set(nil, forKey: "petPhoto1")
I can still see that "Documents & Data" for my app under Settings is still full with the same size as the original image, indicating that it didn't actually delete it, even though it no longer displays when it gets loaded back from UserDefaults.
Can anyone figure out a way to fix this? Thanks!
By the way, in case it helps, here is other code related to this issue:
The code I use in the ImageViewController that I display the image after saving it:
#IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
activePet = UserDefaults.standard.string(forKey: "activePet")! // activePet = 1 (confirmed with debugging with other, unrelated code
imageView.image = try? UIImage.loadFromUserDefaults(with: "petPhoto\(activePet)")
}
extension UIImage {
static func loadFromUserDefaults(with key: String) throws -> UIImage? {
let activePet = UserDefaults.standard.string(forKey: "activePet")!
guard let data = UserDefaults.standard.data(forKey: "petPhoto\(activePet)") else {
return nil
}
do {
let decodedImageData = try PropertyListDecoder().decode(Data.self, from: data)
return UIImage(data: decodedImageData)
} catch let error {
throw error
}
}
}
When you do this:
UserDefaults.standard.set(nil, forKey: "petPhoto1")
The link between the key and the file saved will be removed synchronously. that means if you try to access the value for this key, it gives nil.
But this image needs to be cleared from storage too, that will be happening asynchronously [we don't have completion handler API support from apple to get this information].
Apple Documentation for reference:
At runtime, you use UserDefaults objects to read the defaults that
your app uses from a user’s defaults database. UserDefaults caches the
information to avoid having to open the user’s defaults database each
time you need a default value. When you set a default value, it’s
changed synchronously within your process, and asynchronously to
persistent storage and other processes.
What you can try:
First approch
Give some time for the file delete operation to get completed by OS. then try to access the image in the disk.
Second approch
Try observing to the changes in the directory using GCD. Refer: https://stackoverflow.com/a/26878163/5215474
In my application I use Firebase Realtime Database to store data about users. I would like to be sure that when I read this data to display it in the view (e.g. their nickname), that the reading has been done before displaying it. Let me explain:
//Initialization of user properties
static func initUsers(){
let usersref = dbRef.child("Users").child(userId!)
usersref.observeSingleEvent(of: .value) { (DataSnapshot) in
if let infos = DataSnapshot.value as? [String : Any]{
self.username = infos["username"] as! Int
//The VC is notified that the data has been recovered
let name = Notification.Name(rawValue: "dataRetrieved")
let notification = Notification(name: name)
NotificationCenter.default.post(notification)
}
}
}
This is the code that runs in the model and reads the user's data when they log in.
var isFirstAppearance = true
override func viewDidLoad() {
super.viewDidLoad()
//We initialise the properties associated with the user
Users.initUsers()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if isFirstAppearance {
let name = Notification.Name(rawValue: "dataRetrieved")
NotificationCenter.default.addObserver(self, selector: #selector(registerDataToView), name: name, object: nil)
isFirstAppearance = false
}
else{
registerDataToView()
}
}
//The user's data is assigned to the view
#objc func registerDataToView(){
usernameLabel.text = String(Users.username)
}
Here we are in the VC and when the view loads we call initUsers in viewDidLoad. In viewWillAppear, if it's the first time we load the view then we create a listener which calls registerDataToView if the reading in the database is finished. Otherwise we simply call registerDataToView (this is to update the labels when we return to this VC).
I would like to know if it is possible, for example when we have a very bad connection, that the listener does not intercept the dataRetrieved notification and therefore that my UI displays only the default texts? Or does the headset wait to receive the notification before moving on?
If it doesn't wait then how can I wait for the database read to finish before initializing the labels?
Thanks for your time :)
Don't wait. Never wait. Tell the receiver that the data are available with a completion handler
static func initUsers(completion: #escaping (Bool) -> Void) {
let usersref = dbRef.child("Users").child(userId!)
usersref.observeSingleEvent(of: .value) { (DataSnapshot) in
if let infos = DataSnapshot.value as? [String : Any]{
self.username = infos["username"] as! Int
completion(true)
return
}
completion(false)
}
}
And use it
override func viewDidLoad() {
super.viewDidLoad()
//We initialise the properties associated with the user
Users.initUsers { [unowned self] result in
if result (
// user is available
} else {
// user is not available
}
}
}
I'm somewhat new to this and this is my first question on stackoverflow. Thanks in advance for your help and bear with me if my formatting sucks
I've got multiple views within my app (all displaying data using tableview subviews) that need to update automatically when the data changes on the database (Firestore), i.e. another user updates the data.
I've found a way to do this which is working well, but I want to ask the community if there's a better way.
Currently, I am creating a Timer object with a timeInterval of 2. On the interval, the timer queries the database and checks a stored data sample against updated data. If the two values vary, I run viewDidLoad which contains my original query, tableView.reloadData(), etc..
Any suggestions or affirmations would be very useful.
var timer = Timer()
var oldChallengesArray = [String]()
var newChallengesArray = [String]()
override func viewDidLoad() {
super.viewDidLoad()
//set tableview delegate
mainTableView.delegate = self
mainTableView.dataSource = self
//set challengesmodel delegate
challengesModel.delegate = self
//get challenges
DispatchQueue.main.async {
self.challengesModel.getChallenges(accepted: true, challengeDenied: false, incomingChallenges: false, matchOver: false)
self.mainTableView.reloadData()
}
scheduledTimerWithTimeInterval()
}
func scheduledTimerWithTimeInterval(){
// Scheduling timer to Call the function "updateCounting" with the interval of 1 seconds
timer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(self.updateTableView), userInfo: nil, repeats: true)
}
#objc func updateTableView(){
ChallengeService.getAllUserChallengeIDs(accepted: true, challengeDenied: false, matchOver: false) { (array) in
if array.isEmpty {
return
} else {
self.newChallengesArray = array
if self.oldChallengesArray != self.newChallengesArray {
self.oldChallengesArray = self.newChallengesArray
self.newChallengesArray.removeAll()
self.viewDidLoad()
}
}
}
}
Firestore is a "realtime database", that means that the database warns you when changes happen to the data. To achieve that the app needs to subscribe to relevant changes in the db. The sample code below can be found here:
db.collection("cities").document("SF")
.addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
print("Error fetching document: \(error!)")
return
}
guard let data = document.data() else {
print("Document data was empty.")
return
}
print("Current data: \(data)")
}
Also, I would like to point out that calling viewDidLoad is incorrect, you should never call viewDidLoad yourself, create an func to update the data. Something like this:
DispatchQueue.main.async {
self.mainTableView.reloadData()
}
I am building an app that populates data in a collectionView. The data come from API calls. When the screen first loads I get the products and store them locally in my ViewController.
My question is when should I get the products again and how to handle screen changing. My data will change when the app is running (sensitive attributes like prices) , but I don't find ideal solution to make the API call each time viewWillAppear is being called.
Can anybody please tell me what is the best pattern to handle this situation. My first though was to check if [CustomObject].isEmpty on viewWillAppear and then make the call. Including a timer that check again every 10-15 minutes for example.
Thank you for your input.
I'm not sure what the data looks like and how your API in detail works, but you certainly don't have to call viewWillAppear when your API updates the data.
There are two possible solutions to be notified when your data is updated.
You can either use a notification that lets you know whether the API is providing some data. After the Data has been provided your notification then calls to update the collection view. You can also include in the objects or structs that contain the data from your API the "didSet" call. Every time the object or struct is being updated the didSet routine is called to update your collection view.
To update your collection view you simply call the method reloadData() and the collection view will update itself and query the data source that now contains the newly received data from your API.
Hope this helps.
There is no set pattern but it is advisable not to send repeated network requests to increase energy efficiency (link). You can check the time interval in ViewWillApear and send the network requests after certain gap or can use timer to send requests at time intervals. First method would be better as it sends request only when user is on that screen. You can try following code snippet to get the idea
class ViewController: UIViewController {
let time = "startTime"
let collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
update()
}
private func update() {
if let startDateTime = NSUserDefaults.standardUserDefaults().objectForKey(time) as? NSDate {
let interval = NSDate().timeIntervalSinceDate(startDateTime)
let elapsedTime = Int(interval)
if elapsedTime >= 3600 {
makeNetworkRequest()
NSUserDefaults.standardUserDefaults().setObject(startDateTime, forKey: time)
}
} else {
makeNetworkRequest()
NSUserDefaults.standardUserDefaults().setObject(NSDate(), forKey: time)
}
}
func makeNetworkRequest() {
//Network Request to fetch data and update collectionView
let urlPath = "http://MyServer.com/api/data.json"
guard let endpoint = NSURL(string: urlPath) else {
print("Error creating endpoint")
return
}
let request = NSMutableURLRequest(URL:endpoint)
NSURLSession.sharedSession().dataTaskWithRequest(request) { (data, response, error) in
do {
guard let data = data else {
return
}
guard let json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject] else {
print("Error in json parsing")
return
}
self.collectionView.reloadData()
} catch let error as NSError {
print(error.debugDescription)
}
}.resume()
}
I use firebase in swift, I call the listener in viewDidAppear: and as it is written in the docs it should't download the same data again but now every time the view appears the same data will be displayed.
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(true)
getData()
self.messagesRef.keepSynced(true)
}
func getData(){
messagesRef.observeEventType(.ChildAdded) { (mesData: FDataSnapshot!) -> Void in
let messageDict = mesData.value as! NSDictionary
let key = mesData.key
self.likedRef = self.ref.childByAppendingPath("likedMessages/\(currentUser.uid)/\(key)")
self.likedRef.observeSingleEventOfType(.Value) { (likedFd: FDataSnapshot!) -> Void in
if likedFd.value is NSNull{
let liked = false
let message = Message(message: messageDict, key: key, liked: liked, topicKey: self.topic.key)
self.messages.append(message)
self.messages.sortInPlace{$0.createdAt.compare($1.createdAt) == .OrderedDescending}
}
else{
let liked = likedFd.value as! Bool
let message = Message(message: messageDict, key: key, liked: liked, topicKey: self.topic.key)
self.messages.append(message)
self.messages.sortInPlace{$0.createdAt.compare($1.createdAt) == .OrderedDescending}
}
dispatch_async(dispatch_get_main_queue()) { () -> Void in
self.messagesTableView.reloadData()
}
self.likedRef.keepSynced(true)
}
print(mesData.value)
}
}
If you add the first listener to a location, Firebase will download the current data for that location and start synchronizing the changes. It will keep a copy of the data available in memory.
If you add a second listener to the location, while the first one is still active, Firebase will not re-download the data. I just quickly verified this http://jsbin.com/foduxun/edit?js,console.
If you remove the first listener before you attach the second listener, Firebase will purge its cache. So then when you attach a listener again, it will re-download the data.
If you want to keep the data available on the device even after you detached all listeners, enable disk persistence:
[Firebase defaultConfig].persistenceEnabled = YES;