2

I'm quite new to Swift and programming altogether. I am very keen on learning all the right ways. So any additional tips or remarks are always appreciated.

I'm doing a HTTP request to an api and that works fine. The problem is that it's limited to 100 results per request. Theres's an optional offset and a limit i can set. If i give a limit of 101 i get a server error saying: "Bad Request: Invalid value specified for limit. Maximum allowed value is 100." The total is 101, so i need to do at least two requests. Only after receiving the total data of all the requests i want to populate my tableview. This is what i have:

class Book {

var id: Int
var title: String
let description: String
var coverImage: String
var isbn: String
var publisherID: Int
var publisherName: String
var authorID: Int
var authorFirstName: String
var authorLastName: String

class func getDataFromJson(completionHandler: ([Book]) -> ()) {

    var books = [Book]()

    let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())

    let request = NSURLRequest(URL: NSURL(string: "http://example.website.nl/books/highlighted")!)

    let task: NSURLSessionDataTask = session.dataTaskWithRequest(request) { (data, response, error) -> Void in

        if let data = data {

            do {
                let json = try NSJSONSerialization.JSONObjectWithData(data, options: .AllowFragments)

                if let booksFromResult = json["books"] as? [[String: AnyObject]] {
                    for book in booksFromResult {
                        let bookID = book["id"] as! Int
                        let bookTitle = book["title"] as! String
                        let bookDescription = book["description"] as! String
                        let bookCoverImage = book["cover_url"] as! String
                        let bookISBN = book["isbn"] as! String
                        if let bookPublisher = book["publisher"] as? [String: AnyObject] {
                            let bookPublisherID = bookPublisher["id"] as! Int
                            let bookPublisherName = bookPublisher["name"] as! String
                            if let bookAuthor = book["author"] as? [String: AnyObject] {
                                let bookAuthorID = bookAuthor["id"] as! Int
                                let bookAuthorFirstname = bookAuthor["first_name"] as! String
                                let bookAuthorLastName = bookAuthor["last_name"] as! String
                                books.append(Book(id: bookID, title: bookTitle, description: bookDescription, coverImage: bookCoverImage, isbn: bookISBN, publisherID: bookPublisherID, publisherName: bookPublisherName, authorID: bookAuthorID, authorFirstName: bookAuthorFirstname, authorLastName: bookAuthorLastName))
                            }
                        }

                    }
                    print(books.count)

                }
                dispatch_async(dispatch_get_main_queue(),{
                    completionHandler(books)
                })
            } catch {
                print("error serializing JSON: \(error)")
            }

        }
    }
    task.resume()
}




init(id: Int, title: String, description: String, coverImage: String, isbn: String, publisherID: Int, publisherName: String, authorID: Int, authorFirstName: String, authorLastName: String) {

    self.id = id
    self.title = title
    self.description = description
    self.coverImage = coverImage
    self.isbn = isbn
    self.publisherID = publisherID
    self.publisherName = publisherName
    self.authorID = authorID
    self.authorFirstName = authorFirstName
    self.authorLastName = authorLastName
  }
}

I have been trying to solve this for more than 24 hours. I have really searched here and on the web for an example. The little i found here couldn't help me.

My thoughts are of how this should be done:

  1. make first request -> store data somewhere
  2. make second request -> add data to stored data
  3. make last request -> add data to stored data
  4. send data to populate tableview.

Should i use an array of urls and iterate through it and than append the data somewhere?

I hope someone can help me. I would really appreciate it.

Thanks in advance.

