How to set an array's value from a JSON file? - ios

I am building a SwiftUI app for iOS to help me and my classmates keep up with assignments.
The way I am syncing homework across devices is by pushing some JSON with the assignments into GitHub Pages and then the app parses the JSON code into a sleek and simple Section within a Form.
I would like to have the following JSON...
[
{
"subject": "Maths",
"content": "Page 142, exercises 4, 5, 6.",
"dueDate": "15/01/2022"
},
{
"subject": "English",
"content": "Write an essay about your favorite subject. 2 pages at least.",
"dueDate": "18/01/2022"
},
{
"subject": "Chemistry",
"content": "Learn every element from the Periodic Table.",
"dueDate": "16/01/2022"
}
]
... turn into something that looks like this:
The easiest way would be to create about 5 Sections and, if there aren't 5 assignments, leave them empty. This solution didn't work because not having 5 assignments in the JSON file means the function that handles the file would abort because JSONDecoder would return nil when unwrapping the 4th assignment.
I've been struggling for quite a while to find a solution. I tried this:
struct Assignments: Decodable {
let subject: [String]
let content: [String]
let dueDate: [String]
}
struct AssignmentView: View {
#State private var currentNumber = 0
#State private var numberOfAssignments = 0
#State private var subject = [""]
#State private var dueDate = [""]
#State private var content = [""]
func downloadAssignments() {
let assignemtURL = URL(string: "https://example.com/assignments.json")!
var assignmentRequest = URLRequest(url: assignmentURL)
assignmentRequest.httpMethod = "GET"
assignmentRequest.attribution = .developer
NSURLConnection.sendAsynchronousRequest(assignmentRequest, queue: OperationQueue.main) {(response, data, error) in
if error != nil {
print("Could not get assignments. :/")
} else {
guard let data = data else { return }
let assignmentJSON = "\(data)"
let jsonData = assignmentJSON.data(using: .utf8)!
let decodedAssignments: Assignments = try! JSONDecoder().decode(Assignments.self, from: jsonData)
let numberOfAssignments: Int = decodedAssignments.count
// Here comes the problem. How do I create an array / modify an existing array with each individual subject taken in order?
}
}
}
var body: some View {
let _ = downloadAssignments()
VStack {
Form {
Section {
ForEach(0..<numberOfAssignments) {
Text("\(subject[$0]) - Due \(dueDate[$0])")
.font(.system(size: 20, weight: .semibold))
Text("\(content[$0])")
}
}
}
}
}
}
}
How can I get the value of each variable from the JSON file and combine it into an array (example: var subjects = ["Maths", "English", "Chemistry"])?
I've been looking for an answer for weeks with no solution. Some help on this would be highly appreciated!

There are quite a few things to point out in your code. I'm afraid I can't present you a full solution, but these pointers might help you go into the right direction:
The structure of your Assignments model doesn't match the JSON (and is also not really easy to work with in the list). Instead of having a single model which has an array for each property, there should one model object per assignment
struct Assignment: Decodable {
let subject: String
let content: String
let dueDate: String
}
and then you can decode/store an array of those:
let decodedAssignments: [Assignment] = try! JSONDecoder().decode([Assignment].self, from: jsonData)
Triggering the download as a side-effect of the body being evaluated is not a good idea, because this does not happen only once. In fact, whenever you modify any state of the view it would re-trigger the download, basically leaving you in an infinite download-loop. Instead, a better place to start the download would be when the view appears:
var body: some View {
VStack {
// ...
}
.onAppear(perform: downloadAssignments)
}
NSURLConnection is an extreeeemly old API that you don't want to use nowadays (it has been officially deprecated for 6 years now!). Have a look at URLSession instead.

Related

Query relation images from Parse that uses Back4App database

