I am trying to get data from JSON. So far, I can extract it and print it.
My goal now is to be able to use it in my ContentView so that it can be used in a Text View or something similar. I have tried creating #State variables, passing it as a parameter, etc. etc. and nothing seems to work.
I'm fairly new to SwiftUI, so I appreciate the help!
struct GeoService: Codable {
var status: String
var results: [GeoResult]
}
struct GeoResult: Codable {
struct Geometry: Codable {
struct Location: Codable {
let lat: Float
let lng: Float
init() {
lat = 32
lng = 30
}
}
let location: Location
}
let formatted_address: String
let geometry: Geometry
}
struct ContentView: View {
// #State private var results: Any ?????????
var body: some View {
NavigationView {
Text("Test")
.navigationTitle("Quotes")
.task {
await handleData()
}
}
}
func handleData() async {
let geoResult="""
{
"results": [
{
"formatted_address": "1600 Amphitheatre Parkway, Mountain View, CA 94043, USA",
"geometry": {
"location": {
"lat": 37.4224764,
"lng": -122.0842499
}
}
},
{
"formatted_address": "Test addresss",
"geometry": {
"location": {
"lat": 120.32132145,
"lng": -43.90235469
}
}
}
],
"status": "OK"
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
print("executing handleData()")
do {
let obj = try decoder.decode(GeoService.self, from: geoResult)
for result in obj.results {
print("Address: \(result.formatted_address)")
print("Lat/long: (\(result.geometry.location.lat), \(result.geometry.location.lng))")
}
} catch {
print("Did not work :(")
}
}
}
I moved the get request to its own class so we can leverage ObservableObject and Publish the data. Publishing the data allows the subscribing views to update data when the published data changes. In this case, your ContentView is subscribing to the data via the #ObservedObject var geoData: ResponseData line of code.
Additionally, I added how to access two pieces of relevant data in your view, and displayed it in a list for easy reading. This should give you an idea of how to access/display the data.
Hopefully this provides enough information for you to tweak the answer to get it work the way you desire.
import SwiftUI
struct GeoService: Codable {
var status: String?
var results: [GeoResult]?
}
struct GeoResult: Codable {
struct Geometry: Codable {
struct Location: Codable {
let lat: Float
let lng: Float
init() {
lat = 32
lng = 30
}
}
let location: Location
}
let formatted_address: String
let geometry: Geometry
}
struct ContentView: View {
#ObservedObject var geoData: ResponseData
var body: some View {
NavigationView {
if #available(iOS 15.0, *) {
List {
Text(geoData.geoResultsData?.results?[0].formatted_address ?? "Loading")
Text(String(geoData.geoResultsData?.results?[0].geometry.location.lat ?? 0))
}
.navigationTitle("Quotes")
.task {
await geoData.handleData()
print(geoData.geoResultsData, "yessssss")
}
} else {
Text("failure")
}
}
}
}
class ResponseData: ObservableObject {
#Published var geoResultsData: GeoService?
func handleData() async {
let geoResult="""
{
"results": [
{
"formatted_address": "1600 Amphitheatre Parkway, Mountain View, CA 94043, USA",
"geometry": {
"location": {
"lat": 37.4224764,
"lng": -122.0842499
}
}
},
{
"formatted_address": "Test addresss",
"geometry": {
"location": {
"lat": 120.32132145,
"lng": -43.90235469
}
}
}
],
"status": "OK"
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
print("executing handleData()")
do {
let obj = try decoder.decode(GeoService.self, from: geoResult)
geoResultsData = obj
} catch {
print("Did not work :(")
}
}
}
EDIT:
You will need to initialize some data within your app. You can initialize it as empty for the time being, if desired. This can be done by doing something like the following: ContentView(geoData: ResponseData.init())
Related
As a follow-up to this question, I now want to iterate through an array of Codable structs in SwiftUI and render them in my ContentView{} as Text or List items.
I have tried implementing a variable, geoDataArray, in the .task section then iterating over it with a ForEach in my ContentView but received a lot of errors about types and unwrapping values.
Any help is appreciated! I am still new to SwiftUI.
Below is my code:
struct GeoService: Codable {
var status: String
var results: [GeoResult]
}
struct GeoResult: Codable {
struct Geometry: Codable {
struct Location: Codable {
let lat: Float
let lng: Float
init() {
lat = 32
lng = 30
}
}
let location: Location
}
let formatted_address: String
let geometry: Geometry
}
struct ContentView: View {
// #State private var results: Any ?????????
var body: some View {
NavigationView {
Text("Test")
.navigationTitle("Quotes")
.task {
await handleData()
}
}
}
func handleData() async {
let geoResult="""
{
"results": [
{
"formatted_address": "1600 Amphitheatre Parkway, Mountain View, CA 94043, USA",
"geometry": {
"location": {
"lat": 37.4224764,
"lng": -122.0842499
}
}
},
{
"formatted_address": "Test addresss",
"geometry": {
"location": {
"lat": 120.32132145,
"lng": -43.90235469
}
}
}
],
"status": "OK"
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
print("executing handleData()")
do {
let obj = try decoder.decode(GeoService.self, from: geoResult)
for result in obj.results {
print("Address: \(result.formatted_address)")
print("Lat/long: (\(result.geometry.location.lat), \(result.geometry.location.lng))")
}
} catch {
print("Did not work :(")
}
}
}
Your code works fine the way it is for printing to the console, but ForEach requires that GeoResult conforms to either Identifiable (preferred) or at least Hashable. Given that you didn't include the property id in your code, let's have that struct conforming to Hashable.
So, assuming that each GeoResult is different because formatted_address is never the same (you must check if that's true), you can add two functions to ensure conformance. You will get the following:
struct GeoResult: Codable, Hashable { // <- Conform to Hashable
// Differentiating
static func == (lhs: GeoResult, rhs: GeoResult) -> Bool {
lhs.formatted_address == rhs.formatted_address
}
// Hashing
func hash(into hasher: inout Hasher) {
hasher.combine(formatted_address)
}
struct Geometry: Codable {
struct Location: Codable {
let lat: Float
let lng: Float
init() {
lat = 32
lng = 30
}
}
let location: Location
}
let formatted_address: String
let geometry: Geometry
}
In the view, add an array of GeoResult, that will be the #State variable to iterate over. Place the .task() modifier on the outermost view.
// This is the list
#State private var geoArray: [GeoResult] = []
var body: some View {
NavigationView {
VStack {
// GeoResult is not Identifiable, so it is necessary to include id: \.self
ForEach(geoArray, id: \.self) { result in
NavigationLink {
Text("Lat/long: (\(result.geometry.location.lat), \(result.geometry.location.lng))")
} label: {
Text("Address: \(result.formatted_address)")
}
}
.navigationTitle("Quotes")
}
}
// Attach the task to the outermost view, in this case the NavigationView
.task {
await handleData()
}
}
Finally, change the #State variable in your function, after decoding:
func handleData() async {
// ...
let decoder = JSONDecoder()
do {
let obj = try decoder.decode(GeoService.self, from: geoResult)
// Add this
geoArray = obj.results
} catch {
print("Did not work :(\n\(error)")
}
}
I want to parse the following JSON into a list with SwiftUI, however I am experiencing some issues with this.
JSON (there are many more entires than this with random Identifiers as the entry. This is what caused it to be much harder to perform):
{
"DCqkboGRytms": {
"name": "graffiti-brush-3.zip",
"downloads": "5",
"views": "9",
"sha256": "767427c70401d43da30bc6d148412c31a13babacda27caf4ceca490813ccd42e"
},
"gIisYkxYSzO3": {
"name": "twitch.png",
"downloads": "19",
"views": "173",
"sha256": "aa2a86e7f8f5056428d810feb7b6de107adefb4bf1d71a4ce28897792582b75f"
},
"bcfa82": {
"name": "PPSSPP.ipa",
"downloads": "7",
"views": "14",
"sha256": "f8b752adf21be6c79dae5b80f5b6176f383b130438e81b49c54af8926bce47fe"
}
}
SwiftUI Codables:
struct Feed: Codable {
let list: [FeedValue]
}
struct FeedValue: Codable, Identifiable {
var id = UUID()
let name, downloads, views, sha256: String
}
JSON Fetch method (and parse):
func getFeeds(completion:#escaping (Feed) -> ()) {
// URL hidden to respect API privacy
guard let url = URL(string: "") else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
let feed = try! JSONDecoder().decode(Feed.self, from: data!)
DispatchQueue.main.async {
completion(feed)
}
}.resume()
}
Finally, the view containing my ForEach loop:
struct HomeView: View {
#State private var scrollViewContentOffset: CGFloat = .zero
#State var listFeed = Feed(list: [])
var body: some View {
// Z-coord status bar holder
ZStack(alignment: .top) {
TrackableScrollView(.vertical, showIndicators: true, contentOffset: $scrollViewContentOffset) {
// Public files Start
LazyVStack(spacing: 0) { {
// Withholds our content for each asset retrieved.
// Main part
ForEach(listFeed.list, id: \.id) { download in
AssetView(name: .constant(download.name), downloads: .constant(download.downloads), views: .constant(download.views))
}
}.padding([.leading, .top, .trailing], 25)
// Public files End
}.edgesIgnoringSafeArea([.leading, .trailing, .top])
}.background(Color.backgroundColor.ignoresSafeArea())
.onAppear() {
getFeeds { feed in
//print(feed) : this did happen to work at the time of testing
self.listFeed = feed
}
}
}
}
And for the error that is being presented:
Starfiles/HomeView.swift:32: Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "list", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "list", intValue: nil) ("list").", underlyingError: nil))
I've spent several hours today changing the method of parsing, but was unable to find a solution on my own.
EDIT 1: I would like to mention that I need a way to access the dictionary ID's as well, as they'll be used for entering in a new view.
EDIT 2: Using what #jnpdx provided, JSON parsing now works as expectedly, and gets the information I need. However there's an issue with ForEach loops, the same one which persisted before (It does work in a List(), as his answer showed).
Here is the updated code (not going to show any code that's irrelevant)
ForEach(listItems) { item in // Cannot convert value of type '[IdentifiableFeedValue]' to expected argument type 'Binding<C>' & Generic parameter 'C' could not be inferred
AssetView(name: .constant(item.feedValue.name), // Cannot convert value of type 'Binding<Subject>' to expected argument type 'String'
downloads: .constant(item.feedValue.downloads), views: .constant(item.feedValue.views))
}.onAppear() {
// URL hidden to respect API privacy, refer to JSON above.
guard let url = URL(string: "") else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
do {
list = try JSONDecoder().decode([String:FeedValue].self, from: data!)
} catch {
print("error")
}
}.resume()
}
// AssetView
struct AssetView: View {
#Binding var name: String
#Binding var downloads: String
#Binding var views: String
...
}
Your JSON doesn't have any keys called list, which is why it's failing. Because it's a dynamically-keyed dictionary, you will want to decode it as [String:FeedValue].
I used a wrapper to keep the id from the original JSON, but you could also do some fancier decoding if you wanted to keep it in a one-level struct, but that's beyond the scope of this question.
let jsonData = """
{
"DCqkboGRytms": {
"name": "graffiti-brush-3.zip",
"downloads": "5",
"views": "9",
"sha256": "767427c70401d43da30bc6d148412c31a13babacda27caf4ceca490813ccd42e"
},
"gIisYkxYSzO3": {
"name": "twitch.png",
"downloads": "19",
"views": "173",
"sha256": "aa2a86e7f8f5056428d810feb7b6de107adefb4bf1d71a4ce28897792582b75f"
},
"bcfa82": {
"name": "PPSSPP.ipa",
"downloads": "7",
"views": "14",
"sha256": "f8b752adf21be6c79dae5b80f5b6176f383b130438e81b49c54af8926bce47fe"
}
}
""".data(using: .utf8)!
struct FeedValue: Codable {
let name, downloads, views, sha256: String
}
struct IdentifiableFeedValue : Identifiable {
let id: String
let feedValue: FeedValue
}
struct ContentView: View {
#State var list : [String:FeedValue] = [:]
var listItems: [IdentifiableFeedValue] {
list.map { IdentifiableFeedValue(id: $0.key, feedValue: $0.value) }
}
var body: some View {
List(listItems) { item in
Text(item.feedValue.name)
}.onAppear {
do {
list = try JSONDecoder().decode([String:FeedValue].self, from: jsonData)
} catch {
print(error)
}
}
}
}
I need to create a search logic for NameString instead of NameSection. I was able to make the search bar here (How do I create a search bar for SwiftUI?). I want to search names instead of sections. I tried numerous times but it won't work. NameString thinks it's not String.
So in the JSON File there are sections like Monday, Wednesday, etc, currently the code supports a search for those instead of items in those days like Name11, Name 12. I need to create a search for those items.
Here is what I have
import SwiftUI
var counter = 0
struct ContentView: View {
let name = Bundle.main.decode([NameSection].self, from: "name copy.json")
let names = Bundle.main.decode([NameString].self,from: "name copy.json")
#State private var searchText = ""
var body: some View {
NavigationView{
VStack {
SearchBarView(searchText: $searchText)
List{
ForEach(name.filter{$0.name.hasPrefix(searchText) || searchText == ""}, id:\.self) {
section in
Section(header: Text(section.name)) {
ForEach(section.items){ item in
NavigationLink(destination:
TrackerDetails(item: item)){
Tracker(item: item)
}
}
}
}
}
.navigationTitle("Names")
.listStyle(GroupedListStyle())
}
}
}
}
import SwiftUI
struct NameSection: Codable, Identifiable, Hashable {
var id: UUID
var name: String
var items: [NameItem]
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
struct NameItem: Codable, Equatable, Identifiable {
var id: UUID
var name: String
var mainImage: String {
name.replacingOccurrences(of: " ", with: "-").lowercased()
}
var thumbnailImage: String {
"\(mainImage)-thumb"
}
#if DEBUG
static let example = NameItem(id: UUID(), name: "Maple French Toast")
#endif
}
struct NameString: Codable, Identifiable{
var id: UUID
var name: String
}
JSON
[{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A66",
"name": "Monday",
"items": [{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A67",
"name": "Name11"
},
{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A68",
"name": "Name12"
},
{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A69",
"name": "Name13"
}
]
},
{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A67",
"name": "Wednesday",
"items": [{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A60",
"name": "Name21"
},
{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A61",
"name": "Name22"
},
{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A62",
"name": "Name23"
}
]
},
{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A90",
"name": "SATURDAY",
"items": [{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A96",
"name": "Name21"
},
{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A94",
"name": "Name22"
},
{
"id": "EF1CC5BB-4785-4D8E-AB98-5FA4E00B6A35",
"name": "Name23"
}
]
},
]
JSON Decoder
import UIKit
extension Bundle {
func decode<T: Decodable>(_ type: T.Type, from file: String) -> T {
guard let url = self.url(forResource: file, withExtension: nil) else {
fatalError("Failed to locate \(file) in bundle.")
}
guard let data = try? Data(contentsOf: url) else {
fatalError("Failed to load \(file) from bundle.")
}
let decoder = JSONDecoder()
guard let loaded = try? decoder.decode(T.self, from: data) else {
fatalError("Failed to decode \(file) from bundle.")
}
return loaded
}
}
At your request in a comment on yesterdays question, here's a brief function to filter the results based on the name of the item:
struct ContentView: View {
let name = Bundle.main.decode([NameSection].self, from: "name.json")
#State private var searchText = ""
var filteredItems : [NameSection] {
if searchText.isEmpty { return name }
return name.map { nameSection in
var nameSectionCopy = nameSection
nameSectionCopy.items = nameSection.items.filter { $0.name.lowercased().contains(searchText.lowercased()) }
return nameSectionCopy
}.filter { !$0.items.isEmpty }
}
var body: some View {
NavigationView{
VStack {
SearchBarView(searchText: $searchText)
List{
ForEach(filteredItems, id:\.self) {
section in
Section(header: Text(section.name)) {
ForEach(section.items){ item in
Text(item.name)
}
}
}
.navigationTitle("Names")
.listStyle(GroupedListStyle())
}
}
}
}
}
It checks for a case-insensitive match (see that both searchText and name are lowercased()) that appears anywhere in the name field (see contains()).
It filters out sections (ie Days) that don't have any items (see the last filter of filteredItems).
I didn't have access to your TrackerDetails or Tracker, so you'll have to re-implement your NavigationLink.
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()
}
}
How do I decode this JSON Data?
I've done it with the "drilling down" method where I kept calling each key and printing the value. I also tried the data model but it never worked. I probably did something wrong but I don't know what.
Thanks in advance, If you need more information, just ask, I'm fairly new to Swift and Stackoverflow.
{
"items":[
{
"id":16000014,
"name":"BO",
"starPowers":[
{
"id":23000090,
"name":"CIRCLING EAGLE"
},
{
"id":23000148,
"name":"SNARE A BEAR"
}
],
"gadgets":[
{
"id":23000263,
"name":"SUPER TOTEM"
},
{
"id":23000289,
"name":"TRIPWIRE"
}
]
},
{
"id":16000015,
"name":"PIPER",
"starPowers":[
{
"id":23000091,
"name":"AMBUSH"
},
{
"id":23000152,
"name":"SNAPPY SNIPING"
}
],
"gadgets":[
{
"id":23000268,
"name":"AUTO AIMER"
},
{
"id":23000291,
"name":"HOMEMADE RECIPE"
}
]
}
],
"paging":{
"cursors":{
}
}
}
Decoding JSON in swift is insanely easy. Just use the JSONDecoder class. Firstly, create a Codable class for your json response like this
struct Items: Codable {
let items: [Item]
let paging: Paging
}
struct Item: Codable {
let id: Int
let name: String
let starPowers, gadgets: [Gadget]
}
struct Gadget: Codable {
let id: Int
let name: String
}
struct Paging: Codable {
let cursors: Cursors
}
struct Cursors: Codable {
}
And then use it to parse your JSON like this
let decoder = JSONDecoder()
do {
let items = try decoder.decode(Items.self, from: jsonData)
print(items)
// Do something with the items here
} catch {
print(error.localizedDescription)
}
struct Name {
var id:Int
var name:String
}
struct Cursor {
// Your cursor model
}
struct Paging {
var cursors: Cursor
}
struct Items {
var id:Int
var name:String
var starPowers:[Name]
var gadgets:[Name]
}
struct MainModel {
var items : [Items]
var paging : Paging
}
You can decode that data using let yourData = try! JSONDecoder().decode(MainModel.self, from: jsonData) to get your desired JSON data.