FredFlinstone
  • 896
  • 11
  • 16
  • 1
    You say "The problem is that it's limited to 50 per request." But it isn't clear what that means. WHAT limits the response to 50 items? The server code? How are you supposed to request the next batch of 50 items then? Is there a defined protocol for finding out how many total items there are for a request, and then requesting batches of 50 results? – Duncan C Jul 24 '16 at 14:45
  • Hi Duncan,Sorry for not being clear, there's something they call an optional offset and limit. When i receive the batch i see that the limit is 50 and there's also a value total which says 101. – FredFlinstone Jul 24 '16 at 15:10
  • @ Duncan, you made me think in another way. I probably didn't understand the explanation. The limit is 100, but i need to get 101 back. If i give a limit of 101 i get a server error saying: "Bad Request: Invalid value specified for `limit`. Maximum allowed value is 100." – FredFlinstone Jul 24 '16 at 15:18
  • Ok, you need to edit your question to show that additional information. It sounds like you'll need to ask the server how many total entries their are, then if the result is > 50, write your code to loop through requesting 50 at a time, then sending a new request each time the previous 50 are received. – Duncan C Jul 24 '16 at 16:54
  • @Duncan, i edited the question, thanks. I already get back the total entries, that is 101. I do understand the logic of doing the requests one after the other. What i don't understand is how to them. Thanks. – FredFlinstone Jul 24 '16 at 17:06
  • Where is the code that shows how many responses you get from the server? Where is sample data from the server? Where are the optional offset and limit parameters? I don't see any of that. – Duncan C Jul 24 '16 at 18:17

1 Answers1

3

Direct Answer to the Question:

// Heavily based on the video I recommended. Watch it for a great explanation
struct Resource<A>{
    let url: NSURL
    let parse: (NSData) -> [A]?
}

extension Book {

    // You could figure a way to dynamically populate this based on limiting
    static let urls = [NSURL(string: "http://example.website.nl/books/highlighted")!,
                       NSURL(string: "http://example.website.nl/books/highlighted2")!]

    // Creates an array of Requests passing in each url for the url, but the same parse function
    static let requests = urls.map { Resource<Book>(url: $0, parse: Book.parse) }

    // Used by Webservice (from the Resource struct) to parse the data into a Book
    static let parse: (NSData?) -> [Book]? = { data in
        guard let data = data else { return nil }

        guard let json = try? NSJSONSerialization.JSONObjectWithData(data, options: .AllowFragments) else {
            print("Error deserializing json.")
            return nil
        }
        var books: [Book]? = nil
        guard let jsonBooks = json["books"] as? [[String: AnyObject]] else { return nil }
        for jsonBook in jsonBooks {
            guard let book = Book(fromJson: jsonBook) else { continue } // skips nil books from failable initializer, depends on how you want to handle that
            books = books ?? [Book]() // if nil create a new array, if not use the old one
            books!.append(book)
        }
        return books
    }
}

class Webservice {

    // A stands for a generic type. You could add a type called Publisher later and use the same function
    // This adopted from the video I showed you so it's a little more in depth
    func loadAll<A>(resources: [Resource<A>], completion: [A] -> ()) {
        let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
        var currentRequest = 0 // used to keep track of asynchronous callback order
        var allA = [A]()
        for resource in resources {
            session.dataTaskWithURL(resource.url) { (data, _, _) in
                defer {
                    currentRequest += 1 // marks that we're done with one request

                    // This check ensures that we only call the completion handler
                    // after we're done with the last request
                    if currentRequest == resources.count {
                        completion(allA)
                    }
                }
                guard let data = data else { return }

                // this parse function comes from the resource struct passed in.
                // It converts the data we get back from one request into an array of books.
                guard let manyA = resource.parse(data) else { return }

                // This is the total running tally of books from all our requests.
                allA.appendContentsOf(manyA)
            }
        }
    }
}

class TableViewController: UITableViewController {

    var books = [Book]() {
        didSet { tableView.reloadData() }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // Call site
        Webservice().loadAll(Book.requests) { [weak self] (books) in
            dispatch_async(dispatch_get_main_queue()) {
                self?.books.appendContentsOf(books)
            }
        }

    }

    //... all your normal methods for cells and stuff
}

Extra

I made a failable initializer for your Book class based on a JSON object so that your class method doesn't have to do the bulk of the parsing. You probably want your Book type to be a struct to gain the memberwise initializer and pass by value semantics)

Take advantage of guard let else control statements to avoid the pyramid of doom from optional unwrapping.

defer statements are useful to call a completion handler once no matter how you exit your scope (avoid duplication of code).

I highly recommend this video showing a webservice api design. It's a bit advanced but it shows you a great way initialize model objects from a webservice.

class Book {

    var id: Int
    var title: String
    let description: String
    var coverImage: String
    var isbn: String
    var publisherID: Int
    var publisherName: String
    var authorID: Int
    var authorFirstName: String
    var authorLastName: String

