I'm new to SwiftUI and trying to build a simple single-player Quiz app. I started by following this tutorial and then tried to continue on my own. Currently, the questions are hardcoded in my ViewModel but I'd like to change it to fetching from a local JSON file instead. Anyone that can point me in the right direction?
Here's a part of my ViewModel with the questions as static data:
extension GameManagerVM {
static var questions = quizData.shuffled()
static var quizData: [QuizModel] {
[
QuizModel(
id: 1,
question: "Question title 1",
category: "Sport",
answer: "A",
options: [
QuizOption(id: 11, optionId: "A", option: "A"),
QuizOption(id: 12, optionId: "B", option: "B"),
QuizOption(id: 13, optionId: "C", option: "C"),
QuizOption(id: 14, optionId: "D", option: "D")
]
),
...
}
}
Here's a test I did, but I get the error that Xcode can't decode it.
I replace the above code with this:
extension GameManagerVM {
static var questions = quizData.shuffled()
static var quizData: [QuizModel] = Bundle.main.decode("quizquestions2022.json")
}
And here's the JSON.
[
{
"id": "1",
"question": "Question title 1",
"category": "Sport",
"answer": "A",
"options": [
{
"id": "1001",
"optionId": "A",
"option": "A"
},
{
"id": "1002",
"optionId": "B",
"option": "B"
},
{
"id": "1003",
"optionId": "C",
"option": "C"
},
{
"id": "1004",
"optionId": "D",
"option": "D"
}
]
},
]
Here are my models
struct Quiz {
var currentQuestionIndex: Int
var quizModel: QuizModel
var quizCompleted: Bool = false
var quizWinningStatus: Bool = false
var score: Int = 0
}
struct QuizModel: Identifiable, Codable {
var id: Int
var question: String
var category: String
var answer: String
var options: [QuizOption]
}
struct QuizOption: Identifiable, Codable {
var id: Int
var optionId: String
var option: String
var isSelected: Bool = false
var isMatched: Bool = false
}
When you are decoding, unless you make your own decoding like this sample init from decoder Then all of non-optional the vars or lets in the struct need to be in the json. Your data doesn't have isSelected or isMatched in the options, so those need to be optional
struct QuizOption: Identifiable, Codable {
var id: Int
var optionId: String
var option: String
var isSelected: Bool?
var isMatched: Bool?
to fetch your data from a local JSON file, you could try
this approach, where you need to have a model (QuizModel) to
match the json data in your file. Also I used a class GameManagerVM: ObservableObject
to hold/publish your data as an example:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#StateObject var gameManager = GameManagerVM()
var body: some View {
List {
ForEach(gameManager.quizData) { quiz in
Text(quiz.question)
}
}
}
}
struct QuizModel: Identifiable, Codable {
let id, question, category, answer: String
let options: [QuizOption]
enum CodingKeys: String, CodingKey {
case id, question, category, answer, options
}
}
struct QuizOption: Codable {
let id, optionId, option: String
var isSelected: Bool = false
var isMatched: Bool = false
enum CodingKeys: String, CodingKey {
case id, optionId, option
}
}
class GameManagerVM: ObservableObject {
#Published var questions: [QuizModel] = []
#Published var quizData: [QuizModel] = []
init() {
quizData = Bundle.main.decode("quizquestions2022.json")
questions = quizData.shuffled()
}
}
Related
I have been trying to decode this Json data but I'm not able to do it completly :
This is my sample json data :
{
"id": 10644,
"name": "CP2500",
"numberOfConnectors": 2,
"connectors": [
{
"id": 59985,
"name": "CP2500 - 1",
"maxchspeed": 22.08,
"connector": 1,
"description": "AVAILABLE"
},
{
"id": 59986,
"name": "CP2500 - 2",
"maxchspeed": 22.08,
"connector": 2,
"description": "AVAILABLE"
}
]
}
this is my struct :
`
struct Root: Codable {
var id: Int
var name: String
var numberOfConnectors: Int
var connectors: [Connector]
}
struct Connector: Codable {
var id: Int
var name: String
var maxchspeed: Double
var connector: Int
var connectorDescription: String
enum CodingKeys: String, CodingKey {
case id, name, maxchspeed, connector
case connectorDescription = "description"
}
}
I want to parse the element within the [Connector] array but I'm just getting the elements of the Root level :
let jsonData = array.data(using: .utf8)!
let root = try JSONDecoder().decode(Root.self, from: jsonData)
print("\(root.id)")
Any idea how to do this ?
do {
let root = try JSONDecoder().decode(Root.self, from: jsonData)
print("root id : \(root.id)")
root.connectors.forEach {
print("name : \($0.name),"," connector id : \($0.id),","status : \($0.description)");
}
} catch {
print(error.localizedDescription)
}
I writing because I need to search a json-key passed in a function like a string. Do you have any suggestion on how I could implement it? Once I find the key I also need to edit the value. Here there is the code I wrote until now:
JSON:
{
"JSONRoot": {
"version": 1,
"Town": {
"hour": 0,
"latitude": "",
"longitude": 0,
"latitudine": 0
},
"MeasurePoints": {
"MeasurePoint": [
{
"code": "",
"codelocation": "",
}
]
},
"Wakeup": {
"startH": 6,
"startM": 0,
"maxAttempts": 3,
"maxRetry": 10
},
"Config": {
"port": 12345,
"A": {
"writable": true,
"value": 12
},
"B": {
"writable": true,
"value": 8
},
},
"Sales": {
"Stores": {
"Store": [
{
"description": "A description",
"type": "1",
"Floors": {
"basement": true,
"number": 2
},
"Doors": {
"type": "",
"number": 7
},
"Lights": {
"number": 20
}
},
{
"description": "A description",
"type": "4",
"Floors": {
"basement": none,
"number": 1
},
"Doors": {
"type": "",
"number": 4
},
"Lights": {
"number": 8
}
}
]
}
}
}
}
Structs with codable:
// MARK: - JSONConfig
struct JsonConfig: Codable {
let jsonRoot: JSONRoot?
enum CodingKeys: String, CodingKey {
case jsonRoot = "JSONRoot"
}
}
// MARK: - JSONRoot
struct JSONRoot: Codable {
let version: Int?
let measurePoints: MeasurePoints?
let wakeup: Wakeup?
let config: Config?
let sale: Sale?
enum CodingKeys: String, CodingKey {
case version
case measurePoints = "MeasurePoints"
case wakeup = "Wakeup"
case config = "Config"
case sale = "Sale"
}
}
// MARK: - Stores
struct Stores: Codable {
let stores: [Store]?
enum CodingKeys: String, CodingKey {
case stores = "Stores"
}
}
// MARK: - Store
struct Store: Codable {
let storeDescription: String?
let type: Int?
let floors: Floors?
let doors: Doors?
let lights: Lights?
enum CodingKeys: String, CodingKey {
case storeDescription = "description"
case type
case floors = "Floors"
case doors = "Doors"
case lights = "Lights"
}
}
// MARK: - Floors
struct Floors: Codable {
let basement: Bool?
let number: Int?
}
// MARK: - Doors
struct Doors: Codable {
let type: String?
let number: Int?
}
// MARK: - Lights
struct Lights: Codable {
let number: Int?
}
// MARK: - MeasurePoints
struct MeasurePoints: Codable {
let measurePoint: [MeasurePoint]?
enum CodingKeys: String, CodingKey {
case measurePoint = "MeasurePoint"
}
}
// MARK: - MeasurePoint
struct MeasurePoint: Codable {
let code, codeLocation: String?
}
// MARK: - Config
struct Config: Codable {
let port: Int?
let a, b: K?
enum CodingKeys: String, CodingKey {
case port
case a = "A"
case b = "B"
}
}
// MARK: - K
struct K: Codable {
let writable: Bool?
let value: Int?
}
// MARK: - Wakeup
struct Wakeup: Codable {
let startH, startM, maxAttempts, maxRetry: Int?
}
Function to search for a key:
func setKeyValue(jsonKey: String, value: String) {
let decoder = JSONDecoder()
let jsonData = Data(C.jsonString.utf8)
if let jsonResult = try? decoder.decode(JsonConfig.self, from: jsonData) {
// At this point I have the jsonKey = "JSONRoot.Wakeup.maxRetry" but how I can use it to search for
// the key in the jsonResult?
}
}
Obviously I need to create a new struct to edit the json but one step at a time.
Using JSONSerialisation is probably the most straightforward way here
var value: Any?
do {
if let jsonResult = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
let keys = "JSONRoot.Wakeup.maxRetry".split(separator: ".").map {String($0)}
var dict = jsonResult
for i in 0..<keys.count {
if let temp = dict[keys[i]] as? [String:Any] {
dict = temp
continue
}
value = dict[keys[i]]
}
}
} catch {
print(error)
}
Note that this doesn't support arrays but a solution for that is very dependent on how the search key syntax would handle an array
If my thinking is correct as you, you can try with this code.
override func viewDidLoad() {
super.viewDidLoad()
let jsonString = """
{
"JSONRoot": {
"version": 1,
"Town": {
"hour": 0,
"latitude": "",
"longitude": 0,
"latitudine": 0
},
"MeasurePoints": {
"MeasurePoint": [{
"code": "",
"codelocation": ""
}]
},
"Wakeup": {
"startH": 6,
"startM": 0,
"maxAttempts": 3,
"maxRetry": 10
},
"Config": {
"port": 12345,
"A": {
"writable": true,
"value": 12
}
},
"Sales": {
"Stores": {
"Store": [{
"description": "A description",
"type": "1",
"Floors": {
"basement": true,
"number": 2
},
"Doors": {
"type": "",
"number": 7
},
"Lights": {
"number": 20
}
},
{
"description": "A description",
"type": "4",
"Floors": {
"basement": "none",
"number": 1
},
"Doors": {
"type": "",
"number": 4
},
"Lights": {
"number": 8
}
}
]
}
}
}
}
"""
editJson(jsonString)
}
func editJson(_ jsonString: String) {
do{
let jsonData = Data(jsonString.utf8)
var jsonObject = try JSONSerialization.jsonObject(with: jsonData)
parseDict(&jsonObject)
print("jsonObject: \(String(describing: jsonObject))")
}catch let error {
print(error.localizedDescription)
}
}
func parseDict(_ jsonObject: inout Any) {
if let _ = jsonObject as? String {
return
} else if var dictionary = jsonObject as? Dictionary<String, Any> {
for (key, value) in dictionary {
var nextObject = value
parseDict(&nextObject)
if let value = getValueWith(key), let _ = dictionary.removeValue(forKey: key) {
dictionary[key] = value
} else {
dictionary[key] = nextObject
}
}
jsonObject = dictionary
}else if let array = jsonObject as? Array<Any> {
var updatedArray = array
for (index, value) in array.enumerated() {
var nextObject = value
parseDict(&nextObject)
updatedArray[index] = nextObject
}
jsonObject = updatedArray
}
}
func getValueWith(_ key: String) -> String? {
return [
"description" : "Amit (amitpstu1#gmail.com) ... so on"
][key]
}
You can refresh your memory or learn more here:
https://developer.apple.com/documentation/foundation/archives_and_serialization/using_json_with_custom_types
You would be looking at merge json from different depths section. Using encodable extension etc.
You could also look here: In Swift, can one use a string to access a struct property? If you want to roll your own search function, like a modified dfs or something.
I'm accessing the data from an API with XCode(10.2.1) and Swift(5.0) and ran into a problem I cannot seem to find the answer to. I am able to get data from all other parts of the API apart from one, which has been named as a number string "750", im not sure how to grab that data when I can't use a variable that jsonDecoder can read?
This is an example of what I know won't work but gives you an idea of what I'm trying to do.
class Images: Codable {
let "750": String
init("750": String){
self."750" = "750"
}
}
Here's a snippet from the API I'm trying to get the images from:
"id": "069f7f26",
"sku": "AP",
"title": "Pizza",
"description": "A really great pizza",
"list_price": "9.95",
"is_vatable": true,
"is_for_sale": false,
"age_restricted": false,
"box_limit": 2,
"always_on_menu": false,
"volume": null,
"zone": null,
"created_at": "2017-03-06T10:52:43+00:00",
"attributes": [
{
"id": "670f0e7c",
"title": "Allergen",
"unit": null,
"value": "Products manufactured in a nut environment"
},
{
"id": "c29e7",
"title": "Weight",
"unit": "g",
"value": "300"
}
],
"tags": [
],
"images": {
"750": {
"src": "https:\/\/some_website.co.uk\/cms\/product_image\some_image.jpg",
"url": "https:\/\/some_website.co.uk\/cms\/product_image\some_image.jpg",
"width": 750
}
}
},
I setup an example that better match your situation in order to give you an overview on how to parse and access your JSON information dynamically with a Dictionary data type:
import Foundation
let jsonData = """
{
"images": {
"750": {
"src": "https:\\/\\/some_website.co.uk/cms/product_image/some_image.jpg",
"url": "https:\\/\\/some_website.co.uk/cms/product_image/some_image.jpg",
"width": 750
}
}
}
"""
let json = jsonData.data(using: .utf8)!
public struct Results: Codable {
public var images: [String:Image] = [:]
enum CodingKeys: String, CodingKey {
case images = "images"
}
}
public struct Image: Codable {
public var src: String = ""
public var url: String = ""
public var width: Int = 0
enum CodingKeys: String, CodingKey {
case src = "src"
case url = "url"
case width = "width"
}
}
if let results = try? JSONDecoder().decode(Results.self, from: json) {
let imagesDict = results.images
for (key, value) in imagesDict {
print("Key: \(key)")
print("Value: \(value)")
}
}
If you try this snippet it will give you this output printed:
Key: 750
Value: Image(src: "https://some_website.co.uk/cms/product_image/some_image.jpg", url: "https://some_website.co.uk/cms/product_image/some_image.jpg", width: 750)
You can try out the snippet above online, if you copy paste it here and run it: http://online.swiftplayground.run/
### UPDATE (in response to comment)
In response to your comment, I found it easier to setup another example to show you how you can achieve that with your exact code sample that you shared in the comment itself.
I left everything as class and just added images in order to leave you an overview on how to achieve that.
In the end, I'd suggest to rename Products and Attributes into Product and Attribute. Also if there is no strong reason on why you choosed the model to be class, just change them to struct and as well if there is no strong reasons to keep most of the attributes of each model optional give them a default value as I did in the example above if you are always expecting some values/attributes to be there.
You can try and run this snippet as well in http://online.swiftplayground.run to try it out:
import Foundation
let jsonData = """
{
"data": [
{
"title": "titlex",
"description": "descx",
"list_price": "123,456",
"attributes": [
{
"title": "titlex",
"unit": "unitx",
"value": "valuex"
}
],
"images": {
"750": {
"src": "https:\\/\\/some_website.co.uk/cms/product_image/some_image.jpg",
"url": "https:\\/\\/some_website.co.uk/cms/product_image/some_image.jpg",
"width": 750
}
}
}
]
}
"""
let json = jsonData.data(using: .utf8)!
class AllProducts: Codable {
let data: [Products]
init(data: [Products]) {
self.data = data
}
}
class Products: Codable {
let title: String?
let description: String?
let list_price: String?
let attributes: [Attributes]?
let images: [String:Image]?
init(title: String, description: String, list_price: String, attributes: [Attributes], images: [String:Image]) {
self.title = title
self.description = description
self.list_price = list_price
self.attributes = attributes
self.images = images
}
}
class Attributes: Codable {
let title: String?
let unit: String?
let value: String?
init(title: String, unit: String, value: String) {
self.title = title
self.unit = unit
self.value = value
}
}
class Image: Codable {
let src: String?
let url: String?
let width: Int?
init(src: String, url: String, width: Int) {
self.src = src
self.url = url
self.width = width
}
}
// parsing/decoding
if let results = try? JSONDecoder().decode(AllProducts.self, from: json) {
if let imagesDict = results.data[0].images {
// there is an "images" for product at position 0 (the only one in my json example)
for (key, value) in imagesDict {
print("Key: \(key)")
print("Value src: \(value.src)")
print("Value url: \(value.url)")
print("Value width: \(value.width)")
}
}
}
Output
Key: 750
Value src: Optional("https://some_website.co.uk/cms/product_image/some_image.jpg")
Value url: Optional("https://some_website.co.uk/cms/product_image/some_image.jpg")
Value width: Optional(750)
JSON File:
{
"success": 1,
"msg": "User Phone Usage",
"data": [
{
"date": "2019-12-19",
"val": [
{
"ride_id": 44,
"date": "2019-12-19 09:32:19",
"total_km": null,
"startTime": "2019-12-19 09:32:19",
"endTime": "2019-12-19 09:37:08",
"rides": 0
},
{
"ride_id": 43,
"date": "2019-12-19 09:28:16",
"total_km": null,
"startTime": "2019-12-19 09:28:16",
"endTime": "2019-12-19 09:32:23",
"rides": 0
},
{
"ride_id": 42,
"date": "2019-12-19 09:28:12",
"total_km": null,
"startTime": "2019-12-19 09:28:12",
"endTime": "2019-12-19 09:29:13",
"rides": 0
}
]
},
{
"date": "2019-12-13",
"val": [
{
"ride_id": 2,
"date": "2019-12-13 08:14:34",
"total_km": 12.64,
"startTime": "2019-12-13 08:14:34",
"endTime": "2019-12-18 03:49:43",
"rides": 2
}
]
},
{
"date": "2019-12-12",
"val": [
{
"ride_id": 1,
"date": "2019-12-12 06:59:26",
"total_km": 101.36,
"startTime": "2019-12-12 06:59:26",
"endTime": "2019-12-18 03:07:00",
"rides": 0
}
]
}
]
}
So i have to set the tableview section title as (date) and show the event occurred on that particular date.
Below is the sample image i have to achieve
Section title is date and total km, rides are the row that i want to populate as per the date specified. API data is saved in model class and not in dictionary format so need some solution where i can sort the data and display it on the table view.
Model class:
class ShowDetails
{
var startRideDateTime:String?
var rideID:Int?
var endRideDateTime:String?
var totalKM:Double?
var rides:Int?
init(rideID: Int?,totalKM: Double?, startRideDateTime: String?, endRideDateTime: String?, rides: Int?) {
self.rideID = rideID
self.totalKM = totalKM
self.startRideDateTime = startRideDateTime
self.endRideDateTime = endRideDateTime
self.rides = rides
}
}
Viewcontroller code:
showdetailsArray is the saved array from API Response
func grouping()
{
let groupData = Dictionary(grouping: self.showDetailsArray){ (element) -> String in
return (element.startRideDateTime ?? "")
}
groupData.forEach { (key) in
print("VALUES")
print(groupData.values)
}
}
class ShowDetails {
var startRideDateTime:String?
var rideID:Int?
var endRideDateTime:String?
var totalKM:Double?
var rides:Int?
init(rideID: Int?,totalKM: Double?, startRideDateTime: String?, endRideDateTime: String?, rides: Int?) {
self.rideID = rideID
self.totalKM = totalKM
self.startRideDateTime = startRideDateTime
self.endRideDateTime = endRideDateTime
self.rides = rides
}
}
class myData {
let date: String // this value should not be optional as you are sorting on basis of this
let value: [ShowDetails]?
init(date: String, value: [ShowDetails]?) {
self.date = date
self.value = value
}
}
// set your data here
var data:[myData] = []
// this is the array sorted according to date
data.sort(by: { $0.date > $1.date })
Use Codable for data parsing and the later sorting it based on date.
The models that you can use,
struct Root: Decodable {
var data: [Model]
}
struct Model: Decodable {
let date: String
let val: [Ride]
}
struct Ride: Decodable {
let rideId: Int
let date: String
let totalKm: Double?
let startTime: String
let endTime: String
}
Now, parse the data like,
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
var response = try decoder.decode(Root.self, from: data)
response.data.sort { $0.date < $1.date }
print(response)
} catch {
print(error)
}
In the above code, use response as your tableView's dataSource.
So i found the solution to the Ans if anybody needs:
After saving the data into class model at the end you can add this code to Sort.
func groupingByDate()
{
let groupData = Dictionary(grouping: self.showDetailsArray){ (element) -> String in
return (element.startRideDate ?? "") //It fetches the element from model class array and set as key of dictionary and other elements as values
}
groupData.keys.forEach { (key) in
let values = groupData[key]
twoDArray.append(values ?? []) //twoDArray is a two dimensional Array
}
//reloadTableview if you want
}
I have an issue parsing following JSON, the problem is that it starts with an array. What should be the initial struct where I would identify the array? if there was something like "data" before the array I would create another struct and mention data: [Item]? there but this JSON just starts with array.
[
{
"userId": 1,
"id": 1,
"title": "TEST text"
},
{
"userId": 2,
"id": 2,
"title": "TEST text"
},
{
"userId": 3,
"id": 3,
"title": "TEST text"
}
]
struct Item: Codable {
var userId: Int?
var id: Int?
var title: String?
}
You have to add one more struct:
First Way
struct totalItem: Codable {
var total: [Item]?
}
struct Item: Codable {
var userId: Int?
var id: Int?
var title: String?
}
let myStruct = try JSONDecoder().decode(totalItem.self, from: data)
The second way to do that:
let myStruct = try JSONDecoder().decode([Item].self, from: data )