How do I split a JSON string in Swift? [duplicate] - ios

This question already has answers here:
Split a String into an array in Swift?
(40 answers)
Closed 3 years ago.
I'm writing an app that pulls data from the Timezonedb API to get a list of available timezones:
https://timezonedb.com/references/list-time-zone
I'm trying to parse the zoneName which is currently formatted as "Europe/Andorra". My question is how do I split the JSON string to just display city name i.e "Andorra" in a tableview?
Here's the response I'm getting back:
{
"status":"OK",
"message":"",
"zones":[
{
"countryCode":"AD",
"countryName":"Andorra",
"zoneName":"Europe\/Andorra",
"gmtOffset":7200,
"timestamp":1464453737
},
{
"countryCode":"AE",
"countryName":"United Arab Emirates",
"zoneName":"Asia\/Dubai",
"gmtOffset":14400,
"timestamp":1464460937
},
{"
countryCode":"AF",
"countryName":"Afghanistan",
"zoneName":"Asia\/Kabul",
"gmtOffset":16200,
"timestamp":1464462737
}]}
Here's my code:
Model:
struct TimeZones: Codable {
let status, message: String?
let zones: [Zone]?
}
struct Zone: Codable {
let countryCode, countryName, zoneName: String?
let gmtOffset, timestamp: Int?
}
Here's the networking code:
var cities: [Zone] = []
func getAvailableTimeZones() {
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let url = Bundle.main.url(forResource: "data", withExtension: "json")!
let task = session.dataTask(with: url) { data, response, error in
// Check for errors
guard error == nil else {
print ("error: \(error!)")
return
}
// Check that data has been returned
guard let content = data else {
print("No data")
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let timeZones = try decoder.decode(TimeZones.self, from: content)
if let zones = timeZones.zones {
self.cities.append(contentsOf: zones)
}
} catch let err {
print("Err", err)
}
}
// Execute the HTTP request
task.resume()
}
TableViewController:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "searchResultsCell", for: indexPath)
cell.textLabel?.text = cities[indexPath.row].zoneName
return cell
}
Any help appreciated. Thank you.

A solution is to add CodingKeys and a computed property.
And there is no reason to declare the struct members as optional
struct TimeZones: Decodable {
let status, message: String
let zones: [Zone]
}
struct Zone: Decodable {
let countryCode, countryName, zoneName: String
let gmtOffset, timestamp: Int
private enum CodingKeys: String, CodingKey { case countryCode, countryName, zoneName, gmtOffset, timestamp}
lazy var zoneCountry : String = {
return zoneName.components(separatedBy: "/").last!
}()
}
and use it
cell.textLabel?.text = cities[indexPath.row].zoneCountry

To get the desired output with the minimal change you just have to update cellForRowAt() method like:
let zoneName = cities[indexPath.row].zoneName.components(separatedBy: "/").last ?? ""
cell.textLabel?.text = zoneName

Related

How to debug JSONDecoder when no errors show?

