How to set the value of lazy computed property via a closure in Swift? - 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.

Related

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

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.

How do I wait for a download to complete before continuing?

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
}
}

how to change a variable after data changes in swift

I am using an API to learn how API's work and I wrote a function that takes data from the web and parses it to my model but when I want to print anything from that model it does not work except it is in my function.
import UIKit
class MovieDetailViewController: UIViewController {
var selectedMovie : Int?
var movieDetail : MovieDetailModel? = nil
override func viewDidLoad() {
super.viewDidLoad()
getMovioesDetail(url: "\(URLs().baseURL)/movie/\(selectedMovie!)\(URLs().API_key)")
print(movieDetail?.backdrop_path)
// It does not print the path
}
//MARK: - Functions
func getMovioesDetail (url: String) {
WebServices().get(url) { (type, model : MovieDetailModel?) in
switch type {
case .Succeed :
self.movieDetail = model
print(self.movieDetail?.backdrop_path)
// It prints the path
case .Failed : print("Failed")
case .FailedDecode : print("FailedDecode")
default : break
}
}
}
}
as #Aaron Cyrman stated, your function includes a web service call and the web service runs asynchronously, meaning that it is not known when you get a response.
That's why if you want to print out something or reload your tableview/collectionview, do that in case .Succeed; not right after calling your function.
Also you better do nil check of your model class after filling it.
Just to show an example:
case .Succeed:
self.movieDetail = model
if self.movieDetail != nil{
print(movieDetail!.backdrop_path)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
break
Without knowing more about WebServices is difficult to know.
I'm guessing the WebServices().get(url) runs asynchronous in a different thread, so the closure is being call when the API request returns data and that's why the print works.
When you are executing code in viewDidLoad is running on the main thread. Immediately after the getMovioesDetail call, print(movieDetail?.backdrop_path) is executed and most likely movieDetail is still nil since the WebService hasn't finish.
To be certain change:
print(movieDetail?.backdrop_path)
with this:
print(movieDetail?.backdrop_path ?? "Movie detail not available yet")
If the web service runs very fast you will see the path printed two times, if not you will first the Movie detail not available yet and then the path.

Swift 5: JSON from Alamofiire

I have a class that contains some Alamofire code to get JSON from a server, convert it into a pre-defined model and then return that model as an array.
Here is the code
func GetLights(completionHandler: #escaping (DataResponse<[LightList]>) -> Void) -> Alamofire.DataRequest {
return AF.request(APIString + "/lights").responseJSON { response in
let LightListResponse = response.flatMap { json in
try JSONDecoder().decode([LightList].self, from: response.data!)
}
completionHandler(LightListResponse)
}
}
func GetLightList() {
GetLights { response in
if let lights = response.value {
print(lights)
}
}
}
I can breakpoint through to the JSONDecoder and see the json via debug but the print line at the end prints nothing, it doesn't even hit a breakpoint.
Can anyone see what I'm doing wrong? I think I'm using the completion handler correctly?
I am calling the GetLightList via a SwiftUI file like so:
func InitList() {
let requests = Requests()
requests.GetLightList()
}
You shouldn't be doing this using responseJSON, as that method has already parsed the JSON using JSONSerialization and made it available to you as part of the response. Instead, you should use responseDecodable, since you already have a Decodable type.
return AF.request(apiString + "/lights").responseDecodable(of: [LightList].self) { response in
completionHandler(response)
}
However, it's often best not to expose the DataResponse type produced by Alamofire but instead use the Result from the response in your completion handler.
Additionally, updating your styling to match Swift's recommended style will help you write consistent code. Namely, methods and variable names should start with a lowercase letter to separate them from type declarations. You can see this in your code samples where it thinks things like "APIString" are types and not variables.
Finally, it's often helpful to not overload get as a method prefix. For network calls I like using fetch when requesting a resource. e.g. fetchLights.

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)
})
}