I am new to Swift and SwiftUI and this has been driving me nuts.
I have a list of items coming from a database, each associated with a User ID.
I need to display each item together with some user data (which also comes from the database). I want to save on database calls for getting user data for those users who have already appeared in my list of items.
In order to do that, I create a dictionary of user data, which I populate with each new user. If a user id is already in the dictionary, I won't be querying the database and will instead be using the cached user data in the dictionary.
So that was the idea and it seemed pretty straightforward. So I wrote the following
Main loop in the View goes through the list of items:
ForEach(xlist, id: \.self) { xentry in
Text(myModel.dictUser[xentry.actor_id]?.FirstName ?? "")
}.onAppear(perform: {myModel.cacheUser(uid: xentry.actor_id)})
in myModel, I try to cache the user:
#MainActor class myModel: ObservableObject {
#Published var dictUser: [String: STuser] = [:]
func cacheUser(uid: String) {
Task.init {
do {
if nil == dictUser[uid] {
dictUser[uid] = try await GetUserInfoFromDB(uid:uid)
}
} catch {
}
}
}
}
This should work but doesn't: the nil == dictUser[uid] in some cases (normally after a couple of entries) fails to correctly evaluate. I can see in the debugger that dictUser[uid] is clearly not a nil and contains valid data, yet the execution continues onto GetUserInfoFromDB.
Not sure what I'm doing wrong here. Any help appreciated.
You are trying to load data from a relatively slow, asynchronous source (a database in this case) and you want to utilise a cache to avoid the overhead of repeated database calls for the same item. A sensible approach.
With the cached items, there are three possible states:
it's already been requested and is in the cache
it's not been requested yet
it's been requested but the request is still in progress (and so the data item isn't in in the cache)
The first case is easy: if it's in the cache return it. The second case is also seemingly straightforward - you need to request the item from the database.
The last scenario is more complicated - how do you record that a request has already been made but not yet returned, and then wait for it to return it's data and send it to all those items that have requested it.
The obvious construct to represent something that can be in multiple states is an enum. For example:
enum cacheEntry {
case complete
case inProgress
}
but it's not that simple: you also want to associate each state with the data it returns from the cache.
The complete case is easy - use an associated value of the data item, in your case STUser.
The inProgress case is more complex as you want it to record the asynchronous activity that is in progress and when that activity completes return it's data item. The way to handle this is to store the asynchronous task as the associated value. So your cache entry looks like this:
enum CacheEntry {
case complete(STUser)
case inProgress(Task<STUser, Error>)
}
var cache: [String: CacheEntry] = [:]
(Note: if you're not handling the error you can replace the Error in the generic with Never.)
The question then becomes how do you use this construct?
Create a method to query the cache that can work with the async nature of the operation:
func entry(for bid: String) async throws -> STUser {
if let cacheEntry = imageCache[uid] {
switch cacheEntry {
case let .inProgress(task):
return try await task.value. //wait for the task to complete then return it's completion value
case let .downloaded(stUser):
return stUser //the item is already in the cache so return it
}
}
//There is no entry in the cache for the bid at this point
// Therefore create a task to retrieve the data asynchronously
let task = Task {
try await GetUserInfoFromDB(uid:uid)
}
//and store the task in the cache against the `uid` ready for any subsequent requests
imageCache[url] = .inProgress(task)
//process the task for the initial request
do {
//wait for the task to complete and then access its returned value
let item = try await task.value
cache[uid] = .downloaded(item) //replacing the 'inProgress' entry in the cache with the
return item // and return the retrieved value
} catch {
//if error, delete entry from the cache and handle the error
imageCache[url] = nil
throw error
}
}
The final complication now is that you have a synchronous dictionary that is being updated by an asynchronous task, with the potential for data races. To overcome this wrap the whole cache type in an actor to ensure the access to the cache is coordinated.
actor Cache {
enum CacheEntry {...}
func entry(for bid: String) async throws -> STUser {...}
}
Related
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.
I have this block of code. It fetches data from the API and adds it to a locationDetails array, which is part of a singleton.
private func DownloadLocationDetails(placeID: String) {
let request = AF.request(GoogleAPI.shared.getLocationDetailsLink(placeID: placeID))
request.responseJSON { (data) in
guard let detail = try? JSONDecoder().decode(LocationDetailsBase.self, from: data.data!),
let result = detail.result else {
print("Something went wrong fetching nearby locations.")
return
}
DownloadManager.shared.locationDetails.append(result)
}
}
This block of code is the block in question. I'm creating a caching system of sorts that only downloads new information and retains any old information. This is being done to save calls to the API and for performance gains. The line DownloadLocationDetails(placeID: placeID) is a problem for me because if I execute this line of code it will continue to loop over and over again using unnecessary API calls while waiting for the download to complete. How do I effectively manage this?
func GetLocationDetail(placeID: String) -> LocationDetail {
for location in locationDetails {
if location.place_id == placeID { return location }
}
DownloadLocationDetails(placeID: placeID)
return GetLocationDetail(placeID: placeID)
}
I expect this GetLocationDetail(....) to be called whenever a user interacts with an interface object, so how do I also ensure that the view that calls this is properly notified that the download is complete?
I attempted using a closure but I can't get it to return the way I'm wanting it to. I have a property on the singleton that I want to set this value so that it can be called globally. I am also considering using GCD but I'm not sure of the structure for that.
Generally the pattern for something like this is to store the request object you created in DownloadLocationDetails so you can check to see if one is active before making another call. If you only want to support one at a time, then it's as simple as keeping the bare reference to the request object, but you could make a dictionary of request objects keyed off the placeID (and you probably want to think about maximum request count, and queue up additional requests).
Then the trick is to get notified when the given request object completes. There are a couple ways you could do this, such as keeping a list of callbacks to invoke when it completes, but the easiest would probably be just to refactor the code a bit so that you always update your UI when the request completes, so something like:
private func DownloadLocationDetails(placeID: String) {
let request = AF.request(GoogleAPI.shared.getLocationDetailsLink(placeID: placeID))
request.responseJSON { (data) in
guard let detail = try? JSONDecoder().decode(LocationDetailsBase.self, from: data.data!),
let result = detail.result else {
print("Something went wrong fetching nearby locations.")
return
}
DownloadManager.shared.locationDetails.append(result)
// Notify the UI to refresh for placeID
}
}
I've created a Combine publisher chain that looks something like this:
let pub = getSomeAsyncData()
.mapError { ... }
.map { ... }
...
.flatMap { data in
let wsi = WebSocketInteraction(data, ...)
return wsi.subject
}
.share().eraseToAnyPublisher()
It's a flow of different possible network requests and data transformations. The calling code wants to subscribe to pub to find out when the whole asynchronous process has succeeded or failed.
I'm confused about the design of the flatMap step with the WebSocketInteraction. That's a helper class that I wrote. I don't think its internal details are important, but its purpose is to provide its subject property (a PassthroughSubject) as the next Publisher in the chain. Internally the WebSocketInteraction uses URLSessionWebSocketTask, talks to a server, and publishes to the subject. I like flatMap, but how do you keep this piece alive for the lifetime of the Publisher chain?
If I store it in the outer object (no problem), then I need to clean it up. I could do that when the subject completes, but if the caller cancels the entire publisher chain then I won't receive a completion event. Do I need to use Publisher.handleEvents and listen for cancellation as well? This seems a bit ugly. But maybe there is no other way...
.flatMap { data in
let wsi = WebSocketInteraction(data, ...)
self.currentWsi = wsi // store in containing object to keep it alive.
wsi.subject.sink(receiveCompletion: { self.currentWsi = nil })
wsi.subject.handleEvents(receiveCancel: {
wsi.closeWebSocket()
self.currentWsi = nil
})
Anyone have any good "design patterns" here?
One design I've considered is making my own Publisher. For example, instead of having WebSocketInteraction vend a PassthroughSubject, it could conform to Publisher. I may end up going this way, but making a custom Combine Publisher is more work, and the documentation steers people toward using a subject instead. To make a custom Publisher you have to implement some of things that the PassthroughSubject does for you, like respond to demand and cancellation, and keep state to ensure you complete at most once and don't send events after that.
[Edit: to clarify that WebSocketInteraction is my own class.]
It's not exactly clear what problems you are facing with keeping an inner object alive. The object should be alive so long as something has a strong reference to it.
It's either an external object that will start some async process, or an internal closure that keeps a strong reference to self via self.subject.send(...).
class WebSocketInteraction {
private let subject = PassthroughSubject<String, Error>()
private var isCancelled: Bool = false
init() {
// start some async work
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if !isCancelled { self.subject.send("Done") } // <-- ref
}
}
// return a publisher that can cancel the operation when
var pub: AnyPublisher<String, Error> {
subject
.handleEvents(receiveCancel: {
print("cancel handler")
self.isCancelled = true // <-- ref
})
.eraseToAnyPublisher()
}
}
You should be able to use it as you wanted with flatMap, since the pub property returned publisher, and the inner closure hold a reference to self
let pub = getSomeAsyncData()
...
.flatMap { data in
let wsi = WebSocketInteraction(data, ...)
return wsi.pub
}
I know SwiftUI uses state-driven rendering. So I was assuming, when I delete Core Data Entity entries, that my List with Core Data elements gets refreshed immediately.
I use this code, which gets my Entity cleaned succesfully:
func deleteAll()
{
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = ToDoItem.fetchRequest()
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
let persistentContainer = (UIApplication.shared.delegate as! AppDelegate).persistentContainer
do {
try persistentContainer.viewContext.execute(deleteRequest)
} catch let error as NSError {
print(error)
}
}
To get the List in my View visually empty I have to leave the View afterwards (for example with " self.presentationMode.wrappedValue.dismiss()") and open it again. As if the values are still stored somewhere in the memory or something.
This is of course not user-friendly and I am sure I just oversee something that refreshes the List immediately.
Maybe someone can help.
The reason is that execute (as described in details below - pay attention on first sentence) does not affect managed objects context, so all fetched objects remains in context and UI represents what is really presented by context.
So in general, after this bulk operation you need to inform back to that code (not provided here) force sync and refetch everything.
API interface declaration
// Method to pass a request to the store without affecting the contents of the managed object context.
// Will return an NSPersistentStoreResult which may contain additional information about the result of the action
// (ie a batch update result may contain the object IDs of the objects that were modified during the update).
// A request may succeed in some stores and fail in others. In this case, the error will contain information
// about each individual store failure.
// Will always reject NSSaveChangesRequests.
#available(iOS 8.0, *)
open func execute(_ request: NSPersistentStoreRequest) throws -> NSPersistentStoreResult
For example it might be the following approach (scratchy)
// somewhere in View declaration
#State private var refreshingID = UUID()
...
// somewhere in presenting fetch results
ForEach(fetchedResults) { item in
...
}.id(refreshingID) // < unique id of fetched results
...
// somewhere in bulk delete
try context.save() // < better to save everything pending
try context.execute(deleteRequest)
context.reset() // < reset context
self.refreshingID = UUID() // < force refresh
No need to force a refresh, this is IMO not a clean solution.
As you correctly mentioned in your question, there are still elements in memory. The solution is to update your in-memory objects after the execution with mergeChanges.
This blog post explains the solution in detail under "Updating in-memory objects".
There, the author provides an extension to NSBatchDeleteRequest as follows
extension NSManagedObjectContext {
/// Executes the given `NSBatchDeleteRequest` and directly merges the changes to bring the given managed object context up to date.
///
/// - Parameter batchDeleteRequest: The `NSBatchDeleteRequest` to execute.
/// - Throws: An error if anything went wrong executing the batch deletion.
public func executeAndMergeChanges(using batchDeleteRequest: NSBatchDeleteRequest) throws {
batchDeleteRequest.resultType = .resultTypeObjectIDs
let result = try execute(batchDeleteRequest) as? NSBatchDeleteResult
let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self])
}
}
Here is an update to your code on how to call it:
func deleteAll() {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = ToDoItem.fetchRequest()
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
let persistentContainer = (UIApplication.shared.delegate as! AppDelegate).persistentContainer
do {
try persistentContainer.viewContext.executeAndMergeChanges(deleteRequest)
} catch let error as NSError {
print(error)
}
}
Some more info also here under this link: Core Data NSBatchDeleteRequest appears to leave objects in context.
I am trying to find a simple example of how inside a router a person would send a request to the vapor sample endpoint http://example.vapor.codes/json, receive a response and map it to a struct or class.
I've seen examples elsewhere for Vapor 2 but they are no longer relevant with Vapor 3 and the current Vapor 3 beta documentation isn't clear.
Something like...
router.get("sample") { req in
//1. create client
//2. send get request to sample endpoint at http://example.vapor.codes/json
//3. handle response and map to a struct or class
}
My goal is to go grab something off the endpoint, turn it into a struct or class and display it in a leaf view.
{"array":[0,1,2,3],"dict":{"lang":"Swift","name":"Vapor"},"number":123,"string":"test"}
Here is my outline for how I think it is done but I don't understand how to handle the response and process into the struct so that I can use it in my home.leaf in its html (I'm not concerned with the leaf part assume I have all the configuration for all that and imports already).
router.get("example"){ req -> Future<View> in
struct ExampleData: Codable {
var array : [Int]
var dict : [String : String]
}
return try req.make(Client.self).get("http://example.vapor.codes/json").flatMap(to: ExampleData.self) { res in
//not sure what to do to set the values of the ExampleData
}
return try req.view().render("home", ExampleData())
}
}
Example code
I strongly recommend you read the explaination below, but this is the code.
struct ExampleData: Codable {
var array : [Int]
var dict : [String : String]
}
// Register a GET /example route
router.get("example") { req -> Future<View> in
// Fetch an HTTP Client instance
let client = try req.make(Client.self)
// Send an HTTP Request to example.vapor.codes/json over plaintext HTTP
// Returns `Future<Response>`
let response = client.get("http://example.vapor.codes/json")
// Transforms the `Future<Response>` to `Future<ExampleData>`
let exampleData = response.flatMap(to: ExampleData.self) { response in
return response.content.decode(ExampleData.self)
}
// Renders the `ExampleData` into a `View`
return try req.view().render("home", exampleData)
}
Futures
A Future<Expectation> is a wrapper around the Expectation. The expectation can be successful or failed (with an Error).
The Future type can register callbacks which are executed on successful completion. One of these callbacks that we use here is flatMap. Let's dive into a regular map, first.
If you map a Future you transform the future's successful Expectation and transparently pass through error conditions.
let promise = Promise<String>()
let stringFuture = promise.future // Future<String>
let intFuture = stringFuture.map(to: Int.self) { string -> Int in
struct InvalidNumericString: Error {}
guard let int = Int(string) else { throw InvalidNumericString() }
return int // Int
}
intFuture.do { int in
print("integer: ", int)
}.catch { error in
print("error: \(error)")
}
If we complete the promise with a valid decimal integer formatted string like "4" it'll print integer: 4
promise.complete("4")
If we place any non-numeric characters in there like "abc" it'll throw an error inside the InvalidNumericString error which will be triggering the catch block.
promise.complete("abc")
No matter what you do, an error thrown from a map or flatMap function will cascade transparently through other transformations. Transforming a future will transform the Expectation only, and only be triggered on successful cases. Error cases will be copied from the "base future" to the newly transformed future.
If instead of completing the promise you fail the promise, the map block will never be triggered and the AnyError condition will be found in the catch block instead.
struct AnyError: Error {}
promise.fail(AnyError())
flatMap works very similarly to the above example. It's a map where the trailing closure returns a Future<Expectation> rather than Expectation.
So If we'd rewrite the map block to be a flatMap, although impractical, we'll end up with this:
let intFuture = stringFuture.flatMap(to: Int.self) { string -> Future<Int> in
struct InvalidNumericString: Error {}
guard let int = Int(string) else { throw InvalidNumericString() }
return Future(int) // Int
}
intFuture is still a Future<Int> because the recursive futures will be flattened from Future<Future<Int>> to just Future<Int>.
Content
The response.content.decode bit reads the Content-Type and looks for the default Decoder for this Content Type. The decoded struct will then be returned as a Future<DecodedStruct>, in this case this struct is ExampleData.
The reason the content is returned asynchronously is because the content may not have completely arrived in the HTTP response yet. This is a necessary abstraction because we may be receiving files upwards of 100MB which could crash (cloud) servers with a small amount of memory available.
Logic
Back to the original route:
First make a client
Make a request to http://example.vapor.codes/json
Read the content from the Future<Response> asynchronously
Render the results into the view asynchronously
Return the Future<View>
The framework will understand that you're returning a Future<View> and will continue processing other requests rather than waiting on the results.
Once the JSON is received, this request will be picked up again and processed into a response which your web browser will receive.
Leaf is built on top of TemplateKit which will await the future asynchronously. Just like Vapor, Leaf and TemplateKit will understand Futures well enough that you can pass a Future instead of a struct (or vice versa) and they'll switch to anothe request until the future is completed, if necessary.