I'm building a Swift app and testing in an Xcode Playground. Calling the NYTimes Search API and trying to store its response in a struct. The code executes cleanly and no errors appear (I am using a do, try, catch), but I cannot print any properties from the resulting object (print(json.status)).
My hunch is that something is fishy with this line but I'm not sure what since no errors are printing from the catch statement
let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { data, response, error in
Form the URL endpoint to make the API call:
func APICall() {
let APIKey = "MY_API_KEY_GOES_HERE_BUT_IT'S_A_SECRET"
let searchTerm = "A Super Bowl Sideshow: See the Ageless Man!"
// Remove the spaces and convert them to percents
guard let encodedSearchTerm = searchTerm.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)
else {
print("Error encoding search term in URL")
return
}
let url = "https://api.nytimes.com/svc/search/v2/articlesearch.json?q=" + encodedSearchTerm + "&api-key=" + APIKey
getData(from: url)
}
Data Task:
func getData(from url: String) {
//I believe something is wrong with the following line but I'm not sure what it is
let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { data, response, error in
guard let data = data, error == nil else {
print("Error loading data")
return
}
var result: NYTSearchResponse?
do {
result = try JSONDecoder().decode(NYTSearchResponse.self, from: data)
} catch {
print(error)
}
guard let json = result else {
print("Error assigning result to json")
return
}
//Try to print these from the resulting object but these commands do not print
print(json.status)
print(json.response.docs[0].abstract)
})
task.resume()
}
My NYTSearchResponse struct which mirrors the NYT API JSON response. It's pretty complicated, but I pasted the json response into https://app.quicktype.io/ to build the struct.
// MARK: - Welcome
struct NYTSearchResponse: Codable {
let status, copyright: String
let response: Response
}
// MARK: - Response
struct Response: Codable {
let docs: [Doc]
let meta: Meta
}
// MARK: - Doc
struct Doc: Codable {
let abstract: String
let webURL: String
let snippet, leadParagraph, printSection, printPage: String
let source: String
let multimedia: [Multimedia]
let headline: Headline
let keywords: [Keyword]
let pubDate: Date
let documentType, newsDesk, sectionName, subsectionName: String
let byline: Byline
let typeOfMaterial, id: String
let wordCount: Int
let uri: String
enum CodingKeys: String, CodingKey {
case abstract
case webURL = "web_url"
case snippet
case leadParagraph = "lead_paragraph"
case printSection = "print_section"
case printPage = "print_page"
case source, multimedia, headline, keywords
case pubDate = "pub_date"
case documentType = "document_type"
case newsDesk = "news_desk"
case sectionName = "section_name"
case subsectionName = "subsection_name"
case byline
case typeOfMaterial = "type_of_material"
case id = "_id"
case wordCount = "word_count"
case uri
}
}
// MARK: - Byline
struct Byline: Codable {
let original: String
let person: [Person]
let organization: String?
}
// MARK: - Person
struct Person: Codable {
let firstname: String
let middlename: String?
let lastname: String
let qualifier, title: String?
let role, organization: String
let rank: Int
}
// MARK: - Headline
struct Headline: Codable {
let main: String
let kicker, contentKicker: String?
let printHeadline: String
let name, seo, sub: String?
enum CodingKeys: String, CodingKey {
case main, kicker
case contentKicker = "content_kicker"
case printHeadline = "print_headline"
case name, seo, sub
}
}
// MARK: - Keyword
struct Keyword: Codable {
let name, value: String
let rank: Int
let major: String
}
// MARK: - Multimedia
struct Multimedia: Codable {
let rank: Int
let subtype: String
let caption, credit: String?
let type, url: String
let height, width: Int
let legacy: Legacy
let subType, cropName: String
enum CodingKeys: String, CodingKey {
case rank, subtype, caption, credit, type, url, height, width, legacy, subType
case cropName = "crop_name"
}
}
// MARK: - Legacy
struct Legacy: Codable {
let xlarge: String?
let xlargewidth, xlargeheight: Int?
}
// MARK: - Meta
struct Meta: Codable {
let hits, offset, time: Int
}
I moved the code out of the playground and it works.

How to retrieve Cloud Firestore documents and display data in a TableView?

