I'm struggling with an error in my SwiftUI App. In my JSON I have categories(MenuSection) & each category has an array with many items(MenuItem). JSON is valid! I have decoded it properly. My MenuItems are listed for the MenuSection. Then I tried to implement a Favourite Button exactly as shown in Apples tutorials (landmark project). Launching the app loads my list with the MenuItems for each MenuSection. Clicking on the MenuItem crashes the App with the message I have written in the title. Before I added the favourite Button everything works. Why does the app find nil? I force unwrapped a value because I know that there is a value. But it find nil. Can someone please help and explain what the problem is? I have attached a snipped of the .json, the decoder bundle, the structs for the json and the ItemDetail(View) where it find nil.
JSON:
[
{
"id": "9849D1B2-94E8-497D-A901-46EB4D2956D2",
"name": "Breakfast",
"items": [
{
"id": "4C7D5174-A430-489E-BDDE-BD01BAD957FD",
"name": "Article One",
"author": "Joseph",
"level": ["E"],
"isFavorite": true,
"description": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim."
},
{
"id": "01CDACBC-215F-44E0-9D49-2FDC13EF38C6",
"name": "Article Two",
"author": "Joseph",
"level": ["E"],
"isFavorite": false,
"description": "Description for Article 1.2"
},
{
"id": "E69F1198-1D7C-42C7-A917-0DC3D4C67B99",
"name": "Article Three",
"author": "Joseph",
"level": ["E"],
"isFavorite": false,
"description": "Description for Article 1.3"
}
]
},
{
"id": "D8F266BA-7816-4EBC-93F7-F3CBCE2ACE38",
"name": "Lunch",
"items": [
{
"id": "E7142000-15C2-432F-9D75-C3D2323A747B",
"name": "Article 2.1",
"author": "Joseph",
"level": ["M"],
"isFavorite": false,
"description": "Description for Article 2.1"
},
{
"id": "E22FF383-BFA0-4E08-9432-6EF94E505554",
"name": "Article 2.2",
"author": "Joseph",
"level": ["M"],
"isFavorite": false,
"description": "Description for Article 2.2"
},
{
"id": "9978979F-0479-4A49-85B8-776EEF06A560",
"name": "Article 2.3",
"author": "Joseph",
"level": ["M"],
"isFavorite": false,
"description": "Description for Article 2.3"
}
]
}
]
Decoder:
import Foundation
import Combine
final class MenuModel: ObservableObject {
@Published var items = [MenuItem]()
}
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
}
}
Struct:
import SwiftUI
struct MenuSection: Hashable, Codable, Identifiable {
var id = UUID()
var name: String
var items: [MenuItem]
}
struct MenuItem: Hashable, Codable, Equatable, Identifiable {
var id = UUID()
var name: String
var author: String
var level: [String]
var isFavorite: Bool
var description: String
var mainImage: String {
name.replacingOccurrences(of: " ", with: "-").lowercased()
}
var thumbnailImage: String {
"\(mainImage)-thumb"
}
#if DEBUG
static let example = MenuItem(id: UUID(), name: "Article One", author: "Joseph", level: ["E"], isFavorite: true, description: "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem.")
#endif
}
ItemDetail:
import SwiftUI
struct ItemDetail: View {
@EnvironmentObject var menuModel: MenuModel
var item: MenuItem
let menu = Bundle.main.decode([MenuSection].self, from: "menu.json")
let colors: [String: Color] = ["E": .green, "M": .yellow, "D": .red]
var itemIndex: Int! {
menuModel.items.firstIndex(where: { $0.id == item.id })
}
var body: some View {
ScrollView {
VStack(){
<SOME CODE TO SHOW IMAGES AND TEXT>
}
.navigationTitle(item.name)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
FavoriteButton(isSet: $menuModel.items[itemIndex].isFavorite) <here gets nil for 'itemIndex'>
}
}
}
}
func setFavorite() {}
func report() {}
}
struct ItemDetail_Previews: PreviewProvider {
static let menuModel = MenuModel()
static var previews: some View {
ItemDetail(item: MenuModel().items[0])
.environmentObject(menuModel)
}
}
Favorite Button:
import SwiftUI
struct FavoriteButton: View {
@Binding var isSet: Bool
var body: some View {
Button {
isSet.toggle()
} label: {
Label("Toggle Favorite", systemImage: isSet ? "star.fill" : "star")
.labelStyle(.iconOnly)
.foregroundColor(isSet ? .yellow : .gray)
}
}
}
struct FavoriteButton_Previews: PreviewProvider {
static var previews: some View {
FavoriteButton(isSet: .constant(true))
}
}
ContentView:
import SwiftUI
struct ContentView: View {
@EnvironmentObject var menuModel: MenuModel
let menu = Bundle.main.decode([MenuSection].self, from: "menu.json")
//create search string
@State private var searchString = ""
//search result - search in "SearchModel"
var searchResult : [MenuSection] {
if searchString.isEmpty { return menu }
return menu.map { menuSection in
var menuSearch = menuSection
menuSearch.items = menuSection.items.filter { $0.name.lowercased().contains(searchString.lowercased()) }
return menuSearch
}.filter { !$0.items.isEmpty }
}
// VIEW
var body: some View {
NavigationView {
List {
ForEach(searchResult, id:\.self) { section in
Section(header: Text(section.name)) {
ForEach(section.items) { item in
NavigationLink(destination: ItemDetail(item: item)) {
ItemRow(item: item)
}
}
}
}
}
.navigationTitle("Menu")
}
.searchable(text: $searchString)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(MenuModel())
}
}