    init(id: Int, title: String, description: String, coverImage: String, isbn: String, publisherID: Int, publisherName: String, authorID: Int, authorFirstName: String, authorLastName: String) {

        self.id = id
        self.title = title
        self.description = description
        self.coverImage = coverImage
        self.isbn = isbn
        self.publisherID = publisherID
        self.publisherName = publisherName
        self.authorID = authorID
        self.authorFirstName = authorFirstName
        self.authorLastName = authorLastName
    }

    typealias JSONDictionary = [String: AnyObject] // syntactic sugar, makes it clearer

    convenience init?(fromJson json: JSONDictionary) {
        let bookID = json["id"] as! Int
        let bookTitle = json["title"] as! String
        let bookDescription = json["description"] as! String
        let bookCoverImage = json["cover_url"] as! String
        let bookISBN = json["isbn"] as! String

        // I would use guard let else statements here to avoid the pyramid of doom but it's stylistic
        if let bookPublisher = json["publisher"] as? [String: AnyObject] {
            let bookPublisherID = bookPublisher["id"] as! Int
            let bookPublisherName = bookPublisher["name"] as! String
            if let bookAuthor = json["author"] as? [String: AnyObject] {
                let bookAuthorID = bookAuthor["id"] as! Int
                let bookAuthorFirstname = bookAuthor["first_name"] as! String
                let bookAuthorLastName = bookAuthor["last_name"] as! String
                self.init(id: bookID, title: bookTitle, description: bookDescription, coverImage: bookCoverImage, isbn: bookISBN, publisherID: bookPublisherID, publisherName: bookPublisherName, authorID: bookAuthorID, authorFirstName: bookAuthorFirstname, authorLastName: bookAuthorLastName)
                return

            }
        }
        return nil
    }

}

extension Book {

    class func getDataFromJson(completionHandler: ([Book]) -> ()) {

        var books = [Book]()

        let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())

        let request = NSURLRequest(URL: NSURL(string: "http://example.website.nl/books/highlighted")!)

        let task: NSURLSessionDataTask = session.dataTaskWithRequest(request) { (data, response, error) -> Void in

            defer { // no matter how you exit the scope this will be called
                dispatch_async(dispatch_get_main_queue()) {
                    completionHandler(books)
                }
            }

            guard let data = data else { return } // still will call the deferred completion handler

            guard let json = try? NSJSONSerialization.JSONObjectWithData(data, options: .AllowFragments) else {
                print("Error deserializing json.")
                return // still will call the deferred completion handler
            }

            if let jsonBooks = json["books"] as? [[String: AnyObject]] {
                for jsonBook in jsonBooks {
                    guard let book = Book(fromJson: jsonBook) else { continue } // skips nil books from failable initializer, depends on how you want to handle that
                    books.append(book)
                }
                print(books.count)
            }
        }
        task.resume()
        // call the deferred completion handler after leaving scope
    }
}
Tom Magnusson
  • 195
  • 2
  • 9
  • @ Tommy, thank you very much. I was indeed planning to reorder stuff and to use a struct and enums for error handeling. Definitely will use guard let else. I will study your code and try to understand what it all does, thank you. By going through it, there's one thing i am not sure of. I need to o three requests with different urls. I have to change the offset in the second and third request to get the right batch back. I don't seem to find this option in the code you provided. Maybe i am not understanding well enough? Or i wasn't clear enough with my explanation? – FredFlinstone Jul 24 '16 at 16:57
  • I didn't realize you had to change the url for each request. I'll submit an answer for that in a minute. – Tom Magnusson Jul 24 '16 at 17:19
  • Yes, sorry, that wasn't clear. Thanks for the help, you're great! – FredFlinstone Jul 24 '16 at 17:36
  • 1
    you are an inspiration!! Thank you very much! This will keep me busy for a good while trying to understand what goes on. Thanks for the added comments, this helps a lot! I wanted to up vote your answer but it seems i lack reputation :-( . I'm almost there, i need 4 more. As soon as i have them i will up vote. – FredFlinstone Jul 24 '16 at 19:11
  • haha no problem, if you need any further explanation just ask. And I will warn you that this is only checked by the compiler, I haven't run any tests to make sure each part works at runtime (namely the json, so that's your duty). But if you get any weird compilation issues just comment. – Tom Magnusson Jul 24 '16 at 19:16