I have my data structure: My Firestore Database
As you'll see I have a "Michael 201A" document as well as a "Michael 201B" the idea is to retrieve the fields from these documents and display them in a tableView. Additionally, i would like the tableView to update automatically based off of any new documents that are added to the "Requests" Collection so the tableView data is always populated wit the most recent additions to the firestore database.
Function to retrieve data from FireStore
#IBOutlet weak var tableView: UITableView!
var db: Firestore!
var requestArray = [Request]()
override func viewDidLoad() {
super.viewDidLoad()
db = Firestore.firestore()
tableView.delegate = self
tableView.dataSource = self
loadData()
}
func loadData() {
db.collection("Requests").whereField("Status", isEqualTo: true).getDocuments() {(querySnapshot, err) in
if let err = err {
print("An error occurred\(err)")
} else{
self.requestArray = querySnapshot!.documents.compactMap({Request(dictionary: $0.data())})
print(self.requestArray)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
I've added a print statement to get a reading of the value but it returns empty.
My tableView functions
extension ResidentAdvisorViewController: UITableViewDelegate {
func tableView (_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("You tapped me")
}
}
extension ResidentAdvisorViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return requestArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let request = requestArray[indexPath.row]
cell.textLabel?.text = "\(request.Name)"
return cell
}
}
My Request Struct
protocol DocumentSerializable {
init?(dictionary:[String:Any])
}
struct Request {
var Name: String
var Dorm: String
var Room: Int
var Status: Bool
var UID: String
var TimeStamp: Date
var dictionary:[String:Any] {
return [
"Name":Name,
"Dorm":Dorm,
"Room":Room,
"Status":Status,
"UID": UID,
"TimeStamp": TimeStamp
]
}
}
extension Request : DocumentSerializable {
init?(dictionary: [String : Any]) {
guard let name = dictionary["Name"] as? String,
let dorm = dictionary["Dorm"] as? String,
let room = dictionary["Room"] as? Int,
let status = dictionary["Status"] as? Bool,
let uid = dictionary["UID"] as? String,
let timestamp = dictionary["Timestamp"] as? Date
else { return nil}
self.init(Name: name, Dorm: dorm, Room: room, Status: status, UID: uid, TimeStamp: timestamp)
}
}
As a side note i have checked to ensure my cell identifier matches "cell". Also, when i change the cell text to "Hello World" I am able to get it displayed in my tableView. Any assistance is greatly appreciated thank you.
There's not a whole lot wrong with the code but there are two questions within the question.
1) Why is the value empty
2) How to I populate my dataSource intially and then update it when new documents are added.
Let me address 2) first.
To initially load the data and then watch for future changes, we can uyse the .addSnapshotListener function, and then handle the specific change type within the firebase closure.
func observeAllRequests() {
let requestsCollection = self.db.collection("Requests")
let query = requestsCollection.whereField("Status", isEqualTo: true)
query.addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
snapshot.documentChanges.forEach { diff in
if (diff.type == .added) {
let name = diff.document.get("Name") as? String ?? "No Name"
print("added: \(name)") //add to your dataSource
}
if (diff.type == .modified) {
let name = diff.document.get("Name") as? String ?? "No Name"
print("modified: \(name)") //update the request in the dataSource
}
if (diff.type == .removed) {
let name = diff.document.get("Name") as? String ?? "No Name"
print("removed: \(name)") //remove the request from the dataSource
}
}
//tableView.reloadData()
}
}
The above code will return all of the documents that match the query. Iterate over the items in the snapshot, with each being either .added, .modified or .removed. The first time the function is called, all differences will be .childAdded which allows you to initially populate the dataSource.
Any document changes after that will be just the document that was changed with the difference being by .added, .modified and .removed.
EDIT:
To address question 1)
The reason the array is empty is because of how the extension is structured - it's pretty much an all or none. Here's how it is now
extension Request : DocumentSerializable {
init?(dictionary: [String : Any]) {
guard let name = dictionary["name"] as? String
let dorm = dictionary["Dorm"] as? String,
let room = dictionary["Room"] as? Int,
let status = dictionary["Status"] as? Bool,
let uid = dictionary["UID"] as? String,
let timestamp = dictionary["Timestamp"] as? String
else { return nil}
self.init(Name: name)
} }
If a field is not found then the entire thing fails and returns nil, and compactMap igores nil so you end up when an empty array. Your structure does not include Timestamp, so it fails.
I would suggest something to protect your code but allow for missing fields. The nil-coalescing operator would work well here
extension Request : DocumentSerializable {
init?(dictionary: [String : Any]) {
let name = dictionary["name"] as? String ?? "No Name"
let room = dictionary["room") as? String ?? "No Room"
etc

Parsing multi-level JSON with Codable

I'm trying to parse to following JSON into a tableView : https://www.pathofexile.com/api/trade/data/items
I succeeded in parsing the first array, but I'm unable to parse the key "entries"...
Here's my code, with the data structure I defined :
import UIKit
struct ItemCategories: Codable {
var result: [ItemCategory]
}
struct ItemCategory: Codable {
var label: String
var entries: [Item]
}
struct Item: Codable {
// empty struct
}
class ViewController: UITableViewController {
let urlString = "https://www.pathofexile.com/api/trade/data/items"
var categories = [ItemCategory]()
override func viewDidLoad() {
super.viewDidLoad()
title = "Path of Data"
navigationController?.navigationBar.prefersLargeTitles = true
parse()
}
func parse() {
guard let url = URL(string: urlString) else { return }
guard let data = try? Data(contentsOf: url) else { return }
let decoder = JSONDecoder()
guard let jsonItemCategories = try? decoder.decode(ItemCategories.self, from: data) else { return }
categories = jsonItemCategories.result
tableView.reloadData()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return categories.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
var categoryName = categories[indexPath.row].label
if categoryName == "" { categoryName = "Unknown" }
cell.textLabel?.text = categoryName
cell.textLabel?.textColor = .systemOrange
let numberOfItemsInCategory = String(categories[indexPath.row].entries.count)
cell.detailTextLabel?.text = numberOfItemsInCategory + " items"
return cell
}
}
The struct Item is empty, because when I try to add variable corresponding to the keys in the JSON, then the whole parsing fail (the tableView displays nothing).
When the struct Item is empty, then the parsing succeed and the tableView is able to display the different categories. It even display the number of items for each "entries" thanks to :
let numberOfItemsInCategory = String(categories[indexPath.row].entries.count)
cell.detailTextLabel?.text = numberOfItemsInCategory + " items"
Can someone explain why ? Ideally I would like to display the content of "entries" when the rows are tapped, but I can't figure out how for the moment.
Thanks for you help :)
screenshot
#Laurent Delorme Your Struct Item should be like below, try with this,
struct Item: Codable {
let name: String?
let type: String?
let text: String?
let flags: FlagsRepresentation?
enum CodingKeys: String, CodingKey {
case name
case type
case text
case flags
}
}
struct FlagsRepresentation: Codable {
let unique: Bool?
enum CodingKeys: String, CodingKey {
case unique
}
}

How to handle dynamic keys from a JSON response using jsonDecoder?

How do I handle this JSON and parse this JSON using decoder in Swift 4? I tried several times but failed. I don't understand how to handle this JSON format.
[
{
products: {
id: 69,
name: "test",
des: "Hi this is a test",
sort_des: "this is a test category",
},
documents: {
0: {
id: 1,
name: "105gg_1992uu",
citation: "This is citation for 105gg_1992uu",
file: "105gg_1992uu.pdf",
created_at: "2019-01-25 09:07:09",
category_id: 69,
},
1: {
id: 2,
name: "96tt-1997tt",
citation: "This is citation for 96tt-1997tt",
file: "96tt-1997tt.pdf",
created_at: "2019-01-25 09:07:09",
category_id: 69,
},
},
}
]
I tried the following code.
This is my model class.
struct GenericCodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
self.intValue = intValue
self.stringValue = "\(intValue)"
}
}
struct Model : Codable {
struct Documents : Codable {
var id : Int?
var name : String?
var citation : String?
var file : String?
var created_at : String?
var category_id : Int?## Heading ##
private enum CodingKeys : String, CodingKey{
case id = "id"
case name = "name"
case citation = "citation"
case file = "file"
case created_at = "created_at"
case category_id = "category_id"
}
}
struct Products : Codable {
var id : Int?
var name : String?
var des : String?
var sort_des : String?
private enum CodingKeys: String, CodingKey{
case id = "id"
case name = "name"
case des = "des"
case sort_des = "sort_des"
}
}
var products : Products?
var documents : [Documents]?
private enum CodingKeys : String, CodingKey{
case products
case documents
}
init(from decoder: Decoder) throws {
self.documents = [Documents]()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.products = try container.decode(Products.self, forKey: .products)
let documents = try container.nestedContainer(keyedBy: GenericCodingKeys.self, forKey: .documents)
for doc in documents.allKeys{
let docEach = try documents.decode(Documents.self, forKey: doc)
self.documents?.append(docEach)
}
}
}
This is my fetch data from that JSON function
class LatestStatuesVC: UIViewController,UITableViewDelegate,UITableViewDataSource {
#IBOutlet var tableView: UITableView!
var caseData : [Model]?
var model : Model?
var countCaseData = 0
override func viewDidLoad() {
super.viewDidLoad()
downloadAllData()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return countCaseData
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: ReuseIdentifiers.cellLatestStatues, for: indexPath) as! LatestStatuesTableCell
return cell
}
//MARK: Download all documents into internal directory
func downloadAllData(){
let url = URL(string: URLString.urlForGetDocuments)
URLSession.shared.dataTask(with: url!) { (data, response, err) in
DispatchQueue.main.async {
do{
if err == nil {
let products = try JSONDecoder().decode(Model.Products.self, from: data!)
let documentAll = try JSONDecoder().decode([Model.Documents].self, from: data!)
print(products.name as Any)
self.countCaseData = documentAll.count
for doc in documentAll{
print(doc.name as Any)
print(doc.citation as Any)
}
}
}
catch let err{
print(err)
}
}
}.resume()
}
}
I get this error for this code.
typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil))
The error clearly says that the root object of the JSON is an array but you try to decode a dictionary.
Basically your structs are too complicated. If you want to have documents as an array by getting rid of the numeric dictionary keys just write a custom initializer in the root (Model) struct which decodes documents as dictionary and takes the values sorted by id as the documents array.
struct Model : Decodable {
let products : Product
let documents : [Document]
enum CodingKeys: String, CodingKey { case products, documents }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
products = try container.decode(Product.self, forKey: .products)
let documentData = try container.decode([String:Document].self, forKey: .documents)
documents = documentData.values.sorted(by: {$0.id < $1.id})
}
}
struct Product: Decodable {
let id : Int
let name, description, sortDescription : String
let type : String
}
struct Document: Decodable {
let id, categoryId : Int
let name, citation, file : String
let createdAt : Date
}
Then decode the JSON (assuming data represents the JSON as Data), the createdAt values are decoded as Date
let decoder = JSONDecoder()
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
decoder.dateDecodingStrategy = .formatted(dateFormatter)
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let modelArray = try decoder.decode([Model].self, from: data)
for model in modelArray {
print("products:",model.products)
print("documents:",model.documents)
}
} catch { print(error) }
The convertFromSnakeCase key decoding strategy converts all snake_cased keys to camelCased struct members without specifying any CodingKeys.

