I am trying to make a view that shows the stats of a bulb, I want to show if the device is on or off and what its brightness is. I already have an API that can return this information in JSON and also have a web GUI. But I want to make an app on my iPhone so I am very new to Swift so used this video to parse the JSON response from the API and print it to the console. I now don't know how to actually put the information I get into visible pieces of text. I will show you the JSON return I get and the code I have already done:
Parsed JSON
BulbInfo(error_code: 0, result: UITest.Result(device_on: true, brightness: 100))
API return JSON
{'error_code': 0,
'result': {
'device_id': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
'fw_ver': '1.1.9 Build 20210122 Rel. 56165',
'hw_ver': '1.0.0',
'type': 'SMART.TAPOBULB',
'model': 'L510 Series',
'mac': 'xx-xx-xx-xx-xx-xx',
'hw_id': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
'fw_id': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
'oem_id': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
'specs': 'EU',
'lang': 'en_US',
'device_on': True,
'on_time': 3065,
'overheated': False,
'nickname': 'TWFpbiBMaWdodA==',
'avatar': 'hang_lamp_1',
'brightness': 100,
'default_states': {
'brightness': {
'type': 'last_states',
'value': 100
}
},
'time_diff': 0,
'region': 'Europe/London',
'longitude': -xxxxx,
'latitude': xxxxxx,
'has_set_location_info': True,
'ip': '192.168.x.xxx',
'ssid': 'xxxxxxxxxxxx',
'signal_level': 1,
'rssi': -xx
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
func getDeviceInfo(){
let urlString = "http://192.168.x.xxx:xxx/get_bulb_info"
let url = URL(string:urlString)
let session = URLSession.shared
let dataTask = session.dataTask(with: url!){(data,response,error)in
// Check for error
if error == nil && data != nil {
// Parse JSON
let decoder = JSONDecoder()
do{
let bulbInfo = try decoder.decode(BulbInfo.self, from: data!)
print(bulbInfo)
}
catch{
print(error)
}
}
}
dataTask.resume()
}
var body: some View {
Text("Main Light:").padding()
Button(action:getDeviceInfo){
Text("Get Device Info!")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Bulb.swift
//
// Bulb.swift
// UITest
//
// Created by James Westhead on 18/12/2021.
//
import Foundation
struct BulbInfo: Codable{
var error_code: Int
var result: Result
}
struct Result: Codable{
var device_on:Bool
var brightness: Int
}
Add to ContentView
#State var bulbInfo: BulbInfo? = nil
Then remove the let from the do catch block
You can access the information by using something like
bulbInfo.result.device_on.description
or
bulbInfo.result.brightness.description
inside the Text
Related
I am loading in a json file and creating an array. When a button is clicked, additional data is inserted into the array. What I want to do is export the modified array to a file. So essentially the array that has new data inserted into it.
What I'm not sure about is whether it is possible when exporting data from an array? or maybe I am going about this the wrong way?
EDIT: I don't necessarily want to export a json file, that was just the file type I first tried. I would be happy to export text files or csv's
ContentView
import SwiftUI
import UniformTypeIdentifiers
struct ContentView: View {
#State private var name = ""
#FocusState private var nameIsFocused: Bool
#State var labels: [LabelData] = []
#State var index = 0
#State var saveFile = false
var body: some View {
HStack {
Button(action: {
index += 1
if index <= labels.count {
labels[index - 1]._label = "Yellow" }
}) {
Text("Y")
}
Button(action: {
saveFile.toggle()
//print(labels[index - 1])
}) {
Text("Export")
.frame(width: 100, height: 100)
.foregroundColor(Color(red: 0.362, green: 0.564, blue: 1))
.background(Color(red: 0.849, green: 0.849, blue: 0.849))
.clipShape(RoundedRectangle(cornerRadius: 25.0, style: .continuous))
}
.offset(x: 0, y: 0)
.fileExporter(isPresented: $saveFile, document: Doc(url: Bundle.main.path(forResource: "labeldata", ofType: "json")!), contentType: .json) { (res) in
do {
let fileUrl = try res.get()
print(fileUrl)
}
catch {
print("cannot save doc")
print(error.localizedDescription)
}
}
}
VStack{
VStack {
if index < labels.count{
if let test = labels[index] {
Text(test._name)
}}}
.offset(x: 0, y: -250)
.frame(
minWidth: 0,
maxWidth: 325
)
VStack {
if index < labels.count{
if let test = labels[index] {
Text(test._name)
}}}
.offset(x: 0, y: -150)
.frame(
minWidth: 0,
maxWidth: 325
)
VStack {
if index < labels.count{
if let test = labels[index] {
Text(test._label)
}}}
.offset(x: 0, y: -50)
.frame(
minWidth: 0,
maxWidth: 325
)
}
.onAppear {
labels = load("labeldata.json")
}
}
}
struct Doc : FileDocument {
var url : String
static var readableContentTypes: [UTType]{[.json]}
init(url : String) {
self.url = url
}
init(configuration: ReadConfiguration) throws {
url = ""
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let file = try! FileWrapper(url: URL(fileURLWithPath: url), options: .immediate)
return file
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
LabelData
import Foundation
struct LabelData: Codable {
var _id: Int
var _name: String
var _type: String
var _description: String
var _label: String
}
labeldata.json
[
{
"_id" : 1,
"_name" : "Label1",
"_type" : "type1",
"_description" : "description1",
"_label" : ""
},
{
"_id" : 2,
"_name" : "Label2",
"_type" : "type2",
"_description" : "description2",
"_label" : ""
}
]
DataLoader
import Foundation
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)")
}
}
The fileExporter writes in-memory data to location selected by a user, so we need to create document with our content and generate FileWrapper from content to exported data (CSV in this example).
So main parts, at first, exporter:
.fileExporter(isPresented: $saveFile,
document: Doc(content: labels), // << document from content !!
contentType: .plainText) {
and at second, document:
struct Doc: FileDocument {
static var readableContentTypes: [UTType] { [.plainText] }
private var content: [LabelData]
init(content: [LabelData]) {
self.content = content
}
// ...
// simple wrapper, w/o WriteConfiguration multi types or
// existing file selected handling (it is up to you)
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let text = content.reduce("") {
$0 + "\($1._id),\($1._name),\($1._type),\($1._description),\($1._label)\n"
}
return FileWrapper(regularFileWithContents:
text.data(using: .utf8) ?? Data()) // << here !!
}
Tested with Xcode 13.4 / iOS 15.5
Test module is here
It sounds like you want to create a new JSON file from the modified data within the array. It is a bit unusual to want to create a new JSON file. Maybe you want to persist the data? In that case you wouldn't save it as JSON you would persist it with a proper DB (DataBase) like CoreData, FireBase, Realm, or ect...
But if you really want to do this. Then you need to create a new JSON file from the data in the array. You have a load<T: Decodable> function but you are going to want a save<T: Codable> function. Most people would, once again, use this opportunity to save the data to a DB.
This guys does a pretty good job explaining this: Save json to CoreData as String and use the String to create array of objects
So here is a good example of saving JSON data to a file:
let jsonString = "{\"location\": \"the moon\"}"
if let documentDirectory = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first {
let pathWithFilename = documentDirectory.appendingPathComponent("myJsonString.json")
do {
try jsonString.write(to: pathWithFilename,
atomically: true,
encoding: .utf8)
} catch {
// Handle error
}
}
Currently not getting data from API to display.
Current code:
import SwiftUI
struct Response: Decodable {
var content: [Result]
}
struct Result : Decodable {
var code: String
var fire: String
var name: String
var police: String
var medical: String
}
struct ContentView: View {
#State private var content = [Result]()
var body: some View {
List(content, id: \.code) { item in
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.medical)
}
}
.onAppear(perform: loadData)
}
func loadData() {
let url = URL(string: "https://emergency-phone-numbers.herokuapp.com/country/gb")!
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error { print(error); return }
do {
let decodedResponse = try JSONDecoder().decode(Response.self, from: data!)
// we have good data – go back to the main thread
DispatchQueue.main.async {
// update our UI
self.content = decodedResponse.content
}
} catch {
print(error)
}
}.resume()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Current error:
2021-01-21 19:21:07.094582+0000 iTunes API[39924:1098972] [] nw_protocol_get_quic_image_block_invoke dlopen libquic failed
keyNotFound(CodingKeys(stringValue: "content", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "content", intValue: nil) ("content").", underlyingError: nil))
Please advise. Thanks in advance.
Please note that the content from https://emergency-phone-numbers.herokuapp.com/country/gb is:
{"code":"GB","fire":"999","police":"999","name":"United Kingdom","medical":"999"}
The problem is that there's no content on the response that you're getting, so the JSONDecoder is failing to parse the response. It seems the response you get contains content that fits on Result, so if you change your code to parse that, it works fine:
import SwiftUI
struct Response: Decodable {
var content: [Result]
}
struct Result : Decodable {
var code: String
var fire: String
var name: String
var police: String
var medical: String
}
struct ContentView: View {
#State private var content = [Result]()
var body: some View {
List(content, id: \.code) { item in
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.medical)
}
}
.onAppear(perform: loadData)
}
func loadData() {
let url = URL(string: "https://emergency-phone-numbers.herokuapp.com/country/gb")!
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error { print(error); return }
do {
let decodedResponse = try JSONDecoder().decode(Result.self, from: data!)
// we have good data – go back to the main thread
DispatchQueue.main.async {
// update our UI
self.content = [decodedResponse]
}
} catch {
print(error)
}
}.resume()
}
}
I guess, in the end, you probably wanted a collection of Results to fit on the content, so you might need to change something on that API of yours to return that collection, then your code would work fine.
I'm trying to set up the #AppStorage wrapper in my project.
I'm pulling Texts from a JSON API (see DataModel), and am hoping to store the results in UserDefautls. I want the data to be fetched .OnAppear and stored into the #AppStorage. When the user taps "Get Next Text", I want a new poem to be fetched, and to update #AppStorage with the newest Text data, (which would delete the past Poem stored).
Currently, the code below builds but does not display anything in the Text(currentPoemTitle).
Data Model
import Foundation
struct Poem: Codable, Hashable {
let title, author: String
let lines: [String]
let linecount: String
}
public class FetchPoem: ObservableObject {
// 1.
#Published var poems = [Poem]()
init() {
getPoem()
}
func getPoem() {
let url = URL(string: "https://poetrydb.org/random/1")!
// 2.
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let poemData = data {
// 3.
let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
DispatchQueue.main.async {
self.poems = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
TestView
import SwiftUI
struct Test: View {
#ObservedObject var fetch = FetchPoem()
#AppStorage("currentPoemtTitle") var currentPoemTitle = ""
#AppStorage("currentPoemAuthor") var currentPoemAuthor = ""
var body: some View {
VStack{
Text(currentPoemTitle)
Button("Fetch next text") {
fetch.getPoem()
}
}.onAppear{
if let poem = fetch.poems.first {
currentPoemTitle = "\(poem.title)"
currentPoemAuthor = "\(poem.author)"
}
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
What am I missing? Thanks.
Here are a few code edits to get you going.
I added AppStorageKeys to manage the #AppStorage keys, to avoid errors retyping key strings (ie. "currentPoemtTitle")
Your question asked how to update the #AppStorage with the data, and the simple solution is to add the #AppStorage variables within the FetchPoem class and set them within the FetchPoem class after the data is downloaded. This also avoids the need for the .onAppear function.
The purpose of using #ObservedObject is to be able to keep your View in sync with the data. By adding the extra layer of #AppStorage, you make the #ObservedObject sort of pointless. Within the View, I added a Text() to display the title using the #ObservedObject values directly, instead of relying on #AppStorage. I'm not sure if you want this, but it would remove the need for the #AppStorage variables entirely.
I also added a getPoems2() function using Combine, which is a new framework from Apple to download async data. It makes the code a little easier/more efficient... getPoems() and getPoems2() both work and do the same thing :)
Code:
import Foundation
import SwiftUI
import Combine
struct AppStorageKeys {
static let poemTitle = "current_poem_title"
static let poemAuthor = "current_poem_author"
}
struct Poem: Codable, Hashable {
let title, author: String
let lines: [String]
let linecount: String
}
public class FetchPoem: ObservableObject {
#Published var poems = [Poem]()
#AppStorage(AppStorageKeys.poemTitle) var poemTitle = ""
#AppStorage(AppStorageKeys.poemAuthor) var poemAuthor = ""
init() {
getPoem2()
}
func getPoem() {
let url = URL(string: "https://poetrydb.org/random/1")!
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
guard let poemData = data else {
print("No data")
return
}
let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
DispatchQueue.main.async {
self.poems = decodedData
self.updateFirstPoem()
}
} catch {
print("Error")
}
}
.resume()
}
func getPoem2() {
let url = URL(string: "https://poetrydb.org/random/1")!
URLSession.shared.dataTaskPublisher(for: url)
// fetch on background thread
.subscribe(on: DispatchQueue.global(qos: .background))
// recieve response on main thread
.receive(on: DispatchQueue.main)
// ensure there is data
.tryMap { (data, response) in
guard
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return data
}
// decode JSON data to [Poem]
.decode(type: [Poem].self, decoder: JSONDecoder())
// Handle results
.sink { (result) in
// will return success or failure
print("poetry fetch completion: \(result)")
} receiveValue: { (value) in
// if success, will return [Poem]
// here you can update your view
self.poems = value
self.updateFirstPoem()
}
// After recieving response, the URLSession is no longer needed & we can cancel the publisher
.cancel()
}
func updateFirstPoem() {
if let firstPoem = self.poems.first {
self.poemTitle = firstPoem.title
self.poemAuthor = firstPoem.author
}
}
}
struct Test: View {
#ObservedObject var fetch = FetchPoem()
#AppStorage(AppStorageKeys.poemTitle) var currentPoemTitle = ""
#AppStorage(AppStorageKeys.poemAuthor) var currentPoemAuthor = ""
var body: some View {
VStack(spacing: 10){
Text("App Storage:")
Text(currentPoemTitle)
Text(currentPoemAuthor)
Divider()
Text("Observed Object:")
Text(fetch.poems.first?.title ?? "")
Text(fetch.poems.first?.author ?? "")
Button("Fetch next text") {
fetch.getPoem()
}
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
I am trying to complete a pretty standard thing: decode a JSON response. I can get the data, however decoding does not succeed - worse, there is no error message. Print statement 1 & 2 output with the correct data but I get no output from print statement 3, it outputs print statement 4 instead (e.g. my error message).
I am hoping someone can see my probably obvious error, thanks for any hints that could resolve me issue!
Phil
(Edit) Added JSON Response:
[{
"open": {
"price": 122.52,
"time": 1668853732275
},
"close": {
"price": 125.44,
"time": 1658436480762
},
"high": 125.35,
"low": 123.57,
"volume": 75244144,
"symbol": "AAPL"
}]
struct ResponsePrice: Codable {
var results: [ResultPrice]
}
struct ResultPrice: Codable {
var open: ResultPriceTime
var close: ResultPriceTime
var high: Double
var low: Double
var volume: Double
var symbol: String
}
struct ResultPriceTime: Codable {
var price: Double
var time: Double
}
///
struct SymbolView: View {
#State private var results = [ResultPrice]()
var body: some View {
List(resultsPrice, id: \.symbol) { item in
VStack(alignment: .leading) {
Text(item.symbol)
.font(.headline)
Text(item.symbol)
}
}.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://sandbox.iexapis.com/stable/stock/market/ohlc?symbols=aapl&token=Tpk_af03c7f5bac14742a7ce77969a791c66") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
do {
if let data = data {
//let stringData = String(decoding: data, as: UTF8.self)
//print(stringData)
if let decodedResponse = try JSONDecoder().decode(ResponsePrice.self, from: data) {
// we have good data – go back to the main thread
print("Fetched: \(url)")
DispatchQueue.main.async {
// update our UI
self.results = decodedResponse.results
print("Ticker:\(self.results[0].T)")
}
// everything is good, so we can exit
return
}
}
}
catch {
fatalError("Couldn't parse as :\n\(error)")
}
// if we're still here it means there was a problem
print("4 Decode failed: \(error?.localizedDescription ?? "Unknown error")")
// step 4
}.resume()
}
}
struct SymbolView_Previews: PreviewProvider {
static var previews: some View {
SymbolView()
}
}
NOTE: Forgive my cluelessness, i am still new in regards to this. The full code is posted at the bottom.
ISSUE: It seems that when i have a short nest, i am able to call it for my #Published property however when i try an api request with a longer nest, like this. and type Decodable structs that follows the structure of the GET request
struct TripScheduleTest: Codable {
let TripList: InitialNest
}
struct InitialNest: Codable {
var Trip: [TravelDetail]
}
struct TravelDetail: Codable {
var Leg: [TripTest]
}
struct TripTest: Codable, Hashable {
var name: String
var type: String
}
I am not able to call it for the #Published var dataSet1 = [TripTest]()
self.dataSet1 = tripJSON.TripList.Trip.Leg
I get an error message, that says "Value of type '[TravelDetail]' has no member 'Leg'
I am not sure why, however it works when i use [TravelDetail]() instead of [TripTest]() in the #Published var and stop at Trip before Leg for the dataSet1, then it seems to at least build successfully. But now i am not able to get the name and type information from the request
Full code
import SwiftUI
struct TripScheduleTest: Codable {
let TripList: InitialNest
}
struct InitialNest: Codable {
var Trip: [TravelDetail]
}
struct TravelDetail: Codable {
var Leg: [TripTest]
}
struct TripTest: Codable, Hashable {
var name: String
var type: String
}
class TripViewModel: ObservableObject {
#Published var dataSet1 = [TripTest]()
init() {
let urlString = "http://xmlopen.rejseplanen.dk/bin/rest.exe/trip?originId=8600790&destId=6553&format=json"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, resp, err) in
guard let data = data else { return }
do {
let tripJSON = try
JSONDecoder().decode(TripScheduleTest.self, from: data)
print(data)
DispatchQueue.main.async {
self.dataSet1 = tripJSON.TripList.Trip.Leg
}
} catch {
print("JSON Decode error: ", error)
}
}.resume()
}
}
struct TripView: View {
#ObservedObject var vm = TripViewModel()
var body: some View {
List(vm.dataSet1, id: \.self) { day in
Text("Test")
.font(.system(size: 12, weight: .bold))
Text(" \(day.name)")
.font(.system(size: 12))
}
}
}
Trip is an array (note the [])
You need to get one item of the array by index for example
tripJSON.TripList.Trip.first?.Leg
To assign the value to a non-optional array write
self.dataSet1 = tripJSON.TripList.Trip.first?.Leg ?? []