I created a local json. I change the value of the name key in the json and when I close and open the application, it says "Test" again. How can I save the change I made on the Json file?
Why can't I save the string value? I shared all the codes with you. If you want I can share the project.
Local JSON File
{
"person": {
"name": "Test"
}
}
Model
struct PersonContainer: Codable {
var person: Person?
}
struct Person: Codable {
var name: String?
}
JSON Provider
class JSONProvider: ObservableObject {
#Published var personContainer: PersonContainer = PersonContainer()
var fm = FileManager.default
var fresult: Bool = false
#Published var subUrl: URL? = URL(string: "")
var mainUrl: URL? = Bundle.main.url(forResource: "test", withExtension: "json")
func getData() {
do {
let documentDirectory = try fm.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
subUrl = documentDirectory.appendingPathComponent("test.json")
loadFile(mainPath: mainUrl!, subPath: subUrl!)
} catch {
print(error)
}
}
func loadFile(mainPath: URL, subPath: URL){
if fm.fileExists(atPath: subPath.path){
decodeData(pathName: subPath)
if ((personContainer.person) != nil) {
decodeData(pathName: mainPath)
}
}else{
decodeData(pathName: mainPath)
}
}
func decodeData(pathName: URL){
do{
let jsonData = try Data(contentsOf: pathName)
let decoder = JSONDecoder()
let personContainer = try decoder.decode(PersonContainer.self, from: jsonData)
self.personContainer = personContainer
} catch {}
}
func writeToFile(location: URL) {
do{
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let JsonData = try encoder.encode(personContainer)
try JsonData.write(to: location)
} catch {
}
}
}
ContentView
struct ContentView: View {
#State var text: String = ""
#ObservedObject var jsonProvider: JSONProvider = JSONProvider()
var body: some View {
VStack {
TextField("Placeholder", text: $text)
.padding()
.background(Color(UIColor.secondarySystemBackground))
.cornerRadius(15)
.padding(.horizontal)
Text("Hello, world! \(jsonProvider.personContainer.person?.name ?? "")")
.padding()
Button(action: {
jsonProvider.personContainer.person?.name = text
jsonProvider.writeToFile(location: jsonProvider.subUrl!)
}) {
Text("Button")
}
}
.onAppear {
jsonProvider.getData()
}
}
}
Looks like you were on the right track, but there were a few things missing.
Since the original main bundle's test.json should only be loaded if the file in the documents directory doesn't exist, a lot of the logic can be simplified. For example, you can remove the #Published subUrl, since it never gets changed and isn't observed by the View.
Make sure that you call the writeToFile when the button is pressed.
Also, it's always a good idea to do something (like printing the error) inside the catch blocks in case something has gone wrong.
class JSONProvider: ObservableObject {
#Published var personContainer: PersonContainer = PersonContainer()
private var fm = FileManager.default
private let mainUrl: URL = Bundle.main.url(forResource: "test", withExtension: "json")!
func getData() {
if fm.fileExists(atPath: documentDirectoryJSONURL().path) {
decodeData(fromURL: documentDirectoryJSONURL())
} else {
decodeData(fromURL: mainUrl)
}
}
func documentDirectoryJSONURL() -> URL {
do {
let documentDirectory = try fm.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
return documentDirectory.appendingPathComponent("test.json")
} catch {
fatalError("Couldn't create URL")
}
}
func decodeData(fromURL url: URL){
do{
let jsonData = try Data(contentsOf: url)
let decoder = JSONDecoder()
let personContainer = try decoder.decode(PersonContainer.self, from: jsonData)
self.personContainer = personContainer
} catch {
print(error)
assertionFailure("Error decoding JSON")
}
}
func writeToFile() {
do{
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let jsonData = try encoder.encode(personContainer)
try jsonData.write(to: documentDirectoryJSONURL())
} catch {
print(error)
}
}
}
struct ContentView: View {
#State var text: String = ""
#ObservedObject var jsonProvider: JSONProvider = JSONProvider()
var body: some View {
VStack {
TextField("Placeholder", text: $text)
.padding()
.background(Color(UIColor.secondarySystemBackground))
.cornerRadius(15)
.padding(.horizontal)
Text("Hello, world! \(jsonProvider.personContainer.person?.name ?? "")")
.padding()
Button(action: {
jsonProvider.personContainer.person?.name = text
jsonProvider.writeToFile()
}) {
Text("Write")
}
}
.onAppear {
jsonProvider.getData()
}
}
}
Related
I'm trying implementing a data manager singleton class reading/write from a JSON file but I obtain this error:
'self' used in method call 'LoadData' before all stored properties are initialized
this is the code:
import SwiftUI
import Combine
class DataManager: ObservableObject {
static let shared = DataManager()
var TTDItemMainList: TTDItemList = TTDItemList(itemList: [TTDItem(id: UUID(), itemDesc: "", itemCreaDate: Date(), itemUpdDate: Date(), itemTags: [], linkedItemsUID: [])])
var urlFile: URL
init() { LoadData() }
func LoadData() {
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
let urlFile = getDocumentsDirectory().appendingPathComponent("/SMT.json")
do {
let data = try Data(contentsOf: urlFile)
let decoder = JSONDecoder()
TTDItemMainList = try decoder.decode(TTDItemList.self, from: data)
} catch {
debugPrint(error.localizedDescription)
}
}
func saveData() {
let encoder = JSONEncoder()
do {
let data = try encoder.encode(TTDItemMainList)
try data.write(to: urlFile)
} catch {
debugPrint(error.localizedDescription)
}
}
}
I do not understand how restructure the code to avois this issue.
You don't set urlFile during your init. In fact, you never set it (later, your let urlFile = is locally scoped).
The easiest solution looks like turning urlFile into a computed property:
class DataManager: ObservableObject {
static let shared = DataManager()
var TTDItemMainList: TTDItemList = TTDItemList(itemList: [TTDItem(id: UUID(), itemDesc: "", itemCreaDate: Date(), itemUpdDate: Date(), itemTags: [], linkedItemsUID: [])])
var urlFile: URL { //<-- Here
getDocumentsDirectory().appendingPathComponent("/SMT.json")
}
init() { loadData() }
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
func loadData() {
do {
let data = try Data(contentsOf: urlFile)
let decoder = JSONDecoder()
TTDItemMainList = try decoder.decode(TTDItemList.self, from: data)
} catch {
debugPrint(error.localizedDescription)
}
}
func saveData() {
let encoder = JSONEncoder()
do {
let data = try encoder.encode(TTDItemMainList)
try data.write(to: urlFile)
} catch {
debugPrint(error.localizedDescription)
}
}
}
I'm new to SwiftUI and have worked through the server requests and JSON. I now need to programmatically transition to a new view which is where I get stuck with a "Cannot find 'json' in scope" error on the NavigationLink in ContentView.swift. I've watched videos and read articles but nothing quite matches, and everything I try just seems to make things worse.
JSON response from server
{"status":{"errno":0,"errstr":""},
"data":[
{"home_id":1,"name":"Dave's House","timezone":"Australia\/Brisbane"},
{"home_id":2,"name":"Mick's House","timezone":"Australia\/Perth"},
{"home_id":3,"name":"Jim's House","timezone":"Australia\/Melbourne"}
]}
JSON Struct file
import Foundation
struct JSONStructure: Codable {
struct Status: Codable {
let errno: Int
let errstr: String
}
struct Home: Codable, Identifiable {
var id = UUID()
let home_id: Int
let name: String
let timezone: String
}
let status: Status
let data: [Home]
}
ContentView file
import SwiftUI
struct ContentView: View {
#State private var PushViewAfterAction = false
var body: some View {
NavigationLink(destination: ListView(json: json.data), isActive: $PushViewAfterAction) {
EmptyView()
}.hidden()
Button(action: {
Task {
await performAnAction()
}
}, label: {
Text("TEST")
.padding()
.frame(maxWidth: .infinity)
.background(Color.blue.cornerRadius(10))
.foregroundColor(.white)
.font(.headline)
})
}
func performAnAction() {
PushViewAfterAction = true
return
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ListView file
import SwiftUI
struct ListView: View {
#State var json: JSONStructure
var body: some View {
VStack {
List (self.json.data) { (home) in
HStack {
Text(home.name).bold()
Text(home.timezone)
}
}
}.onAppear(perform: {
guard let url: URL = URL(string: "https://... ***removed*** ") else {
print("invalid URL")
return
}
var urlRequest: URLRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
URLSession.shared.dataTask(with: urlRequest, completionHandler: { (data, response, error) in
// check if response is okay
guard let data = data, error == nil else { // check for fundamental networking error
print((error?.localizedDescription)!)
return
}
let httpResponse = (response as? HTTPURLResponse)!
if httpResponse.statusCode != 200 { // check for http errors
print("httpResponse Error: \(httpResponse.statusCode)")
return
}
// convert JSON response
do {
self.json = try JSONDecoder().decode(JSONStructure.self, from: data)
} catch {
print(error.localizedDescription)
print(String(data: data, encoding: String.Encoding.utf8)!)
}
print(json)
if (json.status.errno != 0) {
print(json.status.errstr)
}
print("1. \(json.data[0].name)), \(json.data[0].timezone)")
print("2. \(json.data[1].name)), \(json.data[1].timezone)")
}).resume()
})
}
}
struct ListView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
I've tried to keep the code to a minimum for clarity.
It's because there is no "json" in ContentView, you need to pass json object to ListView, but since you load json in ListView, then you need to initialize json in ListView like:
struct ListView: View {
#State var json: JSONStructure = JSONStructure(status: JSONStructure.Status(errno: 0, errstr: ""), data: [JSONStructure.Home(home_id: 0, name: "", timezone: "")])
var body: some View {
and remove it form NavigationLink in ContentView like:
NavigationLink(destination: ListView(), isActive: $PushViewAfterAction) {
or you could build your JSONStructure to accept optional like:
import Foundation
struct JSONStructure: Codable {
struct Status: Codable {
let errno: Int?
let errstr: String?
init() {
errno = nil
errstr = nil
}
}
struct Home: Codable, Identifiable {
var id = UUID()
let home_id: Int?
let name: String?
let timezone: String?
init() {
home_id = nil
name = nil
timezone = nil
}
}
let status: Status?
let data: [Home]
init() {
status = nil
data = []
}
}
but then you need to check for optionals or provide default value like:
struct ListView: View {
#State var json: JSONStructure = JSONStructure()
var body: some View {
VStack {
List (self.json.data) { (home) in
HStack {
Text(home.name ?? "Could not get name").bold()
Text(home.timezone ?? "Could not get timeZone")
}
}
}.onAppear(perform: {
guard let url: URL = URL(string: "https://... ***removed*** ") else {
print("invalid URL")
return
}
var urlRequest: URLRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
URLSession.shared.dataTask(with: urlRequest, completionHandler: { (data, response, error) in
// check if response is okay
guard let data = data, error == nil else { // check for fundamental networking error
print((error?.localizedDescription)!)
return
}
let httpResponse = (response as? HTTPURLResponse)!
if httpResponse.statusCode != 200 { // check for http errors
print("httpResponse Error: \(httpResponse.statusCode)")
return
}
// convert JSON response
do {
self.json = try JSONDecoder().decode(JSONStructure.self, from: data)
} catch {
print(error.localizedDescription)
print(String(data: data, encoding: String.Encoding.utf8)!)
}
print(json)
if (json.status?.errno != 0) {
print(json.status?.errstr)
}
print("1. \(json.data[0].name)), \(json.data[0].timezone)")
print("2. \(json.data[1].name)), \(json.data[1].timezone)")
}).resume()
})
}
}
I'm very new to swiftUI and have been working through the landscapes app tutorial.
I have been trying to switch the data source from a bundled JSON file to a remote JSON source but have so far been lost on how to integrate what I've learnt about the URLSession with the tutorials load code.
Apple's code:
final class ModelData: ObservableObject {
#Published var landmarks: [Landmark] = load("landmarkData.json")
// #Published var landmarks: [Landmark] = apiCall.getLocations(locations)
}
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
What I have to load from the remote source:
struct Location: Codable, Identifiable {
let id = UUID()
let country: String
let name: String
}
class apiCall {
func getLocations(completion:#escaping ([Location]) -> ()) {
guard let url = URL(string: "https://overseer.cyou/heritage/heritageData.json") else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
let locations = try! JSONDecoder().decode([Location].self, from: data!)
print(locations)
DispatchQueue.main.async {
completion(locations)
}
}
.resume()
}
}
Can anyone show me how I go about doing this, ideally from a complete beginners point of view?
// framework support
import SwiftUI
import Combine
// List view setup
struct LocationsView: View {
#ObservedObject var viewModel = LocationModel()
var body: some View {
List(viewModel.locations) { location in
HStack {
VStack(alignment: .leading) {
Text(location.name)
.font(.headline)
Text(location.country)
.font(.subheadline)
}
}
}
}
}
// Location model
struct Location: Codable, Identifiable {
var id = UUID()
let country: String
let name: String
let locationId: Int = 0
enum CodingKeys: String, CodingKey {
case locationId = "id"
case country
case name
}
}
// Location view model class
class LocationModel: ObservableObject {
#Published var locations: [Location] = []
var cancellationToken: AnyCancellable?
init() {
getLocations()
}
}
extension LocationModel {
func getLocations() {
cancellationToken = self.request("https://overseer.cyou/heritage/heritageData.json")?
.mapError({ (error) -> Error in
print(error)
return error
})
.sink(receiveCompletion: { _ in },
receiveValue: {
self.locations = $0
})
}
// API request
private func request(_ path: String) -> AnyPublisher<[Location], Error>? {
guard let url = URL(string: path)
else { return nil }
let request = URLRequest(url: url)
return apiCall.run(request)
.map(\.value)
.eraseToAnyPublisher()
}
}
// API setup
struct apiCall {
struct Response<T> {
let value: T
let response: URLResponse
}
static func run<T: Decodable>(_ request: URLRequest) -> AnyPublisher<Response<T>, Error> {
return URLSession.shared
.dataTaskPublisher(for: request)
.tryMap { result -> Response<T> in
let value = try JSONDecoder().decode(T.self, from: result.data)
return Response(value: value, response: result.response)
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
The code takes json data from list.json and I created a swift view BookView as another file.
import SwiftUI
struct product: Codable, Hashable {
var name: String
var author: String
var page: String
}
struct ContentView: View {
func jsonTwo() -> [product]{
let url = Bundle.main.url(forResource: "list", withExtension: "json")!
let data = try! Data(contentsOf: url)
let decoder = JSONDecoder()
let products = try? decoder.decode([product].self, from: data)
return products!
}
#State var number: Int = 5
var body: some View {
NavigationView {
VStack {
Form {
ForEach(jsonTwo(), id: \.self) {item in
BookView(name: "item.name",author: "item.author",page: "item.page")
}
}
.navigationBarTitle("Books")
Button(action: {
// Button tapped
}, label: {
Image(systemName: "plus.rectangle")
.font(Font.system(.largeTitle).bold())
.foregroundColor(.primary)
})
.padding(.top)
}
}
}
}
This code give me the error "Cannot preview in this file -- Failed to update preview" but the following code works fine (difference in the ForEach statement) .Note I put the parameters for BookView in quotes to test.
import SwiftUI
struct product: Codable, Hashable {
var name: String
var author: String
var page: String
}
struct ContentView: View {
func jsonTwo() -> [product]{
let url = Bundle.main.url(forResource: "list", withExtension: "json")!
let data = try! Data(contentsOf: url)
let decoder = JSONDecoder()
let products = try? decoder.decode([product].self, from: data)
return products!
}
#State var number: Int = 5
var body: some View {
NavigationView {
VStack {
Form {
ForEach(0..<number) {item in
BookView(name: "item.name",author: "item.author",page: "item.page")
}
}
.navigationBarTitle("Books")
Button(action: {
// Button tapped
}, label: {
Image(systemName: "plus.rectangle")
.font(Font.system(.largeTitle).bold())
.foregroundColor(.primary)
})
.padding(.top)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewDevice("iPhone 11")
}
}
I'm having some trouble fetching data from the PiHole API;
This is the JSON format (from the url http://pi.hole/admin/api.php?summary):
{
"domains_being_blocked": "1,089,374",
"dns_queries_today": "34,769",
"ads_blocked_today": "11,258",
"ads_percentage_today": "32.4",
"unique_domains": "9,407",
"queries_forwarded": "17,972",
"queries_cached": "5,539",
"clients_ever_seen": "35",
"unique_clients": "23",
"dns_queries_all_types": "34,769",
"reply_NODATA": "1,252",
"reply_NXDOMAIN": "625",
"reply_CNAME": "10,907",
"reply_IP": "21,004",
"privacy_level": "0",
"status": "enabled",
"gravity_last_updated": {
"file_exists": true,
"absolute": 1588474361,
"relative": {
"days": "0",
"hours": "14",
"minutes": "18"
}
}
}
This is my code:
ContentView.swift
import SwiftUI
struct NetworkController {
static func fetchData(completion: #escaping (([PiHole.Stat]) -> Void)) {
if let url = URL(string: "http://pi.hole/admin/api.php?summary") {
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data {
let stat = try? JSONDecoder().decode(PiHole.self, from: data)
completion(stat?.stats ?? [])
}
}.resume()
}
}
}
class ContentViewModel: ObservableObject {
#Published var messages: [PiHole.Stat] = []
func fetchData() {
NetworkController.fetchData { messages in
DispatchQueue.main.async {
self.messages = messages
}
}
}
}
struct ContentView: View {
#ObservedObject var viewModel = ContentViewModel()
var body: some View {
List {
ForEach(viewModel.messages, id: \.self) { stat in
Text(stat.domains_being_blocked)
}
}.onAppear{
self.viewModel.fetchData()
}
}
}
Data.swift
struct PiHole: Decodable {
var stats: [Stat]
struct Stat: Decodable, Hashable {
var domains_being_blocked: String
var ads_percentage_today: String
var ads_blocked_today: String
var dns_queries_today: String
}
}
Everything seems okay, no errors, yet when I run it, the simulator only shows an empty list
In Playground I can retrieve those data just fine:
import SwiftUI
struct PiHoleTest: Codable {
let domains_being_blocked: String
let ads_blocked_today: String
}
let data = try! Data.init(contentsOf: URL.init(string: "http://pi.hole/admin/api.php?summary")!)
do {
let decoder: JSONDecoder = JSONDecoder.init()
let user: PiHoleTest = try decoder.decode(PiHoleTest.self, from: data)
print("In Blocklist \(user.domains_being_blocked)")
print("Blocked Today: \(user.ads_blocked_today) ")
} catch let e {
print(e)
}
The Output:
In Blocklist 1,089,374
Blocked Today: 11,258
What am I doing wrong? Or better, is there another way to fetch these stats?
Thanks in Advance!
The issue was related to the structure. Your JSON decoded were not an array. So PiHole struct was unnecessary. I can tested and this code is working now.
import SwiftUI
struct NetworkController {
static func fetchData(completion: #escaping ((Stat) -> Void)) {
if let url = URL(string: "http://pi.hole/admin/api.php?summary") {
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { (data, response, error) in
do {
if let data = data {
let stat = try JSONDecoder().decode(Stat.self, from: data)
DispatchQueue.main.async() {
completion(stat)
}
return
} else {
print("Error Found")
}
} catch let error as NSError {
print(error.localizedDescription)
}
}.resume()
}
}
}
class ContentViewModel: ObservableObject {
#Published var stat: Stat? = nil
func fetchData() {
NetworkController.fetchData { stat in
self.stat = stat
}
}
}
struct TestView: View {
#ObservedObject var viewModel = ContentViewModel()
var body: some View {
List {
Text(viewModel.stat?.domains_being_blocked ?? "No Data")
Text(viewModel.stat?.ads_blocked_today ?? "No Data")
Text(viewModel.stat?.ads_percentage_today ?? "No Data")
Text(viewModel.stat?.dns_queries_today ?? "No Data")
}.onAppear{
self.viewModel.fetchData()
}
}
}
struct Stat: Decodable, Hashable {
var domains_being_blocked: String
var ads_percentage_today: String
var ads_blocked_today: String
var dns_queries_today: String
}