Update UI after async await call - swift

I load books from API, show activity indicator while loading, update label after server response.
activityView.isHidden = false
let task = detach {
do {
let books = try await self.bookService.fetchBooks()
DispatchQueue.main.async {
self.show(books: books)
}
} catch {
DispatchQueue.main.async {
self.resultLabel.text = error.localizedDescription
}
}
DispatchQueue.main.async {
self.activityView.isHidden = true
}
}
//...
My question is what is better approach to update UI on the main queue? DispatchQueue.main.async look ugly and I guess there is a better approach to do the same.
I must use it, because all UI updates should be on the main thread and I get compiler errors without DispatchQueue.main.async something like
Property 'text' isolated to global actor 'MainActor' can not be mutated from a non-isolated context
or
Property 'isHidden' isolated to global actor 'MainActor' can not be mutated from a non-isolated context
P.S. Use Xcode 13.0b2

Use #MainActor like this -
self.updateAcitivityIndicator(isHidden: false)
let task = detach {
do {
let books = try await self.bookService.fetchBooks()
self.showBooks(books)
} catch {
self.showError(error)
}
self.updateAcitivityIndicator(isHidden: true)
}
#MainActor
private func showBooks(_ books: [Book]) {
}
#MainActor
private func showError(_ error: Error) {
self.resultLabel.text = error.localizedDescription
}
#MainActor
private func updateAcitivityIndicator(isHidden: Bool) {
self.activityView.isHidden = isHidden
}

Related

SwiftUI Running Async Code Within Synchronous Handler

