`NSDocument`'s `data(ofType:)` getting data from (async) `actor` - swift

I've a document based macOS, that's using a NSDocument based subclass.
For writing the document's file I need to implement data(ofType:) -> Data which should return the document's data to be stored on disk. This is (of course) a synchronous function.
My data model is an actor with a function that returns a Data representation.
The problem is now that I need to await this function, but data(ofType:) wants the data synchronously.
How can I force-wait (block the main thread) until the actor has done its work and get the data?
EDIT:
In light of Sweepers remark that this might be an XY-problem I tried making the model a #MainActor, so the document can access the properties directly. This however doesn't allow me to create the model in the first place:
#MainActor class Model {}
class Document: NSDocument {
let model = Model() <- 'Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context'
}
I then tried to make the whole Document a #MainActor, but that makes my whole app to collapse in compiler errors. Even the simplest of calls need to be performed async. This doesn't allow any kind of upgrade path to the new concurrency system.
In the past my model was protected by a serial background queue and I could basically do queue.sync {} to get the needed data out safely (temporarily blocking the main queue).
I've looked into the saveToURL:ofType:forSaveOperation:completionHandler: and I think I can use this very much to my need. It allows async messaging that saving is finished, so I now override this method and in an async Task fetch the data from the model and store it in temporarily. I then call super, which finally calls data(forType:) where I return the data.

Based on the idea by #Willeke in the comments, I came up with the following solution:
private var snapshot: Model.Snapshot?
override func save(to url: URL, ofType typeName: String, for saveOperation: NSDocument.SaveOperationType, completionHandler: #escaping (Error?) -> Void) {
//Get the data and continue later
Task {
snapshot = await model.getSnapshot()
super.save(to: url, ofType: typeName, for: saveOperation, completionHandler: completionHandler)
}
}
override func data(ofType typeName: String) throws -> Data {
defer { snapshot = nil }
guard let snapshot = snapshot else {
throw SomeError()
}
let encoder = JSONEncoder()
let data = try encoder.encode(snapshot)
return data
}
As the save() function is prepared to handle the save result asynchronous we first take the snapshot of the data and then let the save function continue.

Related

Swift: withCheckedContinuation and Dispatch QoSClass

I am adapting some old code which was using common completions in order to use the new async/await syntax with Parse SDK. Here is an example, this:
static func get(
className: String,
id: String,
_ completion: #escaping (PFObject?) -> Void
) {
let query = PFQuery(className: className)
query.getObjectInBackground(withId: id) { object, _ in
completion(object)
}
}
is becoming this:
static func get(
className: String,
objectId: String
) async -> PFObject? {
let query = PFQuery(className: className)
return await withCheckedContinuation { continuation in
query.getObjectInBackground(withId: objectId) { result, _ in
continuation.resume(returning: result)
}
}
}
However, I was also using DispatchQueue/QoS previously and so the old function actually looked like this:
static func get(
className: String,
id: String,
_ completion: #escaping (PFObject?) -> Void
) {
let query = PFQuery(className: className)
DispatchQueue.global(qos: .userInteractive).async {
query.getObjectInBackground(withId: id) { object, _ in
DispatchQueue.main.async {
completion(object)
}
}
}
}
How can I use this with the async/await syntax? Is it even needed?
Thank you for your help
You probably want to execute your async function get(className:objectId:) within a Swift Task, using this Task initialiser:
func foo(
className: String,
objectId: String
) async -> PFObject? {
await Task(priority: .userInitiated) {
await get(className: className, objectId: objectId)
}
.value
}
Note that, when using this Task initialiser you are getting "structured concurrency", which means, that the embedded task in function foo inherits the actor context of the calling function.
That is, you can use the result safely from whatever thread you called it.
That means also, if the task where function foo() is running, gets cancelled, the embedded task will be cancelled as well. Of course, cancellation is cooperatively, which means, you need to stop a running task explicitly. For example, as preferred in your use case, with a withTaskCancellationHandler, which calls cancel() on your PFQuery object. Or when you have a long running iterating task, you may poll the Task's cancellation status in reasonably steps while your task progresses.
Please also read about "detached" which behave differently regarding cancellation and inheriting the task priority.
As of your question whether it is needed:
I assume, you ask if using specifying a priority is needed:
Short answer: in many use cases it may be more safe to just inherit the priority from whatever thread the function originates.
In your case, I would not explicitly change it: when it is called from a background thread with low priority, your task should inheriting this priority as well. There will be no reason to make it high priority with userInitiated. Otherwise, if your function is already called from a user action, it will be inheriting this higher priority as well. I believe, this is what most use cases would require.
Also, when your worker is running in a background thread, as is the case in your code, it really doesn't matter much "how fast you schedule" this task.
So, basically you end up with your async get function as defined, use it as is, and all is good. ;)

Combine: How to clean up resources while an AnyCancellable is being cancelled?

