I'm using HanekeSwift to retrieve cached data and then set it to labels in a swipeView every time the view appears. My code retrieves the data no problem, but because cache.fetch() is asynchronous, when I call my method to update the view, my labels are set to nil. Is there anyway to tell swift to wait until my cached data is retrieved before loading the view?
See code below:
override func viewWillAppear(animated: Bool) {
updateEntries() // updates entries from cache when view appears
}
func updateEntries() {
guard let accessToken = NSUserDefaults.standardUserDefaults().valueForKey("accessToken") as? String else { return }
guard let cachedEntryKey = String(accessToken) + "food_entries.get" as? String else { return }
cache.fetch(key: cachedEntryKey).onSuccess { data in
...
// if successful, set labels in swipeView to data retrieved from cache
...
dispatch_group_leave(dispatchGroup)
} .onFailure { error in
print(error)
...
// if unsuccessful, call servers to retrieve data, set labels in swipeView to that data
...
dispatch_group_leave(dispatchGroup)
}
}
When I step through the above code, it always displays the view and then steps into the cache block. How do I make viewWillAppear() allow updateEntries() to complete and not return out of it until the cache block is executed? Thanks a ton in advance!
Update 1:
The solution below is working pretty well and my calls are made in the correct sequence (my print statement in the notify block executes after the cache retrieval), but my views only update their labels with non-nil values when the server is called. Maybe I'm lumping the wrong code in the notify group?
override func viewWillAppear(animated: Bool) {
self.addProgressHUD()
updateEntries() // updates entries from cache when view appears
}
func updateEntries() {
guard let accessToken = NSUserDefaults.standardUserDefaults().valueForKey("accessToken") as? String else { return }
guard let cachedEntryKey = String(accessToken) + "food_entries.get" as? String else { return }
let dispatchGroup = dispatch_group_create()
dispatch_group_enter(dispatchGroup)
dispatch_group_async(dispatchGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
cache.fetch(key: cachedEntryKey).onSuccess { data in
...
// if successful, set labels in swipeView to data retrieved from cache
...
} .onFailure { error in
print(error)
...
// if unsuccessful, call servers to retrieve data, set labels in swipeView to that data
...
}
}
dispatch_group_notify(dispatchGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
print("Retrieved Data")
self.removeProgressHUD()
}
}
Update 2:
Also, I'm getting this warning in the console when I switch views. I think I'm locking up the main thread with the above code
"This application is modifying the autolayout engine from a background thread, which can lead to engine corruption and weird crashes. This will cause an exception in a future release."
Note:
enter group before calling asynchronous method
leave group is each of the respective completion/failure handlers
dispatch UI updates in notify block to main queue
Thus:
func updateEntries() {
guard let accessToken = NSUserDefaults.standardUserDefaults().valueForKey("accessToken") as? String else { return }
guard let cachedEntryKey = String(accessToken) + "food_entries.get" as? String else { return }
let group = dispatch_group_create()
dispatch_group_enter(group)
cache.fetch(key: cachedEntryKey).onSuccess { data in
...
// if successful, set labels in swipeView to data retrieved from cache
...
dispatch_group_leave(group)
} .onFailure { error in
print(error)
...
// if unsuccessful, call servers to retrieve data, set labels in swipeView to that data
...
dispatch_group_leave(group)
}
dispatch_group_notify(group, dispatch_get_main_queue()) {
print("Retrieved Data")
self.removeProgressHUD()
}
}
Ok suggestions from everyone helped a ton on this. Think I got it. I need to make sure my cache block isn't blocking the main queue. See code below
EDIT
Thanks to #Rob for helping me make the proper adjustments to make this work
let dispatchGroup = dispatch_group_create()
dispatch_group_enter(dispatchGroup)
cache.fetch(key: cachedEntryKey).onSuccess { data in
...
// if successful, set labels in swipeView to data retrieved from cache
...
dispatch_group_leave(dispatchGroup)
} .onFailure { error in
print(error)
...
// if unsuccessful, call servers to retrieve data, set labels in swipeView to that data
...
dispatch_group_leave(dispatchGroup)
}
dispatch_group_notify(dispatchGroup, dispatch_get_main_queue()) {
print("Retrieved Data")
self.removeProgressHUD()
}
Here's simple example that you can stage a loading screen. I just create a alert view, also you can create your custom loading indicator view instead.
let alert = UIAlertController(title: "", message: "please wait ...", preferredStyle: .alert)
override func viewWillAppear(animated: Bool) {
self.present(alert, animated: true, completion: nil)
updateEntries() // updates entries from cache when view appears
}
func updateEntries() {
guard let accessToken = UserDefaults.standard.value(forKey: "accessToken") as? String,
let cachedEntryKey = (accessToken + "food_entries.get") as? String else {
return
}
cache.fetch(key: cachedEntryKey).onSuccess { data in
...
// update value in your UI
alert.dismiss(animated: true, completion: nil)
...
} .onFailure { error in
print(error)
...
// if unsuccessful, call servers to retrieve data, set labels in swipeView to that data
...
}
}
While I entirely agree with #ozgur about displaying some sort of loading indicator from a UX standpoint, I figured the benefit of learning how to use Grand Central Dispatch (Apple's native solution to asynchronous waiting) might help you in the long-term.
You can use dispatch_groups to wait for a block(s) of code to completely finish running before running a completion handler of some sort.
From Apple's documentation:
A dispatch group is a mechanism for monitoring a set of blocks. Your application can monitor the blocks in the group synchronously or asynchronously depending on your needs. By extension, a group can be useful for synchronizing for code that depends on the completion of other tasks.
[...]
The dispatch group keeps track of how many blocks are outstanding, and GCD retains the group until all its associated blocks complete execution.
Here's an example of dispatch_groups in action:
let dispatchGroup = dispatch_group_create()
dispatch_group_async(dispatchGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
// Run whatever code you need to in here. It will only move to the final
// dispatch_group_notify block once it reaches the end of the block.
}
dispatch_group_notify(dispatchGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
// Code in here only runs once all dispatch_group_async blocks associated
// with the dispatchGroup have finished completely.
}
The great part about dispatch_groups are that they allow you to run multiple asynchronous blocks at the same time and wait for all of them to finish before running the final completion handler. In other words, you can associate as many dispatch_group_async blocks with the dispatchGroup as you want.
If you wanted to go for the loading indicator approach (which you should), you can run code to display the loading indicator, then move into a dispatch_group with a completion handler to remove the loading indicator and load data into view once the dispatch_group completes.
Related
I am trying to execute an asynchronous request as part of a search result updater in my app.
I wrote the following code
func updateSearchResults(for searchController: UISearchController) {
guard let text = searchController.searchBar.text else {return}
let threadingContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
threadingContext.parent = self.context
DispatchQueue.global(qos: .userInitiated).async {
let fetchRequest = MyObject.fetchRequest() as NSFetchRequest<MyObject>
fetchRequest.predicate = get_predicate(text)
do {
let objects = try threadingContext.fetch(fetchRequest).map({ object in
return object.objectID
})
}
catch {return}
DispatchQueue.main.async {
// Pass results to the search view controller
}
}
}
but the UI is still slow (even if I don't do any display update), and looking at the Time profiler, I see that my main thread is spending 80% of its time on the following:
So it seems that my request is still being dispatched onto the main thread, which I don't understand. Would anyone see my mistake?
(I tried a few various on the above e.g. using threadingContext.perform but for the same result)
Ok, I understood it, and I should have read Apple's documentation, but basically
If a context’s parent store is another managed object context, fetch and save operations are mediated by the parent context instead of a coordinator.
This is slightly subtle, but my construction would have been useful if the operations performed on the fetch request, rather than the fetch request itself, had been slow.
The solution is to set threadingContext.persistentStoreCoordinator instead.
I'm getting info from an API using the following function where I pass in a string of a word. Sometimes the word doesn't available in the API if it doesn't available I generate a new word and try that one.
The problem is because this is an asynchronous function when I launch the page where the value from the API appears it is sometimes empty because the function is still running in the background trying to generate a word that exists in the API.
How can I make sure the page launches only when the data been received from the api ?
static func wordDefin (word : String, completion: #escaping (_ def: String )->(String)) {
let wordEncoded = word.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
let uri = URL(string:"https://dictapi.lexicala.com/search?source=global&language=he&morph=false&text=" + wordEncoded! )
if let unwrappedURL = uri {
var request = URLRequest(url: unwrappedURL);request.addValue("Basic bmV0YXlhbWluOk5ldGF5YW1pbjg5Kg==", forHTTPHeaderField: "Authorization")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
do {
if let data = data {
let decoder = JSONDecoder()
let empty = try decoder.decode(Empty.self, from: data)
if (empty.results?.isEmpty)!{
print("oops looks like the word :" + word)
game.wordsList.removeAll(where: { ($0) == game.word })
game.floffWords.removeAll(where: { ($0) == game.word })
helper.newGame()
} else {
let definition = empty.results?[0].senses?[0].definition
_ = completion(definition ?? "test")
return
}
}
}
catch {
print("connection")
print(error)
}
}
dataTask.resume()
}
}
You can't stop a view controller from "launching" itself (except not to push/present/show it at all). Once you push/present/show it, its lifecycle cannot—and should not—be stopped. Therefore, it's your responsibility to load the appropriate UI for the "loading state", which may be a blank view controller with a loading spinner. You can do this however you want, including loading the full UI with .isHidden = true set for all view objects. The idea is to do as much pre-loading of the UI as possible while the database is working in the background so that when the data is ready, you can display the full UI with as little work as possible.
What I'd suggest is after you've loaded the UI in its "loading" configuration, download the data as the final step in your flow and use a completion handler to finish the task:
override func viewDidLoad() {
super.viewDidLoad()
loadData { (result) in
// load full UI
}
}
Your data method may look something like this:
private func loadData(completion: #escaping (_ result: Result) -> Void) {
...
}
EDIT
Consider creating a data manager that operates along the following lines. Because the data manager is a class (a reference type), when you pass it forward to other view controllers, they all point to the same instance of the manager. Therefore, changes that any of the view controllers make to it are seen by the other view controllers. That means when you push a new view controller and it's time to update a label, access it from the data property. And if it's not ready, wait for the data manager to notify the view controller when it is ready.
class GameDataManager {
// stores game properties
// updates game properties
// does all thing game data
var score = 0
var word: String?
}
class MainViewController: UIViewController {
let data = GameDataManager()
override func viewDidLoad() {
super.viewDidLoad()
// when you push to another view controller, point it to the data manager
let someVC = SomeOtherViewController()
someVC.data = data
}
}
class SomeOtherViewController: UIViewController {
var data: GameDataManager?
override func viewDidLoad() {
super.viewDidLoad()
if let word = data?.word {
print(word)
}
}
}
class AnyViewController: UIViewController {
var data: GameDataManager?
}
I'm trying to download images from my firebase database and load them into collectionviewcells. The images download, however I am having trouble having them all download and load asynchronously.
Currently when I run my code the last image downloaded loads. However, if I update my database the collection view updates and the new last user profile image also loads in but the remainder are missing.
I'd prefer to not use a 3rd party library so any resources or suggestions would be greatly appreciated.
Here's the code that handles the downloading:
func loadImageUsingCacheWithUrlString(_ urlString: String) {
self.image = nil
// checks cache
if let cachedImage = imageCache.object(forKey: urlString as NSString) as? UIImage {
self.image = cachedImage
return
}
//download
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
//error handling
if let error = error {
print(error)
return
}
DispatchQueue.main.async(execute: {
if let downloadedImage = UIImage(data: data!) {
imageCache.setObject(downloadedImage, forKey: urlString as NSString)
self.image = downloadedImage
}
})
}).resume()
}
I believe the solution lies somewhere in reloading the collectionview I just don't know where exactly to do it.
Any suggestions?
EDIT:
Here is where the function is being called; my cellForItem at indexpath
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: userResultCellId, for: indexPath) as! FriendCell
let user = users[indexPath.row]
cell.nameLabel.text = user.name
if let profileImageUrl = user.profileImageUrl {
cell.profileImage.loadImageUsingCacheWithUrlString(profileImageUrl)
}
return cell
}
The only other thing that I believe could possibly affect the images loading is this function I use to download the user data, which is called in viewDidLoad, however all the other data downloads correctly.
func fetchUser(){
Database.database().reference().child("users").observe(.childAdded, with: {(snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
let user = User()
user.setValuesForKeys(dictionary)
self.users.append(user)
print(self.users.count)
DispatchQueue.main.async(execute: {
self.collectionView?.reloadData()
})
}
}, withCancel: nil)
}
Current Behavior:
As for the current behavior the last cell is the only cell that displays the downloaded profile image; if there are 5 cells, the 5th is the only one that displays a profile image. Also when I update the database, ie register a new user into it, the collectionview updates and displays the newly registered user correctly with their profile image in addition to the old last cell that downloaded it's image properly. The rest however, remain without profile images.
I know you found your problem and it was unrelated to the above code, yet I still have an observation. Specifically, your asynchronous requests will carry on, even if the cell (and therefore the image view) have been subsequently reused for another index path. This results in two problems:
If you quickly scroll to the 100th row, you are going to have to wait for the images for the first 99 rows to be retrieved before you see the images for the visible cells. This can result in really long delays before images start popping in.
If that cell for the 100th row was reused several times (e.g. for row 0, for row 9, for row 18, etc.), you may see the image appear to flicker from one image to the next until you get to the image retrieval for the 100th row.
Now, you might not immediately notice either of these are problems because they will only manifest themselves when the image retrieval has a hard time keeping up with the user's scrolling (the combination of slow network and fast scrolling). As an aside, you should always test your app using the network link conditioner, which can simulate poor connections, which makes it easier to manifest these bugs.
Anyway, the solution is to keep track of (a) the current URLSessionTask associated with the last request; and (b) the current URL being requested. You can then (a) when starting a new request, make sure to cancel any prior request; and (b) when updating the image view, make sure the URL associated with the image matches what the current URL is.
The trick, though, is when writing an extension, you cannot just add new stored properties. So you have to use the associated object API to associate these two new stored values with the UIImageView object. I personally wrap this associated value API with a computed property, so that the code for retrieving the images does not get too buried with this sort of stuff. Anyway, that yields:
extension UIImageView {
private static var taskKey = 0
private static var urlKey = 0
private var currentTask: URLSessionTask? {
get { objc_getAssociatedObject(self, &UIImageView.taskKey) as? URLSessionTask }
set { objc_setAssociatedObject(self, &UIImageView.taskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
private var currentURL: URL? {
get { objc_getAssociatedObject(self, &UIImageView.urlKey) as? URL }
set { objc_setAssociatedObject(self, &UIImageView.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
func loadImageAsync(with urlString: String?, placeholder: UIImage? = nil) {
// cancel prior task, if any
weak var oldTask = currentTask
currentTask = nil
oldTask?.cancel()
// reset image view’s image
self.image = placeholder
// allow supplying of `nil` to remove old image and then return immediately
guard let urlString = urlString else { return }
// check cache
if let cachedImage = ImageCache.shared.image(forKey: urlString) {
self.image = cachedImage
return
}
// download
let url = URL(string: urlString)!
currentURL = url
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
self?.currentTask = nil
// error handling
if let error = error {
// don't bother reporting cancelation errors
if (error as? URLError)?.code == .cancelled {
return
}
print(error)
return
}
guard let data = data, let downloadedImage = UIImage(data: data) else {
print("unable to extract image")
return
}
ImageCache.shared.save(image: downloadedImage, forKey: urlString)
if url == self?.currentURL {
DispatchQueue.main.async {
self?.image = downloadedImage
}
}
}
// save and start new task
currentTask = task
task.resume()
}
}
Also, note that you were referencing some imageCache variable (a global?). I would suggest an image cache singleton, which, in addition to offering the basic caching mechanism, also observes memory warnings and purges itself in memory pressure situations:
class ImageCache {
private let cache = NSCache<NSString, UIImage>()
private var observer: NSObjectProtocol?
static let shared = ImageCache()
private init() {
// make sure to purge cache on memory pressure
observer = NotificationCenter.default.addObserver(
forName: UIApplication.didReceiveMemoryWarningNotification,
object: nil,
queue: nil
) { [weak self] notification in
self?.cache.removeAllObjects()
}
}
deinit {
NotificationCenter.default.removeObserver(observer!)
}
func image(forKey key: String) -> UIImage? {
return cache.object(forKey: key as NSString)
}
func save(image: UIImage, forKey key: String) {
cache.setObject(image, forKey: key as NSString)
}
}
A bigger, more architectural, observation: One really should decouple the image retrieval from the image view. Imagine you have a table where you have a dozen cells using the same image. Do you really want to retrieve the same image a dozen times just because the second image view scrolled into view before the first one finished its retrieval? No.
Also, what if you wanted to retrieve the image outside of the context of an image view? Perhaps a button? Or perhaps for some other reason, such as to download images to store in the user’s photos library. There are tons of possible image interactions above and beyond image views.
Bottom line, fetching images is not a method of an image view, but rather a generalized mechanism of which an image view would like to avail itself. An asynchronous image retrieval/caching mechanism should generally be incorporated in a separate “image manager” object. It can then detect redundant requests and be used from contexts other than an image view.
As you can see, the asynchronous retrieval and caching is starting to get a little more complicated, and this is why we generally advise considering established asynchronous image retrieval mechanisms like AlamofireImage or Kingfisher or SDWebImage. These guys have spent a lot of time tackling the above issues, and others, and are reasonably robust. But if you are going to “roll your own,” I would suggest something like the above at a bare minimum.
I have a very big problem. I use Parse cloud system. When i fetch images with using "dispatch_semaphore" from parse, the main thread is locked. However, i think, i don't use main thread when fetching images. Normally, the task should be executed order by section A,B,C then D but app locked in section B.
Thanks.
let semaphore1:dispatch_semaphore_t = dispatch_semaphore_create(0)
let semaphore2:dispatch_semaphore_t = dispatch_semaphore_create(0)
let userquery = PFQuery(className: "_User")
userquery.findObjectsInBackground().continueWithSuccessBlock { (task) -> AnyObject? in
let results = task.result as! NSArray
for objectarray in results
{
let object = objectarray as! PFObject
let username = object["username"] as! String
let userpictureThumbnail = object["userPhotoThumbnail"] as! PFFile
userpictureThumbnail.getDataInBackground().continueWithSuccessBlock({ (task2) -> AnyObject? in
let result = task2.result as! NSData
let image = UIImage(data: result)
let imageThumbnail = image
// Section C-) Below codes must be executed but main thread is locked by Section B.
Model.sharedInstance.friendsPictureModel.addItem(username,FriendImageThumbnail:imageThumbnail!)
dispatch_semaphore_signal(semaphore2)
return nil
})
// Section B-) Second, enter the below code . And lock main thread then app freezed.
dispatch_semaphore_wait(semaphore2, DISPATCH_TIME_FOREVER)
}
dispatch_semaphore_signal(semaphore1)
return nil
}
// Section A-) When the block("userquery.findObjectsInBackground().continueWithSuccessBlock") is executed, enter the below code firstly.
dispatch_semaphore_wait(semaphore1, DISPATCH_TIME_FOREVER)
// Section D-) Below codes must be executed in the last.
self.collectionview.reloadData()
You should not be using semaphores here. Your section A is (presumably) running on the main thread, and the wait will cause it to block until the semaphore is signalled.
You could remove all the semaphore code and just dispatch self.collectionview.reloadData() onto the main thread in where semaphore2 is currently being signalled. However, you also have a problem that addItem is being called in the background, and it's probably not thread safe.
On the assumption that your example is a simplification of your specific problem, you probably have some separation between the viewController and userquery (let's call it findTheObjects. So at the moment you would have something like:
myObjectFinder.findTheObjects()
In this case, you should pass in your own completion block, along the lines of:
myObjectFinder.findTheObjects(completion: {
(username, imageThumbnail?) -> Void in
dispatch_async(dispatch_get_main_queue(), {
// do something with the results like...
Model.sharedInstance.friendsPictureModel.addItem(username,FriendImageThumbnail:theResults.imageThumbnail!)
self.collectionview.reloadData()
})
}
This completion block would then be called from your Section C.
I have read the other answers but couldn't find a suitable solution.
I have a product which is uploaded to server only if all IMAGES belonging to that product have finished uploading. The product's details (along with the images) are filled on view controller 1 and then he is taken to the next screen (view controller 2), regardless of whether all images have finished uploading or not. MY VC1 completes the product upload process like this.
let areAllImagesUploaded = RetailProductsService.sharedInstance.checkProductImageDependency(realm!, uuid: retailProduct.getClientId())
if areAllImagesUploaded {
uploadProductToServer(retailProduct)
} else {
do {
try realm = Realm()
RetailProductsService.sharedInstance.updateSyncStatusForSellerProduct(realm!, clientId: retailProduct.getClientId(), syncStatus: ProductSyncStatus.SYNC_FAILED)
let groupT = dispatch_group_create()
for sellerSKU in retailProduct.sellersSKUs {
for productImage in sellerSKU.productImages {
dispatch_group_enter(groupT)
let imageUploadInfo = ImageUploadInfo(productId: retailProduct.getClientId(), imageId: sellerSKU.getId(),imageData: productImage.imageData, uploadURL: ServerConfig.RETAIL_SERVER_UPLOAD_FILE_URL)
ImageUploadManager.sharedInstance.queueImageForUpload(imageUploadInfo, completion: { (success, error) -> Void in
dispatch_group_leave(groupT)
})
dispatch_group_notify(groupT, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), { () -> Void in
//self.uploadProduct(retailProduct)
self.uploadProductToServer(retailProduct) // Fails here
})
}
}
} catch {
print("Error in saving product.")
}
}
I have marked the line on which I get this error. My app has moved to the next view controller while this function in view controller 1 continues uploading images and as soon as all images associated the product are uploaded to server, it tries to upload the product. However it fails with this exception.
Terminating app due to uncaught exception 'RLMException', reason: 'Realm accessed from incorrect thread'
Please help!
Realm Objects cannot be accessed from different threads. Your retailProduct is created or fetched from Realm storage by threadX by than you switch to some other thread (threadY) by invoking dispatch_group_notify. To fix the exception you might want to do something like this:
I assume that your retailProduct object is of type RetailProduct and has an id property used as primary key in Realm storage. Of course you can fetch you retailProduct by using another query than that.
let retailProductId = retailProduct.id
dispatch_group_notify(groupT, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), { () -> Void in
// threadY executing this lines
if let retailProduct = realm.objectForPrimaryKey(RetailProduct.self, key: retailProductId){
self.uploadProductToServer(retailProduct)
}
})
We need to understand the fact Realm Objects cannot be accessed from different threads. What does this means and how to workout this issue.
First, realm objects cannot be access from different thread means, one instance of thread defined in one thread cannot be access from different thread. What we should do actually is we need to have different instance of realm instance for each thread.
For eg. let's look at following e.g. where we insert 50 records in database asynchronously in background thread upon button click and we add notification block in main thread to update the no of people in count label. Each thread (main and background ) have its own instance of realm object to access Realm Database. Because Realm Database is thread safe.
class Person: Object {
dynamic var name = ""
convenience init(_ name: String) {
self.init()
self.name = name
}
}
override func viewDidAppear(_ animated: Bool) {
let realmMain = try! Realm ()
self.people = realmMain.objects(Person.self)
self.notification = self.people?.addNotificationBlock{ [weak self] changes in
print("UI update needed")
guard let countLabel = self?.countLabel else {
return
}
countLabel.text = "Total People: \(String(describing: self?.people?.count))"
}
}
#IBAction func addHandler(_ sender: Any) {
print(#function)
let backgroundQueue = DispatchQueue(label: "com.app.queue",
qos: .background,
target: nil)
backgroundQueue.async {
print("Dispatched to background queue")
let realm = try! Realm()
try! realm.write {
for i in 1..<50 {
let name = String(format: "rajan-%d", i)
//print(#function, name)
realm.add(Person(name))
}
}
}
}