I am creating a game where, after a user signs in, I want to send their playerID to my backend. Since this is in SwiftUI, I have the following (btw I know we're not supposed to be using playerID anymore but this is just a minimal reproducible example):
import SwiftUI
import GameKit
struct SampleView: View {
let localPlayer = GKLocalPlayer.local
func authenticateUser() async {
localPlayer.authenticateHandler = { vc, error in
guard error == nil else {
print(error?.localizedDescription ?? "")
return
}
if localPlayer.isAuthenticated {
let playerID = localPlayer.playerID
GKAccessPoint.shared.isActive = localPlayer.isAuthenticated
// here is where I would like to make an async call
}
}
}
var body: some View {
VStack {
Text("Sample View")
}
.task {
await authenticateUser()
}
}
}
struct SampleView_Previews: PreviewProvider {
static var previews: some View {
SampleView()
}
}
In the comment indicating where I'd like to place an async call, I have tried something like
await myBackendCall(playerID)
but this throws the error
Invalid conversion from 'async' function of type '(UIViewController?, (any Error)?) async -> Void' to synchronous function type '(UIViewController?, (any Error)?) -> Void'
which makes sense given that the authenticateHandler function isn't an async function.
What is the best approach here? I'd like to wait until I have the value for PlayerID, and then call await myBackendCall(playerID). Any advice here would be much appreciated, thank you!
To make a completion handler async use a continuation, it returns true if the user is authenticated, otherwise false.
func authenticateUser() async -> Bool {
return await withCheckedContinuation { continuation in
localPlayer.authenticateHandler = { vc, error in
if let error {
print(error.localizedDescription)
continuation.resume(returning: false)
} else {
continuation.resume(returning: localPlayer.isAuthenticated)
}
}
}
}
and in the task scope write
.task {
let isAuthenticated = await authenticateUser()
if isAuthenticated {
let playerID = localPlayer.playerID
GKAccessPoint.shared.isActive = localPlayer.isAuthenticated
// here is where I would like to make an async call
}
}
When you have a callback closure (like authenticateHandler), it invariably means that the closure may possibly be called multiple times. The appropriate async-await pattern would be an AsyncSequence (e.g., an AsyncStream or an AsyncThrowingStream).
So, you might wrap authenticateHandler in an asynchronous sequence, like so:
func viewControllers() -> AsyncThrowingStream<UIViewController, Error> {
AsyncThrowingStream<UIViewController, Error> { continuation in
GKLocalPlayer.local.authenticateHandler = { viewController, error in
if let viewController {
continuation.yield(viewController)
} else {
continuation.finish(throwing: error ?? GKError(.unknown))
}
}
}
}
Then you could do things like:
.task {
do {
for try await _ in viewControllers() {
GKAccessPoint.shared.isActive = GKLocalPlayer.local.isAuthenticated
// do your subsequent `async` call here
}
} catch {
GKAccessPoint.shared.isActive = false
print(error.localizedDescription)
}
}
For more information, see WWDC 2021 video Meet AsyncSequence. But the idea is that withCheckedContinuation (or withThrowingCheckedContinuation) is designed for completion handler patterns, where it must be called once, and only once. If you use a checked continuation and the closure is called again, it will be “logging correctness violations”, because “You must call a resume method exactly once on every execution path throughout the program.”
Instead, in cases where it may be called multiple times, consider handling it as an asynchronous sequence.

Issues with retain cycle using AsyncStream in a Task

Found this issue while working with the new Swift concurrency tools.
Here's the setup:
class FailedDeinit {
init() {
print(#function, id)
task = Task {
await subscribe()
}
}
deinit {
print(#function, id)
}
func subscribe() async {
let stream = AsyncStream<Double> { _ in }
for await p in stream {
print("\(p)")
}
}
private var task: Task<(), Swift.Error>?
let id = UUID()
}
var instance: FailedDeinit? = FailedDeinit()
instance = nil
Running this code in a Playground yields this:
init() F007863C-9187-4591-A4F4-BC6BC990A935
!!! The deinit method is never called!!!
Strangely, when I change the code to this:
class SuccessDeinit {
init() {
print(#function, id)
task = Task {
let stream = AsyncStream<Double> { _ in }
for await p in stream {
print("\(p)")
}
}
}
deinit {
print(#function, id)
}
private var task: Task<(), Swift.Error>?
let id = UUID()
}
var instance: SuccessDeinit? = SuccessDeinit()
instance = nil
By moving the code from the method subscribe() directly in the Task, the result in the console changes to this:
init() 0C455201-89AE-4D7A-90F8-D6B2D93493B1
deinit 0C455201-89AE-4D7A-90F8-D6B2D93493B1
This may be a bug or not but there is definitely something that I do not understand. I would welcome any insight about that.
~!~!~!~!
This is crazy (or maybe I am?) but with a SwiftUI macOS project. I still DON'T get the same behaviour as you. Look at that code where I kept the same definition of the FailedDeinit and SuccessDeinit classes but used them within a SwiftUI view.
struct ContentView: View {
#State private var failed: FailedDeinit?
#State private var success: SuccessDeinit?
var body: some View {
VStack {
HStack {
Button("Add failed") { failed = .init() }
Button("Remove failed") { failed = nil }
}
HStack {
Button("Add Success") { success = .init() }
Button("Remove Success") { success = nil }
}
}
}
}
class FailedDeinit {
init() {
print(#function, id)
task = Task { [weak self] in
await self?.subscribe()
}
}
deinit {
print(#function, id)
}
func subscribe() async {
let stream = AsyncStream<Double> { _ in }
for await p in stream {
print("\(p)")
}
}
private var task: Task<(), Swift.Error>?
let id = UUID()
}
Consider the following:
task = Task {
await subscribe()
}
It is true that introduces a strong reference to self. You can resolve that strong reference with:
task = Task { [weak self] in
await self?.subscribe()
}
But that is only part of the problem here. This [weak self] pattern only helps us in this case if either the Task has not yet started or if it has finished.
The issue is that as soon as subscribe starts executing, despite the weak reference in the closure, it will keep a strong reference to self until subscribe finishes. So, this weak reference is prudent, but it is not the whole story.
The issue here is more subtle than appears at first glance. Consider the following:
func subscribe() async {
let stream = AsyncStream<Double> { _ in }
for await p in stream {
print("\(p)")
}
}
The subscribe method will keep executing until the stream calls finish. But you never finish the stream. (You don’t yield any values, either. Lol.) Anyway, without anything in the AsyncStream, once subscribe starts it will never complete and thus will never release self.
So let us consider your second rendition, when you create the Task, bypassing subscribe:
task = Task {
let stream = AsyncStream<Double> { _ in }
for await p in stream {
print("\(p)")
}
}
Yes, you will see the object be deallocated, but you are neglecting to notice that this Task will never finish, either! So, do not be lulled into into a false sense of security just because the containing object was released: The Task never finishes! The memory associated with that Task will never get released (even if the parent object, FailedDeinit in your example, is).
This all can be illustrated by changing your stream to actually yield values and eventually finish:
task = Task {
let stream = AsyncStream<Double> { continuation in
Task {
for i in 0 ..< 10 {
try await Task.sleep(nanoseconds: 1 * NSEC_PER_SECOND)
continuation.yield(Double(i))
}
continuation.finish()
}
}
for await p in stream {
print("\(p)")
}
print("all done")
}
In this case, if you dismiss it while the stream is underway, you will see that the AsyncStream continues until it finishes. (And, if you happen to be doing this inside a method, the object in question will also be retained until the task is canceled.)
So, what you need to do is to cancel the Task if you want the AsyncStream to finish. And you also should implement onTermination of the continuation in such a manner that it stops the asynchronous stream.
But, the result is that if I cancel this when the view controller (or whatever) is released, then my example yielding values 0 through 9 will stop and the task will be freed.
It all comes down to what your AsyncStream is really doing. But in the process of simplifying the MCVE and removing the contents of the AsyncStream, you simultaneously do not handle cancelation and never call finish. Those two, combined, manifest the problem you describe.
This doesn't really have anything to do with async/await or AsyncStream. It's a perfectly normal retain cycle. You (the FailedDeinit instance) are retaining the task, but the task refers to subscribe which is a method of you, i.e. self, so the task is retaining you. So simply break the retain cycle just like you would break any other retain cycle. Just change
task = Task {
await subscribe()
}
To
task = Task { [weak self] in
await self?.subscribe()
}
Also, be sure to test in a real project, not a playground, as playgrounds are not indicative of anything in this regard. Here's the code I used:
import UIKit
class FailedDeinit {
init() {
print(#function, id)
task = Task { [weak self] in
await self?.subscribe()
}
}
deinit {
print(#function, id)
}
func subscribe() async {
let stream = AsyncStream<Double> { _ in }
for await p in stream {
print("\(p)")
}
}
private var task: Task<(), Swift.Error>?
let id = UUID()
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var instance: FailedDeinit? = FailedDeinit()
instance = nil
}
}

MainActor and async await when reading and writing

I understand the new async syntax in Swift in the sense that if I call it, then it will handle a pool of asynchronous queues / threads (whatever) to do the work. What I don't understand is how we return to the main thread once it's all over.
// On main thread now
let manager = StorageManager()
let items = await manager.fetch // returns on main thread?
struct StorageManager {
private func read() throws -> [Item] {
let data = try file.read()
if data.isEmpty { return [] }
return try JSONDecoder().decode([Item].self, from: data)
}
func fetch() async {
fetchAndWait()
}
func fetchAndWait() {
if isPreview { return }
let items = try? read()
fetchedItems = items ?? []
}
func save() throws {
let data = try JSONEncoder().encode(fetchedItems)
try file.write(data)
}
}
I want to make sure that I read and write from/to disk in the correct way i.e. is thread safe when necessary and concurrent where possible. Is it best to declare this struct as a #MainActor ?
There is nothing in the code you've given that uses async or await meaningfully, and there is nothing in the code you've given that goes onto a "background thread", so the question as posed is more or less meaningless. If the question did have meaning, the answer would be: to guarantee that code doesn't run on the main thread, put that code into an actor. To guarantee that code does run on the main thread, put that code into a #MainActor object (or call MainActor.run).
The async methods do not return automatically to the main thread, they either:
complete in the background whatever they are doing
or
explicitly pass at a certain moment the execution to the main thread through a #MainActor function/ class. (edited following #matt's comment)
In the code above you can start by correcting the fact that fetch() does not return any value (items will receive nothing based on your code).
Example of your code for case 1 above:
let manager = StorageManager()
let items = await manager.fetch // not on the main thread, the value will be stored in the background
struct StorageManager {
private func read() throws -> [Item] {
let data = try file.read()
if data.isEmpty { return [] }
return try JSONDecoder().decode([Item].self, from: data)
}
func fetch() async -> [Item] {
if isPreview { return }
let items = try? read()
return items ?? []
}
func save() throws {
let data = try JSONEncoder().encode(fetchedItems)
try file.write(data)
}
}
Example for case 2 above (I created an #Published var, which should only be written on the main thread, to give you the example):
class ViewModel: ObservableObject {
let manager = StorageManager()
#Published var items = [Item]() // should change value only on main thread
func updateItems() {
Task { // Enter background thread
let fetchedItems = await self.manager.fetch()
// Back to main thread
updateItemsWith(fetchedItems)
}
}
#MainActor private func updateItemsWith(newItems: [Item]) {
self.items = newItems
}
}
struct StorageManager {
private func read() throws -> [Item] {
let data = try file.read()
if data.isEmpty { return [] }
return try JSONDecoder().decode([Item].self, from: data)
}
func fetch() async -> [Item] {
if isPreview { return }
let items = try? read()
return items ?? []
}
func save() throws {
let data = try JSONEncoder().encode(fetchedItems)
try file.write(data)
}
}

Firebase Async Callback in NSOperation not returning

I have a AsyncOperation class defined as such
import Foundation
class ASyncOperation: NSOperation {
enum State: String {
case Ready, Executing, Finished
private var keyPath: String {
return "is" + rawValue
}
}
var state = State.Ready {
willSet {
willChangeValueForKey(newValue.keyPath)
willChangeValueForKey(state.keyPath)
}
didSet {
didChangeValueForKey(oldValue.keyPath)
didChangeValueForKey(state.keyPath)
}
}
override var ready: Bool {
return super.ready && state == .Ready
}
override var executing: Bool {
return super.ready && state == .Executing
}
override var finished: Bool {
return super.ready && state == .Finished
}
override var asynchronous: Bool {
return true
}
override func start() {
if cancelled {
state = .Finished
return
}
main()
state = .Executing
}
override func cancel() {
state = .Finished
}
}
and a subclass of it ImageLoadOperation.
import Foundation
import UIKit
import Firebase
class ImageLoadOperation: ASyncOperation {
var imagePath: String?
var image: UIImage?
override func main(){
let storage = FIRStorage.storage()
let storageRef = storage.referenceForURL("gs://salisbury-zoo- 91751.appspot.com")
if let path = imagePath {
let imageReference = storageRef.child(path)
imageReference.dataWithMaxSize(3 * 1024 * 1024) { (data, error) -> Void in
if (error != nil) {
self.image = nil
} else {
self.image = UIImage(data: data!)
self.state = .Finished
}
}
}
}
}
So I go to call the Operation in a Queue
let queue = NSOperationQueue()
let imageLoad = ImageLoadOperation()
queue.addOperation(imageLoad)
let img:UIImage? = imageLoad.image
But it always returns nil. When I put a print statement in the callback of ImageLoadOperation the image is there and state is set to finished. When I add
queue.waitUntilAllOperationsAreFinished()
Inbetween queue.addOperation and let img:UIImage? = imageLoad.load then the entire application stalls as the main thread is blocked. Any other ideas on how I could get the image to be there outside the scope of the callback? I have also tried doing it without a NSOperationQueue and just as an NSOperation with no luck.
The queue.addOperation function adds the operation, and it starts executing in a background thread. It therefore returns well before the background thread is finished, which is why the image is nil.
And as the documentation states, waitUntilAllOperationsAreFinished will block the thread until the operations are finished. This is very undesirable on the main thread.
imageReference.dataWithMaxSize is an asynchronous operation that has a completion handler (where you are currently setting self.image). You need something in there to trigger code to run that will allow you to use imageLoad.image. How you do this will depend on the architecture of your app.
If your image is to be displayed in a UITableViewCell, for example, you will need to store the image in an array of images, possibly where the index matches the table row, and then reload at least that row of the tableView. This is because by the time the image has been received, the cell may no longer exist for that row. Obviously you would not want this code sitting inside your ImageLoadOperation class. Instead it should be passed into main() as a completion handler.

Are there anything similar to Java's "Future" in Swift?

Java has Future or FutureTask that can run a task in a new thread. Then, return the execution result to the original thread. Are there any feature in Swift can achieve that?
You're looking into some kind of language construction called Futures and promises. You can find some examples, like:
https://bitbucket.org/al45tair/async (C#-like async/await primitives in Swift)
https://github.com/mxcl/PromiseKit (Promise kit http://promisekit.org/)
mentioned earlier https://github.com/Thomvis/BrightFutures
However the language itself misses such feature.
Not provided by the language (meaning the standard library), but you can surely roll your own or simply use a library such as https://github.com/Thomvis/BrightFutures
If Apple did implement Futures or Promises in Swift, would they say so? After all, they always avoid talking about Future products. ;)
Anyway, the original question seems to be generally about ways to do asynchronous work, not necessarily about specifically doing that with a Futures/Promises style model. So, while the third party libraries mentioned in other answers are great for that model, you can also do asynchronous work without that model using the same iOS & OS X built-in APIs that you can from ObjC: dispatch or NSOperation. For example:
NSOperationQueue().addOperationWithBlock {
// do background work
NSOperationQueue.mainQueue().addOperationWithBlock {
// back to main thread for follow up work
}
}
There is also now FutureKit
Similar to BrightFuture, but does composition more like BFTask
And I should mention Bolts BFTask, which while written in Objective-C is also a good candidate. (And is now used inside of Facebook iOS SDK)
I end up with the following solution (iOS SDK only, Swift 3) based on Operation and OperationQueue classes:
In short: Wrapping code into synchronous or asynchronous operation. Chaining operations using utility class. Adding operations into serial queue.
In case of error there is no need to cancel current operation, just skip actual code. Additionally asynchronous execution blocks must call finalize callback to inform operation queue about completion. Optionally DispatchQueue can be provided as parameter. Block of code will be asynchronously executed on that queue.
fileprivate func publishProductOnWebsite(listing: Listing) {
var resultSKU: String?
var resultError: Swift.Error?
let chain = OperationsChain{ isExecuting, finalize in
let task = ServerAPI.create(publishInfo: listing.publishInfo) { sku, error in
guard isExecuting() else {
return // We are canceled. Nothing to do.
}
if let error = error {
resultError = error
} else if let sku = sku {
resultSKU = sku // Arbitrary thread. But OK as this example for serial operation queue.
}
finalize() // This will finish asynchronous operation
}
task.resume()
}
chain.thenAsync(blockExecutionQueue: DispatchQueue.main) { _, finalize in
if let sku = resultSKU {
listing.sku = sku
DBStack.mainContext.saveIfHasChanges(savingParent: true) { error in
resultError = error
finalize()
}
} else {
finalize()
}
}
chain.thenSync(blockExecutionQueue: DispatchQueue.main) { [weak self] in
if let error = resultError {
self?.handleError(error) // Executed on Main thread.
} else {
self?.trackPublish()
self?.eventHandler?(.publishCompleted)
}
}
operationQueue.cancelAllOperations()
operationQueue.addOperations(chain.operations, waitUntilFinished: false)
}
OperationsChain class: Wraps block of code into Operation and saves operation into operations array maintaining dependencies.
public class OperationsChain {
public private(set) var operations = [Operation]()
public init(blockExecutionQueue: DispatchQueue? = nil,
executionBlock: #escaping AsynchronousBlockOperation.WorkItemBlock) {
let op = AsynchronousBlockOperation(blockExecutionQueue: blockExecutionQueue, executionBlock: executionBlock)
operations.append(op)
}
public init(blockExecutionQueue: DispatchQueue? = nil,
executionBlock: #escaping SynchronousBlockOperation.WorkItemBlock) {
let op = SynchronousBlockOperation(blockExecutionQueue: blockExecutionQueue, executionBlock: executionBlock)
operations.append(op)
}
#discardableResult
public func thenAsync(blockExecutionQueue: DispatchQueue? = nil,
executionBlock: #escaping AsynchronousBlockOperation.WorkItemBlock) -> AsynchronousBlockOperation {
let op = AsynchronousBlockOperation(blockExecutionQueue: blockExecutionQueue, executionBlock: executionBlock)
if let lastOperation = operations.last {
op.addDependency(lastOperation)
} else {
assertionFailure()
}
operations.append(op)
return op
}
#discardableResult
public func thenSync(blockExecutionQueue: DispatchQueue? = nil,
executionBlock: #escaping SynchronousBlockOperation.WorkItemBlock) -> SynchronousBlockOperation {
let op = SynchronousBlockOperation(blockExecutionQueue: blockExecutionQueue, executionBlock: executionBlock)
if let lastOperation = operations.last {
op.addDependency(lastOperation)
} else {
assertionFailure()
}
operations.append(op)
return op
}
}
SynchronousBlockOperation and AsynchronousBlockOperation classes.
public final class SynchronousBlockOperation: Operation {
public typealias WorkItemBlock = (Void) -> Void
fileprivate var executionBlock: WorkItemBlock?
fileprivate var blockExecutionQueue: DispatchQueue?
public init(blockExecutionQueue: DispatchQueue? = nil, executionBlock: #escaping SynchronousBlockOperation.WorkItemBlock) {
self.blockExecutionQueue = blockExecutionQueue
self.executionBlock = executionBlock
super.init()
}
public override func main() {
if let queue = blockExecutionQueue {
queue.async { [weak self] in
self?.executionBlock?()
}
} else {
executionBlock?()
}
}
}
open class AsynchronousBlockOperation: AsynchronousOperation {
public typealias FinaliseBlock = (Void) -> Void
public typealias StatusBlock = (Void) -> Bool
public typealias WorkItemBlock = (#escaping StatusBlock, #escaping FinaliseBlock) -> Void
fileprivate var executionBlock: WorkItemBlock?
fileprivate var blockExecutionQueue: DispatchQueue?
public init(blockExecutionQueue: DispatchQueue? = nil, executionBlock: #escaping AsynchronousBlockOperation.WorkItemBlock) {
self.blockExecutionQueue = blockExecutionQueue
self.executionBlock = executionBlock
super.init()
}
open override func onStart() {
if let queue = blockExecutionQueue {
queue.async { [weak self] in
self?.executionBlock?({ return self?.isExecuting ?? false }) {
self?.finish()
}
}
} else {
executionBlock?({ [weak self] in return self?.isExecuting ?? false }) { [weak self] in
self?.finish()
}
}
}
}
AsynchronousOperation class: Reusable subclass of Operation.
open class AsynchronousOperation: Operation {
fileprivate var lockOfProperties = NonRecursiveLock.makeDefaultLock()
fileprivate var lockOfHandlers = NonRecursiveLock.makeDefaultLock()
fileprivate var mFinished = false
fileprivate var mExecuting = false
}
extension AsynchronousOperation {
public final override var isAsynchronous: Bool {
return true
}
public final override var isExecuting: Bool {
return lockOfProperties.synchronized { mExecuting }
}
public final override var isFinished: Bool {
return lockOfProperties.synchronized { mFinished }
}
}
extension AsynchronousOperation {
public final override func start() {
if isCancelled || isFinished || isExecuting {
return
}
willChangeValue(forKey: "isExecuting")
lockOfProperties.synchronized { mExecuting = true }
onStart()
didChangeValue(forKey: "isExecuting")
}
public final override func cancel() {
super.cancel()
if isExecuting {
onCancel()
finish()
} else {
onCancel()
lockOfProperties.synchronized {
mExecuting = false
mFinished = true
}
}
}
public final func finish() {
willChangeValue(forKey: "isExecuting")
willChangeValue(forKey: "isFinished")
lockOfProperties.synchronized {
mExecuting = false
mFinished = true
}
onFinish()
didChangeValue(forKey: "isExecuting")
didChangeValue(forKey: "isFinished")
}
}
extension AsynchronousOperation {
/// Subclasses must launch job here.
///
/// **Note** called between willChangeValueForKey and didChangeValueForKey calls, but after property mExecuting is set.
open func onStart() {
}
/// Subclasses must cancel job here.
///
/// **Note** called immediately after calling super.cancel().
open func onCancel() {
}
/// Subclasses must release job here.
///
/// **Note** called between willChangeValueForKey and didChangeValueForKey calls,
/// but after properties mExecuting and mFinished are set.
open func onFinish() {
}
}
[Java Future and Promise]
Swift's Combine framework uses these constructions