Overview:
I have a async task to fetch from the database
I have created a Future for the async task (fetching from the database).
Question:
How can execute custom code when the Future is cancelled?
Purpose:
I would like the database connection to be closed when the subscription is cancelled.
For example, I would like to use Combine to rewrite this helper method:
// Similar to https://developer.apple.com/documentation/coredata/nspersistentcontainer/1640564-performbackgroundtask
func withDatabaseFTSContext(block: #escaping (FMDatabase?) -> Void) {
queue.async {
guard let database = self.database else {
block(nil)
return
}
database.open()
let simpleTokenizer = FMSimpleTokenizer(locale: nil)
FMDatabase.registerTokenizer(simpleTokenizer, withKey: "simple")
database.installTokenizerModule()
block(database)
database.close()
}
}
Could I leverage Combine to rewrite this method to return FMDatabase as a parameter of a publisher?
I was attempting to use Combine but it does not work. The database will be closed before cancel()
private func withDatabaseFTSContext() -> AnyPublisher<FMDatabase?, Never> {
return Future<FMDatabase?, Never> { promise in
self.queue.async {
guard let database = self.database else {
promise(.success(nil))
return
}
database.open()
let simpleTokenizer = FMSimpleTokenizer(locale: nil)
FMDatabase.registerTokenizer(simpleTokenizer, withKey: "simple")
database.installTokenizerModule()
promise(.success(database))
database.close() // When to close this database? Currently it will be closed before `cancel()`
}
}.eraseToAnyPublisher()
}
Short answer: there isn't a callback that triggers through to the underlying Future that you can use to clean things up on a subscriber cancel. In the Combine design, these functions are very intentionally separated and don't have reference links back to their publishers.
(In addition, Future is a tricky figure in the Combine world because the closure is invoked immediately upon creation time, rather than when you have a subscription (if you want that, wrap in the Future publisher in a Deferred publisher)).
All that being said, what you likely want to do to solve your underlying problem is reframe how you're treating this to separate the concerns of managing the FMDB instance and publishing data. One pattern that's been reasonably useful in this context is to the make an object that holds the lifetime of the FMDB reference, and handle cleaning up resources on it's deinit(). You can then also have a function which vends a Publisher of whatever you need from that same object, and then the cancellation of the request is changed semantically to only cancelling getting the database, not cancelling and cleaning up the database connection.

How to set the value of lazy computed property via a closure in Swift?

So I've been stuck on this problem for a while, and can't find questions addressing my particular problem online.
I am trying to set the value in description, which is defined as a lazy computed property and utilizes a self-executing closure.
To get the book's description, I make an API call, passing in another handler to the API completion handler so that I can set the book's description inside the lazy computed property.
I know my below code is wrong, since I get the error:
Cannot convert value of type '()' to specified type 'String'
class Book : NSObject {
func getInfo(for name: String, handler: #escaping (_ string: String) -> String) {
let task = URLSession.shared.dataTask(with: "foo_book.com" + name) { (data, response, error) in
guard let data = data else {return}
descriptionStr = String(data: data, encoding: .utf8) ?? "No description found"
handler(descriptionStr)
}
}
lazy var description: String = {
getInfo(for: self.name) { str in
return str
}
}()
}
How can I set the value of description?
I've tried two methods. Using a while loop to wait for a boolean: inelegant and defeats the purpose of async. Using a temp variable inside description - doesn't work because getInfo returns before the API call can finish.
In case you wonder my use case: I want to display books as individual views in a table view, but I don't want to make api calls for each book when I open the tableview. Thus, I want to lazily make the API call. Since the descriptions should be invariant, I'm choosing to make it a lazy computed property since it will only be computed once.
Edit: For those who are wondering, my solution was as the comments mentioned below. My approach wasn't correct - instead of trying to asynchronously set a property, I made a method and fetched the description in the view controller.
Already the explanation in comments are enough for what's going wrong, I will just add on the solution to your use case.
I want to display books as individual views in a table view, but I
don't want to make api calls for each book when I open the tableview.
Thus, I want to lazily make the API call.
First of all, does making lazy here make sense. Whenever in future you will call description, you are keeping a reference for URLSession and you will do it for all the books. Looks like you will easily create a memory leak.
Second, task.resume() is required in getInfo method.
Third, your model(Book) should not make the request. Why? think, I have given one reason above. Async does mean parallel, all these network calls are in the queue, If you have many models too many networks calls in the event loop.
You can shift network call responsibility to service may be BookService and then have a method like this BookService.getInfo(_ by: name). You Book model should be a dumb class.
class Book {
let description: String
init(desc: String) {
self.description = desc
}
}
Now your controller/Interactor would take care of calling the service to get info. Do the lazy call here.
class BookTableViewController: ViewController {
init(bookService: BookService, book: [String]) {
}
# you can call when you want to show this book
func loadBook(_ name: String) -> Book {
BookService.getInfo(name).map { Book(desc: str) }
}
func tableView(UITableView, didSelectRowAt: IndexPath) {
let bookName = ....
# This is lazy loading
let book = loadBook(bookName)
showThisBook()
}
}
Here, you can do the lazy call for loadBook. Hope this helps.

How to call every struct method inside write transaction

