https://github.com/leoru/SwiftLoader
I'm using this activity indicator library in my app, but it's freezing my main UICollectionView after I call SwiftLoader.hide() to where I can't interact with anything. My iOS device itself is not freezing as I can perform all other functions fine, and my debugger isn't complaining about anything. It's just that my UICollectionView isn't interactive after calling that one method. I know that this library is the problem because it never freezes unless I use it. Also from looking at the library's code, it involves adding UIViews/subviews and removing or dismissing them. I have suspicions that something isn't being removed or dismissed properly. Can someone confirm my suspicions? Below is the only code I'm using that involves a library called SwiftLoader. If I take the SwiftLoader functions out, it works perfectly with no freezing.
func queryPostObjectsWithLocation(loci: CLLocation) {
SwiftLoader.show(title: "Loading...", animated: true)
let postQuery = PFQuery(className: "Post")
let postsNearThisUser = postQuery.whereKey("location", nearGeoPoint:PFGeoPoint(location: loci),withinMiles: miles)
let whiteList = postQuery.whereKey("objectId", notContainedIn: self.flaggedPosts)
let combo = PFQuery.orQueryWithSubqueries([postsNearThisUser,whiteList])
combo.limit = 20
combo.findObjectsInBackgroundWithBlock {(result: [AnyObject]?, error: NSError?) -> Void in
self.posts = result as? [PFObject] ?? []
if self.posts == [] {
SwiftLoader.hide()
}
self.collectionView.reloadData()
SwiftLoader.hide()
}
}
Try executed your code on the main thread like this:
combo.findObjectsInBackgroundWithBlock {(result: [AnyObject]?, error: NSError?) -> Void in {
self.posts = result as? [PFObject] ?? []
NSOperationQueue.mainQueue().addOperationWithBlock {
if self.posts == [] {
SwiftLoader.hide()
}
self.collectionView.reloadData()
SwiftLoader.hide()
}
}
Related
I'm trying to share a record with other users in CloudKit but I keep getting an error. When I tap one of the items/records on the table I'm presented with the UICloudSharingController and I can see the iMessage app icon, but when I tap on it I get an error and the UICloudSharingController disappears, the funny thing is that even after the error I can still continue using the app.
Here is what I have.
Code
var items = [CKRecord]()
var itemName: String?
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = items[indexPath.row]
let share = CKShare(rootRecord: item)
if let itemName = item.object(forKey: "name") as? String {
self.itemName = item.object(forKey: "name") as? String
share[CKShareTitleKey] = "Sharing \(itemName)" as CKRecordValue?
} else {
share[CKShareTitleKey] = "" as CKRecordValue?
self.itemName = "item"
}
share[CKShareTypeKey] = "bundle.Identifier.Here" as CKRecordValue
prepareToShare(share: share, record: item)
}
private func prepareToShare(share: CKShare, record: CKRecord){
let sharingViewController = UICloudSharingController(preparationHandler: {(UICloudSharingController, handler: #escaping (CKShare?, CKContainer?, Error?) -> Void) in
let modRecordsList = CKModifyRecordsOperation(recordsToSave: [record, share], recordIDsToDelete: nil)
modRecordsList.modifyRecordsCompletionBlock = {
(record, recordID, error) in
handler(share, CKContainer.default(), error)
}
CKContainer.default().privateCloudDatabase.add(modRecordsList)
})
sharingViewController.delegate = self
sharingViewController.availablePermissions = [.allowPrivate]
self.navigationController?.present(sharingViewController, animated:true, completion:nil)
}
// Delegate Methods:
func cloudSharingControllerDidSaveShare(_ csc: UICloudSharingController) {
print("saved successfully")
}
func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
print("failed to save: \(error.localizedDescription)")// the error is generated in this method
}
func itemThumbnailData(for csc: UICloudSharingController) -> Data? {
return nil //You can set a hero image in your share sheet. Nil uses the default.
}
func itemTitle(for csc: UICloudSharingController) -> String? {
return self.itemName
}
ERROR
Failed to modify some records
Here is what I see...
Any idea what could be wrong?
EDIT:
By the way, the error is generated in the cloudSharingController failedToSaveShareWithError method.
Looks like you're trying to share in the default zone which isn't allowed. From the docs here
Sharing is only supported in zones with the
CKRecordZoneCapabilitySharing capability. The default zone does not
support sharing.
So you should set up a custom zone in your private database, and save your share and records there.
Possibly it is from the way you're trying to instantiate the UICloudSharingController? I cribbed my directly from the docs and it works:
let cloudSharingController = UICloudSharingController { [weak self] (controller, completion: #escaping (CKShare?, CKContainer?, Error?) -> Void) in
guard let `self` = self else {
return
}
self.share(rootRecord: rootRecord, completion: completion)
}
If that's not the problem it's something with either one or both of the records themselves. If you upload the record without trying to share it, does it work?
EDIT TO ADD:
What is the CKShareTypeKey? I don't use that in my app. Also I set my system fields differently:
share?[CKShare.SystemFieldKey.title] = "Something"
Try to add this to your info.plist
<key>CKSharingSupported</key>
<true/>
Context:
App with all data in CloudKit
ViewController calls a query to load the data for a tableview
tableview crashes because the array of data for the tableview hasn't
come back from CK
I've researched semaphores and have it nearly
working But can't seem to figure out where to place the
semaphore.signal() to get the exact right behaviour
within viewDidLoad, I call the function:
Week.fetchWeeks(for: challenge!.weeks!) { weeks in
self.weeks = weeks
}
and the function:
static func fetchWeeks(for references: [CKRecord.Reference],
_ completion: #escaping ([Week]) -> Void) {
let recordIDs = references.map { $0.recordID }
let operation = CKFetchRecordsOperation(recordIDs: recordIDs)
operation.qualityOfService = .utility
let semaphore = DispatchSemaphore(value: 0)
operation.fetchRecordsCompletionBlock = { records, error in
let weeks = records?.values.map(Week.init) ?? []
DispatchQueue.main.async {
completion(weeks)
//Option 1: putting semaphore.signal() here means it never completes
// beyond initialization of the week records
}
//Option 2: putting semaphore.signal() here means it completes after the
// initialization of the Week items, but before completion(weeks) is done
// so the array isn't initialized in the view controller in time. so the
// VC tries to use weeks and unwraps a nil.
semaphore.signal()
}
Model.currentModel.publicDB.add(operation)
semaphore.wait() // blocking the thread until .signal is called
}
Note: I have tested that the weeks array within the view controller is properly set eventually - so it does seem to be purely a timing issue :)
I've tested placement of .signal() and if I put it within the 'DispatchQueue.main.async' block, it never gets triggered - probably because that block itself is waiting for the signal.
However if I put it anywhere else, then the viewcontroller picks up at that point and the completion(weeks) doesn't get called in time.
Maybe it is obvious - but as my first time working with semaphores - I'm struggling to figure it out!
Update 1: It works with DispatchQueue(label: "background")
I was able to get it working once I twigged that the semaphore.wait() was never going to get called with semaphore.signal() on the main thread.
So I changed it from:
DispatchQueue.main.async
to
DispatchQueue(label: "background").async and popped the semaphore.signal() inside and it did the trick
Comments/critiques welcome!
static func fetchWeeks(for references: [CKRecord.Reference],
_ completion: #escaping ([Week]) -> Void) {
NSLog("inside fetchWeeks in Week ")
let recordIDs = references.map { $0.recordID }
let operation = CKFetchRecordsOperation(recordIDs: recordIDs)
operation.qualityOfService = .utility
let semaphore = DispatchSemaphore(value: 0)
operation.fetchRecordsCompletionBlock = { records, error in
if error != nil {
print(error?.localizedDescription)
}
let weeks = records?.values.map(Week.init) ?? []
DispatchQueue(label: "background").async {
completion(weeks)
semaphore.signal()
}
}
Model.currentModel.publicDB.add(operation)
semaphore.wait() // blocking the thread until .signal is called
}
}
Update 2: Trying to avoid use of semaphores
Per comment thread - we shouldn't need to use semaphores with CloudKit - so it is likely that I'm doing something stupid :)
moving fetchWeeks() to the viewController to try to isolate the issue...but it still blows up as fetchWeeks() has't completed before the code tries to execute the line after and use the weeks array
my viewController:
class ChallengeDetailViewController: UIViewController {
#IBOutlet weak var rideTableView: UITableView!
//set by the inbound segue
var challenge: Challenge?
// set in fetchWeeks based on the challenge
var weeks: [Week]?
override func viewDidLoad() {
super.viewDidLoad()
rideTableView.dataSource = self
rideTableView.register(UINib(nibName: K.cellNibName, bundle: nil), forCellReuseIdentifier: K.cellIdentifier)
rideTableView.delegate = self
fetchWeeks(for: challenge!.weeks!) { weeks in
self.weeks = weeks
}
//This is where it blows up as weeks is nil
weeks = weeks!.sorted(by: { $0.weekSequence < $1.weekSequence })
}
//moved this to the view controller
func fetchWeeks(for references: [CKRecord.Reference],
_ completion: #escaping ([Week]) -> Void) {
let recordIDs = references.map { $0.recordID }
let operation = CKFetchRecordsOperation(recordIDs: recordIDs)
operation.qualityOfService = .utility
operation.fetchRecordsCompletionBlock = { records, error in
if error != nil {
print(error?.localizedDescription)
}
let weeks = records?.values.map(Week.init) ?? []
DispatchQueue.main.sync {
completion(weeks)
}
}
Model.currentModel.publicDB.add(operation)
}
Once again: Never use semaphores with the CloudKit API.
First of all declare data source arrays always as non-optional empty arrays to get rid of unnecessary unwrapping the optional
var weeks = [Week]()
The mistake is that you don't use the fetched data at the right place.
As the closure is asynchronous you have to proceed inside the closure
fetchWeeks(for: challenge!.weeks!) { [weak self] weeks in
self?.weeks = weeks
self?.weeks = weeks.sorted(by: { $0.weekSequence < $1.weekSequence })
}
or simpler
fetchWeeks(for: challenge!.weeks!) { [weak self] weeks in
self?.weeks = weeks.sorted{ $0.weekSequence < $1.weekSequence }
}
And if you need to reload the table view do it also inside the closure
fetchWeeks(for: challenge!.weeks!) { [weak self] weeks in
self?.weeks = weeks.sorted{ $0.weekSequence < $1.weekSequence }
self?.rideTableView.reloadData()
}
To do so you have to call completion on the main thread
DispatchQueue.main.async {
completion(weeks)
}
And finally delete the ugly semaphore!
let semaphore = DispatchSemaphore(value: 0)
...
semaphore.signal()
...
semaphore.wait()
I'm using DispatchGroup.enter() and leave() to process a helper class's reverseG async function. Problem is clear, I'm using mainViewController's object to call mainViewControllers's dispatchGroup.leave() in helper class! Is there a way to do it?
Same code works when reverseG is declared in the main view controller.
class Geo {
var obj = ViewController()
static func reverseG(_ coordinates: CLLocation, _ completion: #escaping (CLPlacemark) -> ()) {
let geoCoder = CLGeocoder()
geoCoder.reverseGeocodeLocation(coordinates) { (placemarks, error) in
if let error = error {
print("error: \(error.localizedDescription)")
}
if let placemarks = placemarks, placemarks.count > 0 {
let placemark = placemarks.first!
completion(placemark) // set ViewController's properties
} else {
print("no data")
}
obj.dispatchGroup.leave() // ** ERROR **
}
}
}
Function call from main view controller
dispatchGroup.enter()
Geo.reverseG(coordinates, setValues) // completionHandler: setValues
dispatchGroup.notify(queue: DispatchQueue.main) {
// call another function on completion
}
Every leave call must have an associated enter call. If you call leave without having first called enter, it will crash. The issue here is that you're calling enter on some group, but reverseG is calling leave on some other instance of ViewController. I'd suggest passing the DispatchGroup as a parameter to your reverseG method. Or, better, reverseG shouldn't leave the group, but rather put the leave call inside the completion handler that reserveG calls.
dispatchGroup.enter()
Geo.reverseG(coordinates) { placemark in
defer { dispatchGroup.leave() }
guard let placemark = placemark else { return }
// use placemark here, e.g. call `setValues` or whatever
}
dispatchGroup.notify(queue: DispatchQueue.main) {
// call another function on completion
}
And
class Geo {
// var obj = ViewController()
static func reverseG(_ coordinates: CLLocation, completion: #escaping (CLPlacemark?) -> Void) {
let geoCoder = CLGeocoder()
geoCoder.reverseGeocodeLocation(coordinates) { placemarks, error in
if let error = error {
print("error: \(error.localizedDescription)")
}
completion(placemarks?.first)
// obj.dispatchGroup.leave() // ** ERROR **
}
}
}
This keeps the DispatchGroup logic at one level of the app, keeping your classes less tightly coupled (e.g. the Geo coder doesn't need to know whether the view controller uses dispatch groups or not).
Frankly, I'm not clear why you're using dispatch group at all if there's only one call. Usually you'd put whatever you call inside the completion handler, simplifying the code further. You generally only use groups if you're doing a whole series of calls. (Perhaps you've just simplified your code snippet whereas you're really doing multiple calls. In that case, a dispatch group might make sense. But then again, you shouldn't be doing concurrent geocode requests, suggesting a completely different pattern, altogether.
Passed dispatchGroup as parameter with function call and it worked.
Geo.reverseG(coordinates, dispatchGroup, setValues)
my two cents to show how can work:
(maybe useful for others..)
// Created by ing.conti on 02/02/21.
//
import Foundation
print("Hello, World!")
let r = AsyncRunner()
r.runMultiple(args: ["Sam", "Sarah", "Tom"])
class AsyncRunner{
static let shared = AsyncRunner()
let dispatchQueue = DispatchQueue(label: "MyQueue", qos:.userInitiated)
let dispatchGroup = DispatchGroup.init()
func runMultiple(args: [String]){
let count = args.count
for i in 0..<count {
dispatchQueue.async(group: dispatchGroup) { [unowned self] in
dispatchGroup.enter()
self.fakeTask(arg: args[i])
}
}
_ = dispatchGroup.wait(timeout: DispatchTime.distantFuture)
}
func fakeTask(arg: String){
for i in 0..<3 {
print(arg, i)
sleep(1)
}
dispatchGroup.leave()
}
}
Ok, pls pardon me if this might be a very elementary question, but how do I refresh my tableView with only new items added to Firebase?
I am trying to implement a 'pull to refresh' for my tableView and populate my tableView with new items that have been added to Firebase. At viewDidLoad, I call .observeSingleEvent(.value) to display the table.
At the refresh function, I call .observe(.childAdded). However doing so will make the app consistently listen for things added, making my app consistently reloadingData. How do I code it such that it only refreshes when I do the 'pull to refresh'? My code so far:
lazy var refresh: UIRefreshControl = {
let refresh = UIRefreshControl(frame: CGRect(x: 50, y: 100, width: 20, height: 20))
refresh.addTarget(self, action: #selector(refreshData), for: .valueChanged)
return refresh
}()
var eventsArray: [FIRDataSnapshot]! = []
override func viewDidLoad() {
super.viewDidLoad()
ref = FIRDatabase.database().reference()
ref.child("events").observeSingleEvent(of: .value, with: { (snapshot) in
for snap in snapshot.children {
self.eventsArray.insert(snap as! FIRDataSnapshot, at: 0)
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}) { (error) in
print(error.localizedDescription)
}
}
func refreshData() {
self.eventsArray = []
ref.child("events").observe(.childAdded, with: { (snapshot) in
print(snapshot)
self.eventsArray.insert(snapshot, at: 0)
DispatchQueue.main.async {
self.refresh.endRefreshing()
self.tableView.reloadData()
}
}) { (error) in
print(error.localizedDescription)
}
}
I also attempted observeSingleEvent(.childAdded) but it only pulls out one single entry (the very top entry) from Firebase.
I am not sure if my approach is correct in the first place. Any advice here pls, thanks.
Add
var lastKey: String? = nil
In the loop where you iterate over the children set the lastKey to the current snap's key.
lastKey = snap.key
Let me know if this works for you.
func refreshData() {
// Make sure you write a check to make sure lastKey isn't nil
ref.child("events").orderByKey().startAt(lastKey).observeSingleEvent(of: .value, with: { (snapshot) in
// You'll have to get rid of the first childsince it'll be a duplicate, I'm sure you can figure that out.
for snap in snapshot.children {
self.eventsArray.insert(snap as! FIRDataSnapshot, at: 0)
}
DispatchQueue.main.async {
self.refresh.endRefreshing()
self.tableView.reloadData()
}
}) { (error) in
print(error.localizedDescription)
}
}
I am working on a social media app where I have implemented basic like/unlike/follow/unfollow features. I am using Parse as a backend. Here is what happens.
I am getting the posts liked by the user from my backend with this function in my ViewDidLoad, this seems to be working:
func getLikes() {
var query = PFQuery(className: "Likes")
query.whereKey("Liker", equalTo: PFUser.currentUser()["displayName"])
query.findObjectsInBackgroundWithBlock { (results:[AnyObject]!, error:NSError!) -> Void in
if error == nil {
for result in results {
self.likedPosts.append(result["likedItem"] as String)
}
println(self.likedPosts)
self.homeTableView.reloadData()
}
}
}
Then, in my cellForRowAtIndexPath, I set the title and the function of the likeButton according to whether or not the id of the post for that cell is contained in the array of liked posts:
if contains(self.likedPosts, self.id[indexPath.row]) {
cell.thankButton.setTitle("Unlike", forState: UIControlState.Normal)
cell.thankButton.addTarget(self, action: "unlike:", forControlEvents: UIControlEvents.TouchUpInside)
}
else {
cell.thankButton.setTitle("Like", forState: UIControlState.Normal)
cell.thankButton.addTarget(self, action: "like:", forControlEvents: UIControlEvents.TouchUpInside)
}
This works fine, as the buttons in each cell display the right title in accordance with the backend. They also have the right function, which code is as follows:
func like(sender:UIButton) {
var id = sender.tag
var postId = self.id[id]
var likeAction = PFObject(className: "Likes")
likeAction["Liker"] = PFUser.currentUser()["displayName"]
likeAction["likedItem"] = postId
likeAction.saveInBackgroundWithBlock { (success:Bool!, error:NSError!) -> Void in
if error == nil {
if success == true {
self.homeTableView.reloadData()
println("liked")
}
}
}
}
func unlike(sender:UIButton) {
var id = sender.tag
var postId = self.id[id]
var query = PFQuery(className: "Likes")
query.whereKey("Liker", equalTo: PFUser.currentUser()["displayName"])
query.whereKey("likedItem", equalTo: postId)
var targetId:String!
query.findObjectsInBackgroundWithBlock { (results:[AnyObject]!, error:NSError!) -> Void in
if error == nil {
targetId = results[0].objectId
var likeObject = PFObject(withoutDataWithClassName: "Likes", objectId: targetId)
likeObject.deleteInBackgroundWithBlock({ (success:Bool!, error:NSError!) -> Void in
if error == nil {
if success == true {
self.homeTableView.reloadData()
println("liked")
}
}
})
}
}
}
However, reloadData never works, and the buttons retain their title and function, even though it should change (as the backend registers the change). I am aware that a recurring reason for reloadData() not to work is that it is not in the right thread, but as far as I can tell, it is here. The "println()" in both functions actually works every time, the backend registers the change every time, but reloadData() never works.
Any idea is greatly appreciated.Thank you!
It seems not in the main thread. The println works well in any thread, but when you update your UI you should do that on the main thread. Try wrapping your code in a block like this:
NSOperationQueue.mainQueue.addOperationWithBlock {
self.homeTableView.reloadData()
}
OK the problem was actually dumb, but I'm posting in case anyone who is a noob like me gets the issue: basically I was calling reloadData without updating anything, since the array of liked posts is refreshed in getLikes(). I put self.getLikes() instead of reloadData() and it works fine.