I'm using a UIProgressView and it uses the observedProgress property. I then have a variable of type Progress that is observed.
Now I'm writing to Core Data on a background thread and then updating the completedUnitCount but it's crashing.
Here's the code:
var downloadProgress: Progress
init() {
downloadProgress = Progress()
}
func saveStuff() {
let stuff: [[String: Any]] = //some array of dictionaries
downloadProgress.totalUnitCount = Int64(stuff.count)
persistentContainer.performBackgroundTask { (context) in
for (index, item) in stuff.enumerated() {
// create items to be saved
context.perform {
do {
try context.save()
self.downloadProgress.completedUnitCont = Int64(index + 1)
} catch {
// handle error
}
}
}
}
}
So it's crashing on line self.downloadProgress.completedUnitCont = Int64(index + 1). I realise in writing this that I should also be using weak or unowned self to stop retain cycles, but are there other issues?
All the UI related code have to be performed from the main thread, so you have to dispatch call self.downloadProgress.completedUnitCont = Int64(index + 1) to main thread. Something like this:
DispatchQueue.main.async {
self.downloadProgress.completedUnitCont = Int64(index + 1)
}
Apple Says:
Updating UI on a thread other than the main thread is a common mistake that can result in missed UI updates, visual defects, data corruptions, and crashes.
So whenever you are performing any task on background thread and need to make any ui updates in the process, use all those code inside the following block.
DispatchQueue.main.async{ <uiupdate code here> }
Related
I receive data items from my API, on a specific serial queue, that I use to calculate the view models to display in a collection view. The simplified code is:
var viewModels: [ViewModel] = [] // read and written on main queue
var _viewModels: [ViewModel] = [] // written in background, read in main queue
func didReceive(items: [Item]) {
// called on the same background serial queue
_viewModels = items.map { ViewModel($0) }
DispatchQueue.main.async {
self.viewModels = self._viewModels
self.updateCollectionView {
// UI update has finished
}
}
}
didReceive can be called at any time.
_viewModels is written always on the same background queue. After that I switch to main queue to cache the computed view models and use it in data source for displaying.
Can self.viewModels = self._viewModels lead to crashes? Do I have to use some kind of locking mechanism? If yes, how does it work when main thread is involved?
Crashes not necessarily, but this can lead to inconsistency. As _viewModels is declared outside the scope of didReceive, it is a shared resource that can be modified each time you enter didReceive. After dispatching this to the main thread, some other thread can enter in didReceive and change your value. So when you assign it to self.viewModels, you would get the "second" value.
I believe that declaring _viewModels inside didReceive would fix your issue, as you would have a reference for each run of didReceive:
var viewModels: [ViewModel] = [] // read and written on main queue
func didReceive(items: [Item]) {
// called on the same background serial queue
let _viewModels = items.map { ViewModel($0) }
DispatchQueue.main.async {
self.viewModels = _viewModels
self.updateCollectionView {
// UI update has finished
}
}
}
If you are not allowed to do this, try taking a look at Actors:
https://www.avanderlee.com/swift/actors/
I am trying to create an iOS app to get data from API that I want to show the user in a Label.
So far I have this:
func getJoke(completion: #escaping (ChuckNorrisResponse) -> ()) {
URLSession.shared.dataTask(with: URL(string: url)!) { data, response, error in
guard let data = data, error == nil else {
print("Something fucked up")
return
}
var result: ChuckNorrisResponse?
do {
result = try JSONDecoder().decode(ChuckNorrisResponse.self, from: data)
} catch {
print("Fucked up to convert \(error.localizedDescription)")
}
guard let joke = result else {
return
}
completion(joke)
}.resume()
}
And in the ViewController
func setNewJoke() {
jokesProvier.getJoke { joke in
self.JokeLabel.text = joke.value
}
But it doesn't like that I try to edit the text in the Label inside the closure.
It shows error - UILabel.Text must be used from main thread only.
I cannot find anywhere how should I do this properly so it works.
Thanks in advance
Basically, as Aaron stated - you have to pass the closure to the main thread with DispatchQueue.main.async. The reason is that URLSession.shared.dataTask completionHandler runs on the thread different from main and self.JokeLabel.text = joke.value is an UI update - you're changing the text on the label, and UIKit requires you to update the screen on the main thread! That's why you have to pass label update to the main thread. Trying to update it on the thread different from main will result in undefined behaviour - it may work, it may freeze, it may crash.So, whenever you're doing something on the background thread and at some point want to update the UI always pass this job to the main thread Hope this helps
I'd recommend calling the completion handler on the main thread. So instead of
completion(joke)
do
DispatchQueue.main.async { completion(joke) }
I use coreData as persistent store.
To read data, I use (only essential parts are shown):
func fetchShoppingItems(completion: #escaping (Set<ShoppingItem>?, Error?) -> Void) {
persistentContainer.performBackgroundTask { (backgroundManagedContext) in
let fetchRequest: NSFetchRequest<CDShoppingItem> = CDShoppingItem.fetchRequest()
var shoppingItems: Set<ShoppingItem> = []
do {
let cdShoppingItems: [CDShoppingItem] = try backgroundManagedContext.fetch(fetchRequest)
for nextCdShoppingItem in cdShoppingItems {
nextCdShoppingItem.managedObjectContext!.performAndWait {
Thread.sleep(forTimeInterval: 0.1) // This seems to be required
let nextShoppingItem = ShoppingItem.init(name: nextCdShoppingItem.name!)
shoppingItems.insert(nextShoppingItem)
} // performAndWait
} // for all cdShoppingItems
completion(shoppingItems, nil)
return
} catch let error as NSError {
completion(nil, error)
return
} // fetch error
} // performBackgroundTask
} // fetchShoppingItems
To test the coreData implementation, I wrote a unit test that creates multiple threads that write to and read from coreData concurrently.
This test runs only successfully, if the instruction
Thread.sleep(forTimeInterval: 0.1) // This seems to be required
is inserted in the performAndWait closure.
If it is commented out, nextCdShoppingItem is often read back with nil attributes, and the function crashes due to the forced unwrap.
I am not sure, if nextCdShoppingItem.managedObjectContext!.performAndWait is correct or if I had to use backgroundManagedContext.performAndWait, but with backgroundManagedContext the effect is the same.
I do not understand why inserting a small delay before accessing an attribute of a managed object is necessary to avoid the problem.
Any hint is welcome!
EDIT:
I investigated the issue further, and found the following:
Every time nextCdShoppingItem is read back by the background thread (called read thread below) as nil, there is also another background thread that tries to save its own managedContext after all records in its managedContext have been deleted (called write thread below).
Apparently the read thread tries to fetch a record that has just been deleted by the write thread.
So the problem is definitively a multithreading issue, and I found a solution (see my answer below).
performAndWait will add the block to the queue and schedule it to run, just like perform, but performAndWait will not return until the block is complete. Since you are inside a loop of cdShoppingItems, the loop does not stop and wait for the block to return. By adding the thread sleep, you are essentially slowing down the loop and giving core data enough time to complete its fetch. The forced unwrap crash is probably an indication that it's lost its nextCdShoppingItem reference.
I would consider refactoring where you do not need to query core data inside a loop. If it's possible, add the name attribute to CDShoppingItem so you don't have to fetch it to build a ShoppingItem object.
Edit: took a stab at a refactor although I don't know your exact use case:
func fetchShoppingItems(completion: #escaping (Set<ShoppingItem>?, Error?) -> Void) {
persistentContainer.performBackgroundTask { (backgroundManagedContext) in
let fetchRequest: NSFetchRequest<CDShoppingItem> = CDShoppingItem.fetchRequest()
do {
var shoppingItems: Set<ShoppingItem> = []
let cdShoppingItems: [CDShoppingItem] = try backgroundManagedContext.fetch(fetchRequest)
for nextCdShoppingItem in cdShoppingItems {
if let name = nextCdShoppingItem.name {
let nextShoppingItem = ShoppingItem.init(name: name)
shoppingItems.insert(nextShoppingItem)
}
}
completion(shoppingItems, nil)
} catch let error as NSError {
print("Error fetching CDShoppingItem: \(error)")
completion(nil, error)
} // fetch error
return
} // performBackgroundTask
} // fetchShoppingItems
To prevent the multithreading issue, I tried 2 things:
1) Since iOS10, a persistentStore of SQL type maintains a connection pool for concurrent access to the pool, and it is possible to set a maximum pool size, see the WWDC video. I did so using
private lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: modelName)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// ...
} else {
storeDescription.setOption(NSNumber(1), forKey: NSPersistentStoreConnectionPoolMaxSizeKey)
}
})
return container
}()
to prevent concurrent access to the persistentStore. Unfortunately, this did not solve the problem for an unknown reason.
2) I then tried to serialize read and write operations by setting up a serial queue:
private let coreDataSerialQueue = DispatchQueue(label: "com.xxx.ShopEasy.coreDataManager") // serial by default
It is used for the read and write accesses in the following way:
coreDataSerialQueue.async {
let backgroundManagedContext = self.persistentContainer.newBackgroundContext()
backgroundManagedContext.performAndWait {
// …
} // performAndWait
} // coreDataSerialQueue.async
This did solve the problem.
Please note that it would be wrong to use
coreDataSerialQueue.async {
self.persistentContainer.performBackgroundTask { (backgroundManagedContext) in
// …
} // performBackgroundTask
} // coreDataSerialQueue.async
because performBackgroundTask would fork another asynchronous thread and thus break the serialization.
I have recently reported a crash in my app, and I've found out what is happening and I need some help/best practices/best approach to this issue.
I have a pushed UICollectionViewController that on viewDidLoad queries the server to fetch some data to fill the UICollectionView.
My problem here is, if I push this UICollectionViewController and then tap the back button fast - the background thread still continues to fetch the server data, but when the data is fetched I update the UICollectionView with the performBatchUpdates() and my app crashes.
Here it happens because the app is attempting to reload data on a view that's not visible anymore.
What's the best practice here?
Is there any way to "abort" collection view updates if I'm moving back to the previous VC?
something like:
if self.isMovingFromParentViewController { /* abort any update here? */ }
Thanks
You can use DispatchWorkItem for achieving this as follows
let backgroundQueue = DispatchQueue.global()
var backgroundTask: DispatchWorkItem!
backgroundTask = DispatchWorkItem { [weak self] in
// Perform background task
if !backgroundTask.isCancelled {
return to main Queue
}
backgroundTask = nil // resolve strong reference cycle
}
backgroundQueue.async(execute: backgroundTask)
// When you want to cancel the task
backgroundQueue.async { [weak backgroundTask] in
backgroundTask?.cancel()
}
This is desirable in many cases where we should abort all the Server request. I prefer to perform all the clean up in the
deinit() {
// Abort all your APIs and asynchronous call
// Release all dependency
}
Along with this, always have a weak reference of your controllers and then perform optional binding in the response of the Asynchronous call.
Almofire.request(reqData: param, method: get.....) {
[weak self] response in
guard let safeSelfRef = self, let safeCollectionView =
safeSelfRef.collectionView else { return }
//Update view here
}
I'm following a tutorial about working with REST/web requests. In the tutorial, we're working towards a Pokedex app where we fetch Pokemon details from an API using Alamofire, and then displaying that data in our UIs.
Here's the related code:
typealias DownloadComplete = (Bool) -> ()
// Model class
func downloadPokemonDetails(completed: #escaping DownloadComplete)
{
Alamofire.request(_pokemonURL).responseJSON { (response) in
var success = true
if let jsonData = response.result.value as? Dictionary<String, Any>
{
// parse the json here
...
}
else
{
success = false
}
completed(success)
}
}
// Controller class
override func viewDidLoad() {
super.viewDidLoad()
pokemon.downloadPokemonDetails(completed: { (success) in
if success
{
self.updateUI()
}
else
{
print("FAILED: TO PARSE JSON DATA")
}
})
}
func updateUI()
{
attackLbl.text = pokemon.attack
defenseLbl.text = pokemon.defense
heightLbl.text = pokemon.height
weightLbl.text = pokemon.weight
}
Now my question is: shouldn't we use DispatchQueue.main. and update the UI there like so?
pokemon.downloadPokemonDetails(completed: { (success) in
if success
{
DispatchQueue.main.async {
self.updateUI()
}
}
The tutorial left it out and I'm not sure if DispatchQueue is needed here to update the UI. I know that updating UI in a background thread is bad practice so if anyone can shed some light on whether using DispatchQueue here to get the main thread is necessary or not, I would very much appreciate it.
If one does not want to read the whole comments section, I am posting here it as answer.
Firstly, read Alamofire docs, which clearly states: "Response handlers by default are executed on the main dispatch queue."
It means, you can call any UI related code in response block. If you still feel uncomfortable to rely on 3rd party lib doc, you can check by executing this swift3 snippet:
if Thread.isMainThread {
print("Main Thread")
}
xcode 9
Starting from xcode 9 there is a built-in Main Thread Checker which detects invalid use of AppKit, UIKit, and other APIs from a background thread. Main Thread Checker is automatically enabled when you run your app with the Xcode debugger.
If any part of your project contains invalid UI calls from background thread, you will see the following:
** Demonstrated in Xcode Version 9.1 (9B55)