I'm trying to get the View Controller to update while I'm in a loop that calls a function that does tons of Label updates. But the update to the screen doesn't happen until I've finished the loop.
Is there a way to force a UI update before continuing? Something like forcing data to be written to a disk.
code for the button to start the loop is something like this:
while (match.currentInnings == currentInnings)
{
playSingleBall()
if (gameover == true)
{
return
}
}
After playSingleBall, I'd like to force a UI update before continuing.
Thanks.
Call it in Dispatch Queue:
Swift 3.x
DispatchQueue.main.async {
updateYourUI()
}
Swift 2.x
dispatch_async(dispatch_get_main_queue()) {
updateYourUI()
}
Related
In this case the async function reads a file and returns the parsed contents.
In my view I want to load the contents off of the main thread, and then update the view once complete.
I've used this pattern in various places and noticed that in some cases the async call is on the main thread (by debugging) while in others it is on the Thread 4 Queue : com.apple.root.user-initiated-qos.cooperative (concurrent) thread
For example:
struct MyView: View {
#State var data = "some data"
var body: some View {
Button("refresh") {
// when the button is pressed refresh it
Task {
await refresh()
}
}.task {
// when the view appears
await refresh()
}
Text("the data is \(data)") // write the data which was refreshed async
}
}
func refresh() async {
do {
let res = try await anotherAyncFunction()
data = res // this is presumably wrong and off the main thread - obviously might not be correct but leave here for debug as well
} catch {
print("got error \(error)")
}
}
I created several different views using a similar pattern (.task block calling async functions)
In some cases the functions are long running (reading from disk) and that is happening on the main thread
Change Task { to Task.detached {.
From the Swift Language Guide:
To create an unstructured task that runs on the current actor, call the Task.init(priority:operation:) initializer. To create an unstructured task that’s not part of the current actor, known more specifically as a detached task, call the Task.detached(priority:operation:) class method.
When you call Task.init, the asynchronous code runs on the current actor, which, in this context is the main actor. This has the result of blocking the main thread.
By calling Task.detached, you allow the asynchronous work to happen off the main thread.
Using Swift structured concurrency, to run code on a background thread, use a detached task or (better) an actor.
I am utilizing cellChanged.node.setDataValue(fieldChanged, oldValue) inside of the (cellValueChanged) event emitter, and I'm having trouble figuring out how to call a function once the setDataValue function has finished executing. I need to do this to do a check to see if a user has the permission to update a cell.
Here is the full code that checks:
if(this.showPaywallNotification) {
// Okay, so the budget is above what we allow HOWEVER...
if(budget > BUDGET_AMOUNT) {
this.showPaywallNotification = false;
cellChanged.node.setDataValue(fieldChanged, oldValue)
// Note: This timeout is in place to prevent the infinite updating bug
// This is problematic because if the user changes the cells fast enough, they can get around the paywall. If I change the timeout to be smaller, the resulting change triggers the update, which ends up creating an infinite loop of updates.
setTimeout(() => {
this.showPaywallNotification = true;
}, 230)
}
}
Is there a way I can replace my setTimeout() function with something better that can always ensure the user can't get around my paywall by just updating the cell faster than the timeout can execute?
You don't have to do polling. setDataValue is a not an async function.
Also, onCellValueChanged won't get called again if you call node.setDataValue.
Have a look at this plunk: Cell Editing - Revert to old value. Try updating any Age value to negative.
onCellValueChanged($event) {
if ($event.colDef.field === 'age' && $event.newValue < 0) {
// debugger
$event.node.setDataValue('age', $event.oldValue);
console.log('value reverted');
}
}
Let me know if something is not clear, or this is not sufficient.
Basically, I want to upload some images before I run other functions that rely on the image being uploaded. I think I may have a misunderstanding of what GCD is/how the threads work. I want function 1 and 2 to happen AFTER I upload the images. They are both quick to execute but rely heavily on the upload images to be complete. Maybe I shouldn't use GCD (as I want to implement a waiting indicator)? I just can't seem to get this to execute properly
if goToHome {
DispatchQueue.global().async {
DispatchQueue.main.sync {
self.uploadImages() // Uploads the images, takes a good amount of time to execute
function1()
function2()
}
}
Functions 1 and 2 keep running before the upload images get completed as they take much less time to execute.
The basic pattern in Swift is to do work such as uploading on a background thread, then call a completion function on the main thread and continue work based on whether or not your upload finished successfully.
Generally you'll call back onto the main thread in case you need to do something with the user interface such as setting a progress indicator (which has to happen on the main thread).
So something like this:
func uploadInBackground(_ images: [Image], completion: #escaping (_ success: Bool) -> Void) {
DispatchQueue.global(qos: .background).async {
var success = true
// upload the images but stop on any error
for image in images {
success = uploadImage(image) // upload your images somehow
guard success else { break }
}
DispatchQueue.main.async {
completion(success)
}
}
}
func mainThreadUploader() {
let images = [Image(), Image()] // get your images from somewhere
// we are on the main thread where UI operations are ok, so:
startProgressIndicator()
uploadInBackground(images) { success in
// this callback is called on the main thread, so:
stopProgressIndicator()
guard success else { return }
// images uploaded ok, so proceed with functions that depend on
// the upload(s) completing successfully:
function1()
function2()
}
}
Despite running the upload image function in the main queue, the upload image function itself is running operations in a background queue. To fix this, possible strategies would be:
use a completion handler with the image upload, likely the easiest to implement depending on the implementation of the self.uploadImages() function
make the image uploading happen on the main thread, which is likely hard to implement and inadvisable
use a dispatch group, which I personally have less experience with but is an option
Function which initialize GMSMarkers:
func makeMarkers (progressHandler: ((String) -> Void)? = nil) {
progressHandler?("Инициализируем маркеры...")
for device in self.devices {
progressHandler?("Инициализируем маркеры: для устройства \(device.name!)...")
if let position = self.positions.findById(device.positionId) {
if let marker = DeviceMarker(device, at: position) {
self.deviceMarkers.append(marker)
}
}
}
for geofence in self.geofences {
progressHandler?("Инициализируем маркеры: для геометки \(geofence.name!)...")
if let marker = GeofenceMarker(geofence) {
self.geofenceMarkers.append(marker)
}
}
self.locationManager.requestLocation()
progressHandler?("Инициализируем маркеры: для пользователя \(self.loggedUser.name!)...")
if let userLocation = self.locationManager.location {
self.userMarker = UserMarker(self.loggedUser, at: userLocation)
} else {
self.userMarker = UserMarker(self.loggedUser, at: CLLocation(latitude: 48, longitude: 44))
}
progressHandler?("Маркеры инициализированы...")
}
As you can see I want inform user about progress of this operation using progressHandler.
But during execution of that function I can not see any of that messages.
Snipped where I call function makeMarkers:
...
DB.geofences.server.getAll() { geofences in
self.statusLabel.text = "Получили геометки \(geofences.count)..."
DB.devices.server.getAll() { devices in
self.statusLabel.text = "Получили устройства \(devices.count)..."
DB.positions.server.getAll() { positions in
self.statusLabel.text = "Получили позиции \(positions.count)..."
DB.devices.client.insert(devices)
self.statusLabel.text = "Записали устройства..."
DB.positions.client.insert(positions)
self.statusLabel.text = "Записали позиции..."
DB.geofences.client.insert(geofences)
self.statusLabel.text = "Записали геометки..."
MapManager.shared.makeMarkers() { status in
self.statusLabel.text = status
}
...
And if I change view messages using print() all messages will be shown.
So, question is: why UI changes are blocked during execution of my function and how get rid of that?
UPDATE: Sure, I do everything in main thread.
UPDATE 2: Code snippet where I'm calling my function updated. Last message which I can see before map controller shows is "Получили устройства...", it is strange, because I'm sure I get positions as well
Well, that occurs because UI updates happen in the main thread. But when you change your label's text this change does not happen immediately. Instead, all UI changes get scheduled and only get rendered when the app finishes the current pass through the event loop.
Since you are performing time-consuming tasks in the same main loop, it becomes blocked and changes won't happen until those tasks are finished and your function returns.
You may check these articles to get more detailed explanation on how event loops work: Run Loops, Main event loop
I suggest you move all time-consuming tasks (such as database queries) to a background thread and call the main thread from there when you actually want to update your UILabel.
I've recently made a weather that gets data from open weather map, but it takes forever for the parsed data to show up on the screen...
With code like this:
let tempF = jsonResult["tempFahrenheit"]
I know the data is fast because I watch the url result show up immediately in the console, so would swiftyjson speed up displaying data on the screen? Is there a swiftyjson for Swift 3??
The code you have posted doesn't appear to update any UI element on the screen.
If there is another place where you are updating the UI with something like
mylabel.text = someVariable
and the UI is still laggy, it maybe because you have not wrapped the UI
change in the main thread.
The correct way to do the update above would be
DispatchQueue.main.async { mylabel.text = someVariable }
Call API> Get Response> Parse Data > Update UI [It should be in main queue]
Write your UI update related code in main queue like below ...
DispatchQueue.main.async {
textFiled.text = yourText
imageview.image = yourImage
.......
}