0

Still new in Swift. I've searched through many similar questions asked about the subject before and for some reason still can't pinpoint where the issue is in how my structs are built or what I am doing wrong. I'd appreciate your help!

I am using newswatcher api in my swift application, and the data I fetch is built like this:

{
    "status": "ok",
    "total_hits": 10000,
    "page": 1,
    "total_pages": 10000,
    "page_size": 1,
    "articles": [
        {
            "title": "Pizza Pizza has teamed up with Mattel for the ultimate game night combo",
            "author": null,
            "published_date": "2022-03-18 20:34:17",
            "published_date_precision": "full",
            "link": "https://dailyhive.com/vancouver/pizza-pizza-mattel-canada-uno",
            "clean_url": "dailyhive.com",
            "excerpt": "Pizza and game nights are a match made in heaven. Pizza Pizza has partnered with Mattel Canada for the ultimate package deal.",
            "summary": "Pizza and game nights are a match made in heaven — even Pizza Pizza knows that. The Canadian chain has partnered with Mattel Canada for the ultimate package deal.\nReturning for another year is the pizza chain's UNO collab but this time it features a new limited-edition Pizza Pizza or Pizza 73 branded UNO deck with every featured UNO combo purchase.\nThe decks feature pizza art and a surprise bonus offer too.\n\n \nView this post on Instagram\n A post shared by Pizza Pizza (@pizzapizzaltd)\n\n 'For over 50 years, UNO has become a staple at game nights across the country, bringing families and friends together through gameplay,' said Jennifer Gileno, Head of Licensing and Retail Development for Mattel Canada.",
            "rights": null,
            "rank": 9543,
            "topic": "news",
            "country": "CA",
            "language": "en",
            "authors": [],
            "media": "https://images.dailyhive.com/20220318132250/pizza-pizza-uno-500x256.jpg",
            "is_opinion": false,
            "twitter_account": "https://dailyhive.com/vancouver/pizza-pizza-mattel-canada-uno",
            "_score": 14.017945,
            "_id": "feca5f5fe473e561bf0b8c11b01b87bf"
        }
    ],
    "user_input": {
        "q": "pizza",
        "search_in": [
            "title_summary"
        ],
        "lang": null,
        "not_lang": null,
        "countries": null,
        "not_countries": null,
        "from": "2022-03-15 00:00:00",
        "to": null,
        "ranked_only": "True",
        "from_rank": null,
        "to_rank": null,
        "sort_by": "relevancy",
        "page": 1,
        "size": 1,
        "sources": null,
        "not_sources": null,
        "topic": null,
        "published_date_precision": null
    }
}

I have created the following structs in order to decode the data:

struct ArticleModel: Codable {
    
    let totalPages: Int
    let articles: [Articles]
    let numOfResults: Int
    
    enum CodingKeys: String, CodingKey {
        case totalPages = "total_pages"
        case articles = "articles"
        case numOfResults = "total_hits"
    }
}

struct Articles: Codable {
    
    let id: String
    let articleTitle: String
    let date: String
    let url: String
    let content: String
    let author: String?
    let topic: String
    let imageUrl: String?
    
    enum CodingKeys: String, CodingKey {
        case id = "_id"
        case articleTitle = "title"
        case content = "summary"
        case author = "author"
        case url = "link"
        case date = "published_date"
        case topic = "topic"
        case imageUrl = "media"
    }
}

I am using Pagination in my app and my initial fetch is working great with no issue. However - when scrolling down to the bottom of the UITableView I fire another fetch request (for the next batch of data i.e. page 2 of data) and I get the following error in my console:

keyNotFound(CodingKeys(stringValue: "total_pages", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "total_pages", intValue: nil) ("total_pages").", underlyingError: nil))

The pagination works fine, the data-batches are retrieved as should.. but I don't understand why this error keeps popping and why it happens only when fetching from the 2nd time forward.

Edit #1: No matter which query or page I fetched for - total_pages is always returned in the results and always has a value.

Edit #2: I tried making total_pages optional but then the error in the console changes to:

keyNotFound(CodingKeys(stringValue: "articles", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "articles", intValue: nil) ("articles").", underlyingError: nil)) which also doesn't make any sense because I am seeing the new results on the screen..

Edit #3: Here is the response I am getting back on the 2nd page - From Postman:

{
    "status": "ok",
    "total_hits": 10000,
    "page": 2,
    "total_pages": 10000,
    "page_size": 1,
    "articles": [
        {
            "title": "Broadway & Beyond Hosts Webinar on Anti-Racist Stage Management Practices",
            "author": "Raven Brunner",
            "published_date": "2022-03-21 17:17:43",
            "published_date_precision": "full",
            "link": "https://playbill.com/article/broadway-beyond-hosts-webinar-on-anti-racist-stage-management-practices",
            "clean_url": "playbill.com",
            "excerpt": "The webinar will be led by veteran stage managers Narda E. Alcorn and Lisa Porter.",
            "summary": "Education News Education News Education News Education News Theatre Alternatives Industry News Industry News Industry News Education News Education News Education News Education News Education News Education News Industry News Industry News Industry News Education News Education News Industry News Industry News Education News Education News Industry News Education News Industry News Education News Industry News Industry News Education News Education News Industry News Education News Industry New",
            "rights": "playbill.com",
            "rank": 5215,
            "topic": "entertainment",
            "country": "US",
            "language": "en",
            "authors": [
                "Raven Brunner"
            ],
            "media": "https://assets.playbill.com/editorial/_1200x630_crop_center-center_82_none/Narda-E.-Alcorn-and-Lisa-Porter_HR.jpg?mtime=1647876883",
            "is_opinion": false,
            "twitter_account": "@playbill",
            "_score": 5.5872316,
            "_id": "7e297f463684c344e3bb9b70d6229fbf"
        }
    ],
    "user_input": {
        "q": "news",
        "search_in": [
            "title_summary"
        ],
        "lang": null,
        "not_lang": null,
        "countries": null,
        "not_countries": null,
        "from": "2022-03-15 00:00:00",
        "to": null,
        "ranked_only": "True",
        "from_rank": null,
        "to_rank": null,
        "sort_by": "relevancy",
        "page": 2,
        "size": 1,
        "sources": null,
        "not_sources": null,
        "topic": null,
        "published_date_precision": null
    }
}

From the console:

ArticleModel(totalPages: 10000, articles: [Dispatcher_Development.Articles(id: "7e297f463684c344e3bb9b70d6229fbf", articleTitle: "Broadway & Beyond Hosts Webinar on Anti-Racist Stage Management Practices", date: "2022-03-21 17:17:43", url: "https://playbill.com/article/broadway-beyond-hosts-webinar-on-anti-racist-stage-management-practices", content: "Education News Education News Education News Education News Theatre Alternatives Industry News Industry News Industry News Education News Education News Education News Education News Education News Education News Industry News Industry News Industry News Education News Education News Industry News Industry News Education News Education News Industry News Education News Industry News Education News Industry News Industry News Education News Education News Industry News Education News Industry New", author: Optional("Raven Brunner"), topic: "entertainment", imageUrl: Optional("https://assets.playbill.com/editorial/_1200x630_crop_center-center_82_none/Narda-E.-Alcorn-and-Lisa-Porter_HR.jpg?mtime=1647876883"))], numOfResults: 10000)

In case it matters - here is how I detect the user scrolled all the way down:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
        
        let position = scrollView.contentOffset.y
        if position > (tableView.contentSize.height - 100 - scrollView.frame.size.height) {
            fetchNewsFromAPI() {
                DispatchQueue.main.async {
                    self.tableView.tableFooterView = nil
                }
            }
        }
    }

and this is the function that is responsible for fetching the data:

func fetchNewsFromAPI(completionHandler: @escaping () -> ()) {
        
        let alamofireQuery = AlamofireManager(from: "\(Constants.apiCalls.newsUrl)?q=news&page_size=\(amountToFetch)&page=\(currentPaginationPage)")
        
        if !alamofireQuery.isPaginating && currentPaginationPage <= totalPaginationPages {
            alamofireQuery.executeGetQuery(){
                (result: Result<ArticleModel,Error>) in
                switch result {
                case .success(let response):
                    self.currentPaginationPage += 1
                    self.totalPaginationPages = response.totalPages
                    
                    self.newsArray.append(contentsOf: response.articles)
                    DispatchQueue.main.async {
                        self.dataSource.models = self.newsArray
                        self.tableView.reloadData()
                    }
                case .failure(let error):
                    print(error)
                }
                completionHandler()
            }
        }
    }

and this is the executeQuery function inside my Alamofire file:

func executeGetQuery<T>(completion: @escaping (Result<T, Error>) -> Void) where T: Codable {
        isPaginating = true
        AF.request(url, method: .get, headers: headers).responseData(completionHandler: { response in
            do {
                switch response.result {
                case .success:
                    completion(.success(try JSONDecoder().decode(T.self, from: response.data ?? Data())))
                    self.isPaginating = false
                case .failure(let error):
                    completion(.failure(error))
                }
            } catch let error {
                completion(.failure(error))
                self.isPaginating = false
            }
        })
    }
NuK
  • 7
  • 4
  • Does `total_pages` present in the response of the page 2? – lazarevzubov Mar 22 '22 at 07:27
  • Yes I checked it using Postman and no matter which query or page you ask for - total_pages is a property that always comes back and also always has a value @lazarevzubov – NuK Mar 22 '22 at 07:32
  • Could you show the response of the page 2 then? The one that is crashing on decoding. – lazarevzubov Mar 22 '22 at 07:44
  • Updated my question - please see Edit #3 @lazarevzubov – NuK Mar 22 '22 at 07:51
  • 1
    "Yes I checked it using Postman", could you instead print the real value you are getting from your call?: `String(data: response.data, encoding: .utf8)`? Also, unreleated but: `response.data ?? Data()`, the `data` variable should be in the result switch case associated value (see https://stackoverflow.com/questions/70789753/update-responsejson-to-responsedecodable-in-swift/70804441#70804441 with `.sucess(let data)`), and since you use `Decodable`, why not using directly `responseDecodable()` instead of `responseData()`? – Larme Mar 22 '22 at 08:23
  • I added the results as they appear in the console under "Edit #3". Regarding your other question - I took the code's structure from an example I found. I will read about your suggestion once I figure out this error issue, thanks! @Larme – NuK Mar 23 '22 at 06:59
  • add `print(String(data: response.data!, encoding: .utf8))` just before `completion(.success(try JSONDecoder().decode(T.self, from: response.data ?? Data())))` And show us what it prints, not what you get from Postman. – workingdog support Ukraine Mar 23 '22 at 07:25

2 Answers2

0

The thing that comes to my mind is that you need to decode totalPages as optional.

let totalPages: Int?
stoikokolev
  • 484
  • 5
  • 9
  • I tried it but when I do so - then the error changes to: `keyNotFound(CodingKeys(stringValue: "articles", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"articles\", intValue: nil) (\"articles\").", underlyingError: nil))` ... which makes no sense as clearly I am getting results back and they are indeed displayed on my screen @stoikokolev – NuK Mar 22 '22 at 07:38
  • There should be a documentation of the API you are using and there should be marked which key has mandatory or optional value. Maybe articles are also optional. – stoikokolev Mar 22 '22 at 07:42
  • I indeed checked the documentation over here: [link](https://docs.newscatcherapi.com/api-docs/endpoints/search-news) and unfortunately they don't specify which fields are mandatory and which are optionals. However, from playing around with the API I am 99% certain that the ```total_pages``` is mandatory in the response and is never nil @stoikokolev – NuK Mar 22 '22 at 07:48
-1

why it happens only when fetching from the 2nd time forward

Check JSON data of the 2nd time response. Cause according to the error message:

keyNotFound(CodingKeys(stringValue: "total_pages", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "total_pages", intValue: nil) ("total_pages").", underlyingError: nil))

, total_pages is missing.

It might be a backend bug, depending on whether it makes sense that an ArticleModel doesn't has a total_pages.

If it is intended, then make totalPages optional:

let totalPages: Int?

Response to edit#2:

| keyNotFound(CodingKeys(stringValue: "articles", intValue: nil).

This new error indicates articles being nil.

The error provided by Apple is quite straightforward. It's just the lack of formatting that confuses people, same with the auto-layout error info.

I'm doing nothing but interpreting the debug info for you.

Postman doesn't necessarily produce the same responses with your real-world requests. You should instead use tools like Proxyman or Charles to capture the responses to your simulator or test device.

iMoeNya
  • 672
  • 4
  • 11
  • I updated my question (Edit #3) to include the response of the 2nd page and the ```total_pages``` is indeed there and is not missing. I also tried making this field and optional, but then the error message switches to the next field as can be seen in my Edit #2 @iMoeNya – NuK Mar 22 '22 at 07:54