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.nn nView this post on Instagramn A post shared by Pizza Pizza (@pizzapizzaltd)nn '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
}
})
}
2
Answers
The thing that comes to my mind is that you need to decode totalPages as optional.
Check JSON data of the 2nd time response. Cause according to the error message:
,
total_pages
is missing.It might be a backend bug, depending on whether it makes sense that an
ArticleModel
doesn’t has atotal_pages
.If it is intended, then make
totalPages
optional:Response to edit#2:
| keyNotFound(CodingKeys(stringValue: "articles", intValue: nil).
This new error indicates
articles
beingnil
.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.