So I have an ParseObject setup as this - This is the main Object in Parse called MGLocation:
struct MGLocation: ParseObject {
var objectId: String?
var createdAt: Date?
var updatedAt: Date?
var originalData: Data?
var ACL: ParseACL?
var title: String?
var category: String?
init() {}
init(objectId: String?) {
self.objectId = objectId
}
}
Then I have my Codable setup using the following code:
struct Place: Codable, Identifiable {
let id: Int
var b4aId = ""
let title: String?
let category: String
init(
id: Int,
title: String?,
category: String,
) {
self.id = id
self.title = title
self.category = category
}
init(with p: MGLocation) {
self.id = atomicId.wrappingIncrementThenLoad(ordering: .relaxed)
self.b4aId = p.objectId ?? ""
self.title = p.title ?? "Undefined"
self.category = p.category ?? "Uncategorized"
}
}
Then I have the following function which pulls in the MGLocation:
func fetchPlaces() {
let query = MGLocation.query().limit(1000)
query.find { [weak self] result in
guard let self = self else {
return
}
switch result {
case .success(let items):
self.places = items.map({
Place(with: $0)
})
case .failure(let error):
}
}
}
Questions:
What is the best way that I can pull in a relational column? Inside MGLocation, I have a related column called images which can access another object.
This calls MGImage which has the following columns:
id, title, column, mime
Does anyone know how I can infuse and pull in the related column also? All help will be appreciated!
I recommend using Parse-Swift from here: https://github.com/netreconlab/Parse-Swift, as oppose to the parse-community one. I'm one of the original developers of Pase-Swift (you can see this on the contributors list) and I don't support the parse-community version anymore. The netreconlab version is way ahead of the other in terms of bug fixes and features and you will get better and faster support from the netreconlab since I've been the one to address the majority of issues in the past.
What is the best way that I can pull in a relational column? Inside MGLocation, I have a related column called images which can access another object.
I always recommend looking at the Playgrounds for similar examples. The Playgrounds has Roles and Relation examples. Assuming you are using version 4.16.2 on netreconlab. The only way you can do this with your current relation on your server is like so:
// You need to add your struct for MGImage on your client
struct MGImage: ParseObject { ... }
struct MGLocation: ParseObject {
var objectId: String?
var createdAt: Date?
var updatedAt: Date?
var originalData: Data?
var ACL: ParseACL?
var title: String?
var category: String?
var images: ParseRelation<Self>? // Add this relation
/* These aren't needed as they are already given for free.
init() {}
init(objectId: String?) {
self.objectId = objectId
}
*/
}
//: It's recommended to place custom initializers in an extension
//: to preserve the memberwise initializer.
extension MGLocation {
// The two inits in your code aren't needed because ParseObject gives them to you
}
//: Now we will see how to use the stored `ParseRelation on` property in MGLocation to create query
//: all of the relations to `scores`.
Task {
do {
//: Fetch the updated location since the previous relations were created on the server.
let location = try await MGLocation(objectId: "myLocationObjectId").fetch()
print("Updated current location with relation: \(location)")
let usableStoredRelation = try location.relation(location.images, key: "images")
let images = try await (usableStoredRelation.query() as Query<MGImage>).find()
print("Found related images from stored ParseRelation: \(images)")
} catch {
print("\(error.localizedDescription)")
}
}
If your images column was of type [MGImage] instead of Relation<MGImage> then you would be able to use var images: [MGImage]? in your MGLocation model and simply use include on your query. You can see more here: https://github.com/netreconlab/Parse-Swift/blob/325196929fed80ca3120956f2545cf2ed980616b/ParseSwift.playground/Pages/8%20-%20Pointers.xcplaygroundpage/Contents.swift#L168-L173

Updated #Published vars from Pickers won't pass their new values to Button function

