2

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.

Allen Ma
  • 95
  • 7
  • 1
    Terminology: this isn't a computed property, it's a stored property. All computed properties are "lazy". Semantics: you can't have a property asynchronously return a value in Swift. – jscs Jan 24 '19 at 00:41
  • 3
    Possible duplicate of [How to properly declare a computed property, when calculation uses background threads?](https://stackoverflow.com/questions/46595407/how-to-properly-declare-a-computed-property-when-calculation-uses-background-th) – jscs Jan 24 '19 at 00:45
  • Also see: https://stackoverflow.com/questions/25203556/returning-data-from-async-call-in-swift-function, particularly Rob Napier's answer – jscs Jan 24 '19 at 00:45
  • 1
    You are having that error because what `getInfo` returns is nothing (that is why the"()" on the error) and not a string. If you want to return something for `description` by calling your method, this needs to return String – rgkobashi Jan 24 '19 at 00:46

1 Answers1

0

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.

Rahul
  • 2,056
  • 1
  • 21
  • 37