I created struct Repository for manipulating with objects of Realm database (changing some properties, adding new objects, deleting, etc.). When I want to write to the database, I have to do it inside do-try-catch block, so I created a method with completion which I call every time I need to write something to the database
private func action(_ completion: () -> Void) {
do {
try realm.write {
completion()
}
} catch {
print(error)
}
}
then I call methods for manipulating with objects like this:
func createObject(_ object: MyObject) {
action {
realm.add(object)
}
}
func deleteObject(_ object: MyObject) {
action {
realm.delete(object)
}
}
func setTitleForObject(_ object: MyObject, title: String) {
action {
object.title = title
}
}
...
My question is, is there any way how I can call every method inside this Repository struct inside write transaction in do-try-catch block by default instead of calling it inside completion of action? (or is some better way how to write to the Realm database without do-try-catch block?)
Short answer is no, there is no way to write data to realm without write transaction and without try-catch.
realm.write() is a convenient wrapper of transaction building with beginWrite() and commitWrite() calls.
These two functions build a transaction and commitWrite() is throwable, so you need to wrap to try-catch, anyway.
See https://realm.io/docs/swift/latest#writes
Example of using beginWrite()+commitWrite() https://realm.io/docs/swift/latest#interface-driven-writes
There are a lot of failures could happen during write transactions. So, simply, it is not safe to not to handle it somehow.
Also grouping write transactions by "action" is not a good idea if you going to process big amounts of objects because write transactions are costly. You'd rather group these changes to a single transaction instead of having a lot of small transactions.

Returning data after async task

I am uploading an image with using a library. This library is working async.
My function:
func upload() -> String {
let imageData:NSData = UIImageJPEGRepresentation(pureImage!, 100)!
var picture=""
SRWebClient.POST("http://domain.com/upload.php")
.data(imageData, fieldName:"image_field", data: ["username":"test","key":"test"])
.send({(response:AnyObject!, status:Int) -> Void in
if status == 200 {
let responseJSON = response! as! Dictionary<String, AnyObject>
let s_status=responseJSON["status"] as! Int
if s_status == 1 {
picture=responseJSON["picture"] as! String
print(picture)
}
}
},failure:{(error:NSError!) -> Void in
picture=""
})
return picture
}
As you can see, I have to return picture name. But now it is always returning empty string because upload process is async. How can I return the picture name after upload process?
Obviously you cannot return the picture name as function result, not unless you want to wait till the async task is done and waiting would make it a synchronous task again.
There are three very common ways to make async tasks deliver results:
Pass the task a callback (either a callback function or a completion block if you need to capture state or references). Once the task is done, it calls the callback. In your case, the callback could get the image name as argument and the callback code then needs to decide what to do with it.
If the task is encapsulated in an object, allow the object to have a delegate. Once the task is done, a delegate method is called. Either the method gets the image name as argument or can query the image name from the object it is delegate of (usually you'd pass the object itself as an argument to the delegate, that is common practice and good coding style according to Apple).
Send a notification that an image was uploaded. The image name can be the object of the notification; or some object that encapsulates the image name and possibly other properties. Whoever is interested to know when an upload task completed can register for that notification.
Some notes regarding the options above:
I'd use notifications with care. While they are easy to use and very useful if a lot of components spread across a huge project need to be informed about events, they are hard to debug (you cannot follow the code flow easily in a debugger) and they create a very lose coupling (which may or may not be desirable), yet a strong coupling to the notification itself. Also notifications cannot return a value in case that is every required.
A delegate is always a great option, but it forces users to create a class that implements the delegate protocol. This usually only pays off if you need more than just a single callback method or when you plan to call the delegate methods very frequently. Delegates are great for unit testing.
A callback is like a tiny delegate with just a single callback method. If you commonly make "fire and forget" tasks on the go and there is only a single callback required that will be called in case of success and in case of failure; and it will only be called once and there is no need to ever recycle it, then a callback is often preferable to a delegate. It has all the advantages of a delegate but it is more lightweight.
This is the sort of problem that Promises were designed for. You could implement callbacks but it quickly becomes unmanageable if you have more than a few of them to deal with.
Do yourself a big favor and import PromiseKit into you code. Take the half-hour to learn how to use it.
You will end up with something like
func upload() -> Promise<String>
you can use blocks to get a call back
func upload(completionHandler : (pictureName : NSString?)-> Void){
let imageData:NSData = UIImageJPEGRepresentation(pureImage!, 100)!
var picture=""
SRWebClient.POST("http://domain.com/upload.php")
.data(imageData, fieldName:"image_field", data: ["username":"test","key":"test"])
.send({(response:AnyObject!, status:Int) -> Void in
if status == 200 {
let responseJSON = response! as! Dictionary<String, AnyObject>
let s_status=responseJSON["status"] as! Int
if s_status == 1 {
picture=responseJSON["picture"] as! String
print(picture)
completionHandler(pictureName: picture)
}
}
},failure:{(error:NSError!) -> Void in
picture=""
completionHandler(pictureName: nil)
})
}