I have an asynchronous function that performs an SQL query and updates the #Published variables. I can read these new values into the Picker as the Picker's default value but when I change the Picker value - and then pass it to another function that runs an SQL Update function (through a button), it just passes the original default #Published values, not the updated ones chosen by the user interaction with the Picker. The findSingleProduct function is called by passing a Barcode to it that comes from a preceeding view using user input.
class ProductsModel: ObservableObject{
#Published var Comp1Mat: Int = 0
#Published var code: String = ""
func findSingleProduct(code: String){
// Completes an SQL query on the Products database to find a product by it's barcode number.
// prepare json data
let insertProductURL = URL(string: "http://recyclingmadesimple.xyz/service.php")!
var urlRequest = URLRequest(url: insertProductURL)
urlRequest.httpMethod = "POST"
let postString = "code=\(code)"
urlRequest.httpBody = postString.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
guard let data = data, error == nil else {
print(error?.localizedDescription ?? "No data")
return
}
let responseJSON = try? JSONSerialization.jsonObject(with: data, options: [])
if let responseJSON = responseJSON as? [String: Any] {
DispatchQueue.main.async {
self.Comp1Mat = responseJSON["Comp1Mat"] as! Int
}
}
}
task.resume()
}
This is my view that displays Pickers about the object. I have removed a lot of the Pickers as they're all suffering from the same problem so the solution should be transferable.
struct ProductDetail: View {
#ObservedObject private var viewModel = ProductsModel()
init(ad: [String], material: [String], code: String){
self.viewModel.findSingleProduct(code: code)
}
var body: some View {
//This is the main stack that contains everything
VStack(alignment: .leading){
Picker(selection: $viewModel.Comp1Mat, label: Text("Material")){
ForEach(0 ..< material.count){
Text(self.material[$0])
}
}
Button(action: {viewModel.updateProduct(brands: "\(viewModel.Comp1Mat)", **Omitted variables for simplicity, they also run on similar Pickers**); showingAlert = true}, label: {
Text("Update Product")
}).alert(isPresented: $showingAlert) {
Alert(title: Text("Product Updated"), message: Text("Thanks for helping the community!"), dismissButton: .default(Text("Dismiss")))
}
}
I have confirmed the PHP and SQL of the update function work by hardcoding some values into the Button like: Button(action: {viewModel.updateProduct(Comp1Mat: "(5)"}... and they are indeed correctly passed through and the database is updated.
I thought this similar question might be of use but I believe I am already using the $ correctly, which was the suggested solution for this person so I am unsure why it doesn't work for me. Change a #Published var via Picker
Thanks.
You just need to add a tag.
Text(self.material[$0]).tag($0)

SwiftUI: What is #AppStorage property wrapper

I used to save important App data like login credentials into UserDefaults using the following statement:
UserDefaults.standard.set("sample#email.com", forKey: "emailAddress")
Now, I have come to know SwiftUI has introduced new property wrapper called:
#AppStorage
Could anyone please explain how the new feature works?
AppStorage
#AppStorage is a convenient way to save and read variables from UserDefaults and use them in the same way as #State properties. It can be seen as a #State property which is automatically saved to (and read from) UserDefaults.
You can think of the following:
#AppStorage("emailAddress") var emailAddress: String = "sample#email.com"
as an equivalent of this (which is not allowed in SwiftUI and will not compile):
#State var emailAddress: String = "sample#email.com" {
get {
UserDefaults.standard.string(forKey: "emailAddress")
}
set {
UserDefaults.standard.set(newValue, forKey: "emailAddress")
}
}
Note that #AppStorage behaves like a #State: a change to its value will invalidate and redraw a View.
By default #AppStorage will use UserDefaults.standard. However, you can specify your own UserDefaults store:
#AppStorage("emailAddress", store: UserDefaults(...)) ...
Unsupported types (e.g., Array):
As mentioned in iOSDevil's answer, AppStorage is currently of limited use:
types you can use in #AppStorage are (currently) limited to: Bool, Int, Double, String, URL, Data
If you want to use any other type (like Array), you can add conformance to RawRepresentable:
extension Array: RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Element].self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
}
Demo:
struct ContentView: View {
#AppStorage("itemsInt") var itemsInt = [1, 2, 3]
#AppStorage("itemsBool") var itemsBool = [true, false, true]
var body: some View {
VStack {
Text("itemsInt: \(String(describing: itemsInt))")
Text("itemsBool: \(String(describing: itemsBool))")
Button("Add item") {
itemsInt.append(Int.random(in: 1...10))
itemsBool.append(Int.random(in: 1...10).isMultiple(of: 2))
}
}
}
}
Useful links:
What is the #AppStorage property wrapper?
AppStorage Property Wrapper SwiftUI
Disclaimer: iOS 14 Beta 2
In addition to the other useful answers, the types you can use in #AppStorage are (currently) limited to: Bool, Int, Double, String, URL, Data
Attempting to use other types (such as Array) results in the error: "No exact matches in call to initializer"
Re-implementation for iOS 13 and without SwiftUI
In additon to pawello2222 answer, here. is the reimplementation of the AppStorage that I named it as UserDefaultStorage:
#propertyWrapper
struct UserDefaultStorage<T: Codable> {
private let key: String
private let defaultValue: T
private let userDefaults: UserDefaults
init(key: String, default: T, store: UserDefaults = .standard) {
self.key = key
self.defaultValue = `default`
self.userDefaults = store
}
var wrappedValue: T {
get {
guard let data = userDefaults.data(forKey: key) else {
return defaultValue
}
let value = try? JSONDecoder().decode(T.self, from: data)
return value ?? defaultValue
}
set {
let data = try? JSONEncoder().encode(newValue)
userDefaults.set(data, forKey: key)
}
}
}
This wrapper can store/restore any kind of codable into/from the user defaults. Also, it works in iOS 13 and it doesn't need to import SwiftUI.
Usage
#UserDefaultStorage(key: "myCustomKey", default: 0)
var myValue: Int
Note that it can't be used directly as a State
This is a persistent storage provided by SwiftUI. This code will persist the email across app launches.
struct AppStorageView: View {
#AppStorage("emailAddress") var emailAddress = "initial#hey.com"
var body: some View {
TextField("Email Address", text: $emailAddress)
}
}
With pure SwiftUI code, we can now persist such data without using UserDefaults at all.
But if you do want to access the underlying data, it is no secret that the wrapper is using UserDefaults. For example, you can still update using UserDefaults.standard.set(...), and the benefit is that AppStorage observes the store, and the SwiftUI view will update automatically.
#AppStorage property wrapper is a source of truth that allows you read data from and write it to UserDefaults. It has a value semantics. You can use #AppStorage to store any basic data type (like preferences' values or any default values) for as long as the app is installed.
For the best performance, when working with UserDefaults, you should keep the size of the stored data between 512 KB and 1 MB.
import SwiftUI
struct ContentView: View {
#AppStorage("t_101") var t_101: String = "Terminator T-101"
#AppStorage("t_1000", store: .standard) var t_1000 = "Liquid Metal"
var body: some View {
VStack {
Text("Hey, \(t_101)!").foregroundColor(.indigo)
Text("Hey, \(t_1000)!").foregroundColor(.teal)
Divider()
Button("Real Names") {
t_101 = "Arnie"
t_1000 = "Robbie"
}
}
}
}
We can test this via simple approach:
struct Content: View {
private enum Keys {
static let numberOne = "myKey"
}
#AppStorage(Keys.numberOne) var keyValue2: String = "no value"
var body: some View {
VStack {
Button {
keyValue2 = "Hello"
print(
UserDefaults.standard.value(forKey: Keys.numberOne) as! String
)
} label: {
Text("Update")
}
Text(keyValue2)
}
.padding()
.frame(width: 100)
}
}
I have experimented a behavior that I want to share with you. I had a View (called MainView) that used an #AppStorage Bool to display or not a message to users. In this View, I also had many views displaying data loaded from a JSON (cities).
I've developed a 'Add to favorite' feature that uses UserDefault to store a list of cities ID added by user in his/her favorites. The strange behavior was this one: As soon as user added or removed a city as favorite, all MainView's body (and all its child views) was updated. That means all content was reloaded for no reason.
After hours of investigations, specially refactoring all my ObservableObjects (models), my #Published variables and so on, I've find out what was creating this mass update: The #AppStorage in my MainView! As soon as you update any key in 'UserDefaults.standard' (in my case by storing my new favorite), all views's body using an #AppStorage are updated.
So #AppStorage is awesome and easy to use. But be aware of that behavior. You don't expect that a view is updated when you set a key that is not even used in that view!

How to fill a single Model Object with multiple chained URL requests Request Dependency?

I have a data model object that I have to fill with multiple network requests.
My model looks like this
struct CO2Data: Identifiable, Codable {
let id = UUID()
let totalCO2: Double
let dailyAverage: Int
let calendar = Calendar.current
let dateString: String
let emissionStats: EmissionStats
let driveLog: String
// It is an ID to get the trip details
let trip : Trip
// This is what needs to be filled from the next URL request using driveLog
enum CodingKeys: String, CodingKey {
case id
case totalCO2 = "totCO2"
case dateString = "stDt"
case dailyAverage = "avg"
case emissionStats = "emisStats"
case drLogRefId = "drLogRefId"
//Not having case trip here breaks the conformance to codable
// and results in error
}
}
struct Trip: Codable {
let startTime: String
let endTime: String
let startLat: String
let startLong: String
let endLat: String
let endLong: String
}
This is the flow of the network request and responses
request to get the CO2Data and decode it using JSONDecoder()
use the driveLog Property in the fetched CO2Data to make another network request to get the Trip object.
The result of the second network request should be added to the CO2Data object in the trip property.
How should I go about this. Is it even possible to store the Trip in CO2 data?
I am using the Combine & URLSession.shared.dataTaskPublisher(for: URLRequest) for this, but I can totally use plain old simple data task.
Are there known ways to handle request dependencies like this?
I am not using any third party libraries and wish to stick with URLSession.
JSON Objects look like this
// for CO2Data
[
{
"stDt": "2019-11-17T16:00:00.000+0000",
"totCO2": 50,
"avg": 0,
"emisStats": {
"wea": 10,
"aggDr": 10,
"tfc": 10,
"tirePress": 0,
"ac": 10,
"seatHeater": 10
},
"drLogRefId": "5dc20204199f752c7726a8f0"
}
]
//for Trip
[
{
"id": "5dc20204199f752c7726a8f0",
"startTime": "...",
"startLat: "...",
"startLong: "...",
"endLat: "...",
"endLong": "...",
}
]
Here's how you can achieve your goal. You need to make Trip optional during the first network call as you don't have data to set there. Make sure handle this properly during json decoding.
Then use flatMap to make the second network call.
Use zip to wait for both requests to complete and pass result data to extension CO2Data.init.
Full Code where fetchCO2TripData will return CO2Data with Trip :
struct CO2Data: Identifiable, Codable {
let id = UUID()
/// TODO: add other properties
let driveLog: String
let trip : Trip?
}
struct Trip: Codable {
let startTime: String
let endTime: String
let startLat: String
let startLong: String
let endLat: String
let endLong: String
}
func fetchCO2Data() -> AnyPublisher<CO2Data, Error> {
/// TODO: Replace with actual network request
Empty().eraseToAnyPublisher()
}
func fetchTripData(by driveLog: String) -> AnyPublisher<Trip, Error> {
/// TODO: Replace with actual network request
Empty().eraseToAnyPublisher()
}
func fetchCO2TripData() -> AnyPublisher<CO2Data, Error> {
let co2Publisher = fetchCO2Data().share()
let tripPublisher = co2Publisher.flatMap { fetchTripData(by: $0.driveLog) }
return co2Publisher
.zip(tripPublisher)
.map { CO2Data(co2data: $0, trip: $1) }
.eraseToAnyPublisher()
}
extension CO2Data {
init(co2data: CO2Data, trip: Trip) {
self.driveLog = co2data.driveLog
self.trip = trip
}
}
P.S. I would rather recommend creating high-level model that will be composed of both network calls, to eliminate optional trip:
struct Model {
let co2Data: CO2Data
let trip: Trip
}
Depending on the complexity of the surrounding data look either into
Operations: You can specify other operations this operation is dependent on.
DispatchGroup: With DispatchGroup.enter(), DispatchGroup.leave() and finally DispatchGroup.notify() you can also model dependencies of dispatched tasks you perform.
Good luck.

what is the best way to reuse API manager class and store data so that it is reusable

I have a PageViewController which creates instances of DataViewController, and this dataViewController needs to do some API called (from manager Class) then use that data to populate its UI. I.E. a page is added, the dataViewController gets its datasource and uses that to tell the API what to get.
The problem is, as I can add multiple pages, and I need for example data to form a graph for each page, having to do the API call every time the view controller loads I feel is unness esary, and that there must be a better way to store the data for the life of the session once it have been retrieved. as each instance of dataViewController uses the same API Managers, I refresh them clearing any data they have, and start the API call again for the page that loaded, this can sometimes get data mixed up, displaying a mixture of information from two pages or displaying the wrong data on the wrong page. Here is an example of one of my API managers which uses the dataObject of the dataViewController to get chart data:
import Foundation
struct Root: Codable {
let prices: [Price]
}
struct Price: Codable {
let date: Date
let price: Double
}
class CGCharts {
var priceData = [Price]()
var graphPrices: [Double] = []
var graphTimes: [String] = []
static let shared = CGCharts()
var defaultCurrency = UserDefaults.standard.string(forKey: "DefaultCurrency")!
var coin = ""
var currency = ""
var days = ""
enum GraphStatus {
case day, week, month
}
var graphSetup = GraphStatus.day
func getData(arr: Bool, completion: #escaping (Bool) -> ()) {
switch graphSetup {
case .day:
days = "1"
case .week:
days = "14"
case .month:
days = "30"
}
let urlJSON = "https://api.coingecko.com/api/v3/coins/\(coin)/market_chart?vs_currency=\(defaultCurrency)&days=\(days)"
guard let url = URL(string: urlJSON) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let prices = try JSONDecoder().decode(Root.self, from: data).prices
print(prices.first!.date.description(with:.current)) // "Saturday, September 1, 2018 at 6:25:38 PM Brasilia Standard Time\n"
print(prices[0].price)
self.priceData = prices
for element in prices {
self.graphPrices.append(element.price)
}
for element in prices {
self.graphTimes.append(element.date.description(with: nil))
}
completion(arr)
} catch {
print(error)
}
}.resume()
}
func refresh(arr: Bool, completion: #escaping (Bool) -> ()) {
defaultCurrency = UserDefaults.standard.string(forKey: "DefaultCurrency")!
graphPrices = []
graphTimes = []
completion(arr)
}
}
Here as you can see I am getting the data, using codable to parse and in this case the best way I could find to seperate the data was to use the two arrays, I basically want to have thoes 2 arrays stored in memory for that specific dataObject, so that when a page with the same dataObject loads in the pageView it will first check if that data already exists, and use that, instead of getting new information everytime and me having to clear the class. Hope I am making sense, Thank you.
After researching more about NSCache, I learned you can save data to the disk or cache and access it again easily. I found this excellent storage class made by Saoud M. Rizwan here
which makes it super easy to store structs:
var messages = [Message]()
for i in 1...4 {
let newMessage = Message(title: "Message \(i)", body: "...")
messages.append(newMessage)
}
Storage.store(messages, to: .documents, as: "messages.json")
and retrieve them:
let messagesFromDisk = Storage.retrieve("messages.json", from: .documents, as: [Message].self)
Perfect for storing data from an API so that you don't have to make so many API calls, which is especially useful if your requests are limited. In my case, I use the method for checking if storage exists, and if it does I use that to load my data, and if not then I do an api call, store the data using this class, and then run the check again to see that the data is there and use that, It may not be the most efficient but it works.
my example:
var prices: [Double] = []
var days: [String] = []
var priceData = [Price]()
var graphDataExists = false
func checkGraphStorage() {
if Storage.fileExists("\(dataObject)GraphPrices", in:
Storage.Directory.caches) {
print("UsingSavedData")
self.priceData = Storage.retrieve("\(dataObject)GraphPrices", from:
Storage.Directory.caches, as: [Price].self)
if self.prices.isEmpty {
for element in self.priceData {
self.prices.append(element.price)
}
for element in self.priceData {
self.days.append(element.date.description(with: nil))
graphDataExists = true
}
} else {
}
DispatchQueue.main.async {
self.chartView.animate(xAxisDuration: 4.0)
self.lineChartUpdate(dataPoints: self.days, values: self.prices)
}
} else {
self.prepareGraph(arr: true) { (success) in
}
}
}

Resources