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)")
}
}
Related
I'm trying to make an app that is displaying lists with selections/checkmarks based on clicked NavigationLink. The problem I encountered is that my selections disappear when I go back to main view and then I go again inside the NavigationLink. I'm trying to save toggles value in UserDefaults but it's not working as expected. Below I'm pasting detailed and main content view.
Second view:
struct CheckView: View {
#State var isChecked:Bool = false
#EnvironmentObject var numofitems: NumOfItems
var title:String
var count: Int=0
var body: some View {
HStack{
ScrollView {
Toggle("\(title)", isOn: $isChecked)
.toggleStyle(CheckToggleStyle())
.tint(.mint)
.onChange(of: isChecked) { value in
if isChecked {
numofitems.num += 1
print(value)
} else{
numofitems.num -= 1
}
UserDefaults.standard.set(self.isChecked, forKey: "locationToggle")
}.onTapGesture {
}
.onAppear {
self.isChecked = UserDefaults.standard.bool(forKey: "locationToggle")
}
Spacer()
}.frame(maxWidth: .infinity,alignment: .topLeading)
}
}
}
Main view:
struct CheckListView: View {
#State private var menu = Bundle.main.decode([ItemsSection].self, from: "items.json")
var body: some View {
NavigationView{
List{
ForEach(menu){
section in
NavigationLink(section.name) {
VStack{
ScrollView{
ForEach(section.items) { item in
CheckView( title: item.name)
}
}
}
}
}
}
}.navigationBarHidden(true)
.navigationViewStyle(StackNavigationViewStyle())
.listStyle(GroupedListStyle())
.navigationViewStyle(StackNavigationViewStyle())
}
}
ItemsSection:
[
{
"id": "9DC6D7CB-B8E6-4654-BAFE-E89ED7B0AF94",
"name": "Africa",
"items": [
{
"id": "59B88932-EBDD-4CFE-AE8B-D47358856B93",
"name": "Algeria"
},
{
"id": "E124AA01-B66F-42D0-B09C-B248624AD228",
"name": "Angola"
}
Model:
struct ItemsSection: Codable, Identifiable, Hashable {
var id: UUID = UUID()
var name: String
var items: [CountriesItem]
}
struct CountriesItem: Codable, Equatable, Identifiable,Hashable {
var id: UUID = UUID()
var name: String
}
As allready stated in the comment you have to relate the isChecked property to the CountryItem itself. To get this to work i have changed the model and added an isChecked property. You would need to add this to the JSON by hand if the JSON allread exists.
struct CheckView: View {
#EnvironmentObject var numofitems: NumOfItems
//use a binding here as we are going to manipulate the data coming from the parent
//and pass the complete item not only the name
#Binding var item: CountriesItem
var body: some View {
HStack{
ScrollView {
//use the name and the binding to the item itself
Toggle("\(item.name)", isOn: $item.isChecked)
.toggleStyle(.button)
.tint(.mint)
// you now need the observe the isChecked inside of the item
.onChange(of: item.isChecked) { value in
if value {
numofitems.num += 1
print(value)
} else{
numofitems.num -= 1
}
}.onTapGesture {
}
Spacer()
}.frame(maxWidth: .infinity,alignment: .topLeading)
}
}
}
struct CheckListView: View {
#State private var menu = Bundle.main.decode([ItemsSection].self, from: "items.json")
var body: some View {
NavigationView{
List{
ForEach($menu){ // from here on you have to pass a binding on to the decendent views
// mark the $ sign in front of the property name
$section in
NavigationLink(section.name) {
VStack{
ScrollView{
ForEach($section.items) { $item in
//Pass the complete item to the CheckView not only the name
CheckView(item: $item)
}
}
}
}
}
}
}.navigationBarHidden(true)
.navigationViewStyle(StackNavigationViewStyle())
.listStyle(GroupedListStyle())
.navigationViewStyle(StackNavigationViewStyle())
}
}
Example JSON:
[
{
"id": "9DC6D7CB-B8E6-4654-BAFE-E89ED7B0AF94",
"name": "Africa",
"items": [
{
"id": "59B88932-EBDD-4CFE-AE8B-D47358856B93",
"name": "Algeria",
"isChecked": false
},
{
"id": "E124AA01-B66F-42D0-B09C-B248624AD228",
"name": "Angola",
"isChecked": false
}
]
}
]
Remarks:
The aproach with JSON and storing this in the bundle will prevent you from persisting the isChecked property between App launches. Because you cannot write to the Bundle from within your App. The choice will persist as long as the App is active but will be back to default as soon as you either reinstall or force quit it.
As already mentioned in the comment, I don'r see where you read back from UserDefaults, so whatever gets stored there, you don't read it. But even if so, each Toggle is using the same key, so you are overwriting the value.
Instead of using the #State var isChecked, which is used just locally, I'd create another struct item which gets the title from the json and which contains a boolean that gets initialized with false.
From what I understood, I assume a solution could look like the following code. Just a few things:
I am not sure how your json looks like, so I am not loading from a json, I add ItemSections Objects with a title and a random number of items (actually just titles again) with a function.
Instead of a print with the number of checked toggles, I added a text output on the UI. It shows you on first page the number of all checked toggles.
Instead of using UserDefaults I used #AppStorage.
To make that work you have to make Array conform to RawRepresentable you achieve that with the following code/extension (just add it once somewhere in your project)
Maybe you should thing about a ViewModel (e.g. ItemSectionViewModel), to load the data from the json and provide it to the views as an #ObservableObject.
The code for the views:
//
// CheckItem.swift
// CheckItem
//
// Created by Sebastian on 24.08.22.
//
import SwiftUI
struct ContentView: View {
var body: some View {
VStack() {
CheckItemView()
}
}
}
struct CheckItemView: View {
let testStringForTestData: String = "Check Item Title"
#AppStorage("itemSections") var itemSections: [ItemSection] = []
func addCheckItem(title: String, numberOfItems: Int) {
var itemArray: [Item] = []
for i in 0...numberOfItems {
itemArray.append(Item(title: "item \(i)"))
}
itemSections.append(ItemSection(title: title, items: itemArray))
}
func getSelectedItemsCount() -> Int{
var i: Int = 0
for itemSection in itemSections {
let filteredItems = itemSection.items.filter { item in
return item.isOn
}
i = i + filteredItems.count
}
return i
}
var body: some View {
NavigationView{
VStack() {
List(){
ForEach(itemSections.indices, id: \.self){ id in
NavigationLink(destination: ItemSectionDetailedView(items: $itemSections[id].items)) {
Text(itemSections[id].title)
}
.padding()
}
}
Text("Number of checked items: \(self.getSelectedItemsCount())")
.padding()
Button(action: {
self.addCheckItem(title: testStringForTestData, numberOfItems: Int.random(in: 0..<4))
}) {
Text("Add Item")
}
.padding()
}
}
}
}
struct ItemSectionDetailedView: View {
#Binding var items: [Item]
var body: some View {
ScrollView() {
ForEach(items.indices, id: \.self){ id in
Toggle(items[id].title, isOn: $items[id].isOn)
.padding()
}
}
}
}
struct ItemSection: Identifiable, Hashable, Codable {
var id: String = UUID().uuidString
var title: String
var items: [Item]
}
struct Item: Identifiable, Hashable, Codable {
var id: String = UUID().uuidString
var title: String
var isOn: Bool = false
}
Here the adjustment to work with #AppStorage:
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
}
}
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())
I have a weather app that I am creating. I use an API (OpenWeatherApp) to get my data. I load the data into an array, weatherArray, of type struct WeatherJSON.
I can successfully access the data using a ForEach loop, but when I try to directly access specific data using array[index], I get the following error: Thread 1: Fatal error: Index out of range
Here is the top-level struct that I am using:
// holds all data from the JSON request
struct WeatherJSON: Codable {
var coord: Coord // coordinate struct
var weather: [Weather] // array of Weather struct
var base: String // "internal parameter..?"
var main: Main // main struct (contains the juicy data)
var visibility: Int // visibility number
var wind: Wind // wind struct
var clouds: Clouds // clouds struct
var dt: Int // time of data calculation, unix, UTC
var sys: Sys // internal parameer
var timezone, id: Int // timezone
var name: String // city namme
var cod: Int // another internal parameter (..?)
}
My ContentView:
struct ContentView: View {
#State private var weatherArray: [WeatherJSON] = []
// #State private var weatherArray: [Weather] = []
var body: some View {
NavigationView {
VStack {
Text("\(weatherArray[0].name)") // <---- This doesn't work
List {
// Text("Current conditions: \(weatherArray[0].description)")
// ForEach(weatherArray, id: \.self) { result in
// Section(header:Text("Address")) {
// Text("Current conditions: \(result.main)")
// .font(.headline)
// .bold()
// Text("Weather description: \(result.description)")
// .font(.body)
// }
// }
}
.navigationTitle("Weather")
}
}
.task { await handleData() }
}
func handleData() async {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=Seattle&appid={APIKEY}") else {
print("This URL does not work!")
return
}
let decoder = JSONDecoder()
do {
let (weatherData, _) = try await URLSession.shared.data(from: url)
if let weatherObj = try? decoder.decode(WeatherJSON.self, from: weatherData) {
weatherArray.append(weatherObj)// = weatherObj.weather
print(weatherArray[0]) // <--- This works?
}
} catch {
print("Did not work :(")
}
} ```
Your view is accessing the weatherArray before the data has been loaded. You need to account for a possibly empty array to avoid the crash.
Change:
Text("\(weatherArray[0].name)") // <---- This doesn't work
To:
Text("\(weatherArray.first?.name ?? "")")
try this example code to get the weather data into your models and use it in your view:
public struct Weather: Identifiable, Codable {
public let id: Int
public let main, description, icon: String
}
struct Coord: Codable {
var lon: Double
var lat: Double
}
// holds all data from the JSON request
struct WeatherJSON: Codable {
var coord: Coord // coordinate struct
var weather: [Weather] // array of Weather struct
var base: String // "internal parameter..?"
var main: Main // main struct (contains the juicy data)
var visibility: Int // visibility number
var wind: Wind // wind struct
var clouds: Clouds // clouds struct
var dt: Int // time of data calculation, unix, UTC
var sys: Sys // internal parameer
var timezone, id: Int // timezone
var name: String // city namme
var cod: Int // another internal parameter (..?)
}
struct ContentView: View {
#State private var city: WeatherJSON?
var body: some View {
NavigationView {
VStack {
Text("\(city?.name ?? "no name")")
List {
ForEach(city?.weather ?? []) { weather in
Section(header:Text("Address")) {
Text("Current conditions: \(weather.main)")
.font(.headline)
.bold()
Text("Weather description: \(weather.description)")
.font(.body)
}
}
}
.navigationTitle("Weather")
}
}
.task { await handleData() }
}
func handleData() async {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=Seattle&appid={apikey}") else {
print("This URL does not work!")
return
}
do {
let (weatherData, _) = try await URLSession.shared.data(from: url)
city = try JSONDecoder().decode(WeatherJSON.self, from: weatherData)
print(city)
} catch {
print("error: \(error)")
}
}
}
I am making an app that has information about different woods, herbs and spices, and a few other things. I am including the ability to save their favorite item to a favorites list, so I have a heart button that the user can press to add it to the favorites. Pressing the button toggles the isFavorite property of the item and then leaving the page calls a method that encodes the data to save it to the user's device. The problem that I am running into is that it is not encoding the updated value of the isFavorite property. It is still encoding the value as false, so the favorites list is not persisting after closing and reopening the app.
Here is my Wood.swift code, this file sets up the structure for Wood items. I also included the test data that I was using to make sure that it displayed properly in the Wood extension:
import Foundation
struct Wood: Identifiable, Codable {
var id = UUID()
var mainInformation: WoodMainInformation
var preparation: [Preparation]
var isFavorite = false
init(mainInformation: WoodMainInformation, preparation: [Preparation]) {
self.mainInformation = mainInformation
self.preparation = preparation
}
}
struct WoodMainInformation: Codable {
var category: WoodCategory
var description: String
var medicinalUses: [String]
var magicalUses: [String]
var growZone: [String]
var lightLevel: String
var moistureLevel: String
var isPerennial: Bool
var isEdible: Bool
}
enum WoodCategory: String, CaseIterable, Codable {
case oak = "Oak"
case pine = "Pine"
case cedar = "Cedar"
case ash = "Ash"
case rowan = "Rowan"
case willow = "Willow"
case birch = "Birch"
}
enum Preparation: String, Codable {
case talisman = "Talisman"
case satchet = "Satchet"
case tincture = "Tincture"
case salve = "Salve"
case tea = "Tea"
case ointment = "Ointment"
case incense = "Incense"
}
extension Wood {
static let woodTypes: [Wood] = [
Wood(mainInformation: WoodMainInformation(category: .oak,
description: "A type of wood",
medicinalUses: ["Healthy", "Killer"],
magicalUses: ["Spells", "Other Witchy Stuff"],
growZone: ["6A", "5B"],
lightLevel: "Full Sun",
moistureLevel: "Once a day",
isPerennial: false,
isEdible: true),
preparation: [Preparation.incense, Preparation.satchet]),
Wood(mainInformation: WoodMainInformation(category: .pine,
description: "Another type of wood",
medicinalUses: ["Healthy"],
magicalUses: ["Spells"],
growZone: ["11G", "14F"],
lightLevel: "Full Moon",
moistureLevel: "Twice an hour",
isPerennial: true,
isEdible: true),
preparation: [Preparation.incense, Preparation.satchet])
]
}
Here is my WoodData.swift file, this file contains methods that allow the app to display the correct wood in the list of woods, as well as encode, and decode the woods:
import Foundation
class WoodData: ObservableObject {
#Published var woods = Wood.woodTypes
var favoriteWoods: [Wood] {
woods.filter { $0.isFavorite }
}
func woods(for category: WoodCategory) -> [Wood] {
var filteredWoods = [Wood]()
for wood in woods {
if wood.mainInformation.category == category {
filteredWoods.append(wood)
}
}
return filteredWoods
}
func woods(for category: [WoodCategory]) -> [Wood] {
var filteredWoods = [Wood]()
filteredWoods = woods
return filteredWoods
}
func index(of wood: Wood) -> Int? {
for i in woods.indices {
if woods[i].id == wood.id {
return i
}
}
return nil
}
private var dataFileURL: URL {
do {
let documentsDirectory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
return documentsDirectory.appendingPathComponent("evergreenData")
}
catch {
fatalError("An error occurred while getting the url: \(error)")
}
}
func saveWoods() {
if let encodedData = try? JSONEncoder().encode(woods) {
do {
try encodedData.write(to: dataFileURL)
let string = String(data: encodedData, encoding: .utf8)
print(string)
}
catch {
fatalError("An error occurred while saving woods: \(error)")
}
}
}
func loadWoods() {
guard let data = try? Data(contentsOf: dataFileURL) else { return }
do {
let savedWoods = try JSONDecoder().decode([Wood].self, from: data)
woods = savedWoods
}
catch {
fatalError("An error occurred while loading woods: \(error)")
}
}
}
Finally, this is my WoodsDetailView.swift file, this file displays the information for the wood that was selected, as well as calls the method that encodes the wood data:
import SwiftUI
struct WoodsDetailView: View {
#Binding var wood: Wood
#State private var woodsData = WoodData()
var body: some View {
VStack {
List {
Section(header: Text("Description")) {
Text(wood.mainInformation.description)
}
Section(header: Text("Preparation Techniques")) {
ForEach(wood.preparation, id: \.self) { technique in
Text(technique.rawValue)
}
}
Section(header: Text("Edible?")) {
if wood.mainInformation.isEdible {
Text("Edible")
}
else {
Text("Not Edible")
}
}
Section(header: Text("Medicinal Uses")) {
ForEach(wood.mainInformation.medicinalUses.indices, id: \.self) { index in
let medicinalUse = wood.mainInformation.medicinalUses[index]
Text(medicinalUse)
}
}
Section(header: Text("Magical Uses")) {
ForEach(wood.mainInformation.magicalUses.indices, id: \.self) { index in
let magicalUse = wood.mainInformation.magicalUses[index]
Text(magicalUse)
}
}
Section(header: Text("Grow Zone")) {
ForEach(wood.mainInformation.growZone.indices, id: \.self) { index in
let zone = wood.mainInformation.growZone[index]
Text(zone)
}
}
Section(header: Text("Grow It Yourself")) {
Text("Water: \(wood.mainInformation.moistureLevel)")
Text("Needs: \(wood.mainInformation.lightLevel)")
if wood.mainInformation.isPerennial {
Text("Perennial")
}
else {
Text("Annual")
}
}
}
}
.navigationTitle(wood.mainInformation.category.rawValue)
.onDisappear {
woodsData.saveWoods()
}
.toolbar {
ToolbarItem {
HStack {
Button(action: {
wood.isFavorite.toggle()
}) {
Image(systemName: wood.isFavorite ? "heart.fill" : "heart")
}
}
}
}
}
}
struct WoodsDetailView_Previews: PreviewProvider {
#State static var wood = Wood.woodTypes[0]
static var previews: some View {
WoodsDetailView(wood: $wood)
}
}
This is my MainTabView.swift file:
import SwiftUI
struct MainTabView: View {
#StateObject var woodData = WoodData()
var body: some View {
TabView {
NavigationView {
List {
WoodsListView(viewStyle: .allCategories(WoodCategory.allCases))
}
}
.tabItem { Label("Main", systemImage: "list.dash")}
NavigationView {
List {
WoodsListView(viewStyle: .favorites)
}
.navigationTitle("Favorites")
}.tabItem { Label("Favorites", systemImage: "heart.fill")}
}
.environmentObject(woodData)
.onAppear {
woodData.loadWoods()
}
.preferredColorScheme(.dark)
}
}
struct MainTabView_Previews: PreviewProvider {
static var previews: some View {
MainTabView()
}
}
This is my WoodListView.swift file:
import SwiftUI
struct WoodsListView: View {
#EnvironmentObject private var woodData: WoodData
let viewStyle: ViewStyle
var body: some View {
ForEach(woods) { wood in
NavigationLink(wood.mainInformation.category.rawValue, destination: WoodsDetailView(wood: binding(for: wood)))
}
}
}
extension WoodsListView {
enum ViewStyle {
case favorites
case singleCategory(WoodCategory)
case allCategories([WoodCategory])
}
private var woods: [Wood] {
switch viewStyle {
case let .singleCategory(category):
return woodData.woods(for: category)
case let .allCategories(category):
return woodData.woods(for: category)
case .favorites:
return woodData.favoriteWoods
}
}
func binding(for wood: Wood) -> Binding<Wood> {
guard let index = woodData.index(of: wood) else {
fatalError("Wood not found")
}
return $woodData.woods[index]
}
}
struct WoodsListView_Previews: PreviewProvider {
static var previews: some View {
WoodsListView(viewStyle: .singleCategory(.ash))
.environmentObject(WoodData())
}
}
Any assistance into why it is not encoding the toggled isFavorite property will be greatly appreciated.
Your problem is that structs are value types in Swift. Essentially this means that the instance of Wood that you have in WoodsDetailView is not the same instance that is in your array in your model (WoodData); It is a copy (Technically, the copy is made as soon as you modify the isFavourite property).
In SwiftUI it is important to maintain separation of responsibilities between the view and the model.
Changing the favourite status of a Wood is something the view should ask the model to do.
This is where you have a second issue; In your detail view you are creating a separate instance of your model; You need to refer to a single instance.
You have a good start; you have put your model instance in the environment where views can access it.
First, change the detail view to remove the binding, refer to the model from the environment and ask the model to do the work:
struct WoodsDetailView: View {
var wood: Wood
#EnvironmentObject private var woodsData: WoodData
var body: some View {
VStack {
List {
Section(header: Text("Description")) {
Text(wood.mainInformation.description)
}
Section(header: Text("Preparation Techniques")) {
ForEach(wood.preparation, id: \.self) { technique in
Text(technique.rawValue)
}
}
Section(header: Text("Edible?")) {
if wood.mainInformation.isEdible {
Text("Edible")
}
else {
Text("Not Edible")
}
}
Section(header: Text("Medicinal Uses")) {
ForEach(wood.mainInformation.medicinalUses, id: \.self) { medicinalUse in
Text(medicinalUse)
}
}
Section(header: Text("Magical Uses")) {
ForEach(wood.mainInformation.magicalUses, id: \.self) { magicalUse in
Text(magicalUse)
}
}
Section(header: Text("Grow Zone")) {
ForEach(wood.mainInformation.growZone, id: \.self) { zone in
Text(zone)
}
}
Section(header: Text("Grow It Yourself")) {
Text("Water: \(wood.mainInformation.moistureLevel)")
Text("Needs: \(wood.mainInformation.lightLevel)")
if wood.mainInformation.isPerennial {
Text("Perennial")
}
else {
Text("Annual")
}
}
}
}
.navigationTitle(wood.mainInformation.category.rawValue)
.onDisappear {
woodsData.saveWoods()
}
.toolbar {
ToolbarItem {
HStack {
Button(action: {
self.woodsData.toggleFavorite(for: wood)
}) {
Image(systemName: wood.isFavorite ? "heart.fill" : "heart")
}
}
}
}
}
}
struct WoodsDetailView_Previews: PreviewProvider {
static var wood = Wood.woodTypes[0]
static var previews: some View {
WoodsDetailView(wood: wood)
}
}
I also got rid of the unnecessary use of indices when listing the properties.
Now, add a toggleFavorite function to your WoodData object:
func toggleFavorite(for wood: Wood) {
guard let index = self.woods.firstIndex(where:{ $0.id == wood.id }) else {
return
}
self.woods[index].isFavorite.toggle()
}
You can also remove the index(of wood:Wood) function (which was really just duplicating Array's firstIndex(where:) function) and the binding(for wood:Wood) function.
Now, not only does your code do what you want, but you have hidden the mechanics of toggling a favorite from the view; It simply asks for the favorite status to be toggled and doesn't need to know what this actually involves.
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.