Firestore iOS - Delete function in UITableview

I am trying to delete a document from Firestore that appears as a UITableViewCell on my UITableView using the swipe to delete function.
var sourseArray : [Sourse] = []
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let sourseItem = sourseArray[indexPath.row]
Firestore.firestore().collection("sourses").document(sourseItem.documentId).delete(completion: { (error) in
if let error = error {
debugPrint("Could not delete thought: \(error.localizedDescription)")
}
})
}
}
When I swipe and hit the "delete" button. This error appears
*** Terminating app due to uncaught exception 'FIRInvalidArgumentException', reason: 'Invalid document reference.
Document references must have an even number of segments, but sourses
has 1'
I adjusted the "rules" of my Firestore database to allow for deleting.
After some research it appears that I'm not referencing the correct document somehow. Is it a bad reference or is the error something else?
Also here is what a "sourse" model is.
class Sourse {
private(set) var name: String!
private(set) var content: String!
private(set) var timeStamp: Date!
private(set) var documentId: String!
private(set) var userId: String!
init(name: String, timeStamp: Date, content: String, documentId: String, userId: String) {
self.name = name
self.content = content
self.timeStamp = timeStamp
self.documentId = documentId
self.userId = userId
}
}
//EDIT
I just noticed I did not add a documentId when creating a new sourse. As seen below.
#IBAction func addSourse(_ sender: Any) {
Firestore.firestore().collection(SOURSES_REF).addDocument(data: [
NAME : sourseTextField.text ?? "",
CONTENT : contentTextField.text ?? "",
TIMESTAMP : FieldValue.serverTimestamp(),
USERNAME : Auth.auth().currentUser?.displayName ?? "",
USER_ID : Auth.auth().currentUser?.uid ?? ""
]) { (err) in
if let err = err {
debugPrint("Error adding document document: \(err)")
} else {
self.navigationController?.popViewController(animated: true)
}
}
}
However, that is also the way it was in my tutorial and it worked fine.
///Edit 2 To show how I am fetching it.
func loadData() {
db.collection("sourses").getDocuments() { (snapshot, error) in
if let error = error {
print(error.localizedDescription)
} else {
if let snapshot = snapshot {
for document in snapshot.documents {
let data = document.data()
let name = data["name"] as? String ?? ""
let content = data["content"] as? String ?? ""
let timeStamp = data["timeStamp"] as? Date ?? Date()
let documentId = data["documentId"] as? String ?? ""
let userId = data["userId"] as? String ?? ""
let newSourse = Sourse(name:name, timeStamp: timeStamp, content:content, documentId: documentId, userId: userId)
self.sourseArray.append(newSourse)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
}
Answer: The function to swipe to delete was correct the whole time. As Dopapp pointed out, I was incorrectly loading my document.Id.
If the problem is indeed that your documentId is wrong, you may be retrieving it incorrectly. Here is a quick example of how to create your object with the right id:
collection.addSnapshotListener { (snapshot, error) in
if let documents = snapshot?.documents {
for document in documents {
guard let data = document.data() else { continue }
let id = document.documentID
let sourseItem = Sourse(name: data['name'], ..., documentId: id, ...)
// use sourseItem
}
}
}
If you are doing something similar, I would check if the document ids are being swapped between objects. If so, that might suggest an async-related problem.
For your particular case, loadData() should look like this:
func loadData() {
db.collection("sourses").getDocuments() { (snapshot, error) in
if let error = error {
print(error.localizedDescription)
} else {
if let snapshot = snapshot {
for document in snapshot.documents {
let data = document.data()
let name = data["name"] as? String ?? ""
let content = data["content"] as? String ?? ""
let timeStamp = data["timeStamp"] as? Date ?? Date()
let documentId = document.documentID
let userId = data["userId"] as? String ?? ""
let newSourse = Sourse(name:name, timeStamp: timeStamp, content:content, documentId: documentId, userId: userId)
self.sourseArray.append(newSourse)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
}

Resources