ios - SwiftUI - 在展开可选值时意外发现 nil
问题描述
我正在为我的 SwiftUI 应用程序中的错误而苦苦挣扎。在我的 JSON 中,我有类别(MenuSection)并且每个类别都有一个包含许多项目(MenuItem)的数组。JSON 是有效的!我已经正确解码了。我的 MenuItems 为 MenuSection 列出。然后我尝试完全按照 Apples 教程(里程碑项目)中所示的方式实现一个收藏按钮。启动应用程序会加载我的列表,其中包含每个 MenuSection 的 MenuItems。单击 MenuItem 会使应用程序崩溃,并显示我在标题中写的消息。在我添加最喜欢的 Button 之前,一切正常。为什么应用程序会发现 nil?我强制展开一个值,因为我知道有一个值。但它发现为零。有人可以帮忙解释一下问题是什么吗?我附上了 .json 的片段、解码器包、json 的结构和它找到 nil 的 ItemDetail(View)。
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"
}
]
}
]
解码器:
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
}
}
结构:
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
}
项目详情:
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)
}
}
收藏按钮:
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))
}
}
内容视图:
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())
}
}
解决方案
问题在于var itemIndex
in ItemDetail
。ItemDetail
整体是有问题的。你在其中重新创造你menu
的,这是一个问题,因为你现在没有单一的事实来源。您正在比较两个不同的菜单。每当您想编辑数据的一部分时,您应该将该部分作为绑定发送到编辑。问题是您没有要使用的属性包装器的变量。
另一个问题是架构问题。你有一个MenuModel
作为你声称的视图模型,但它只是一个[MenuItem]
. 而且您实际上并没有使用它,而是实例化[MenuSection]
并使用它。那应该是你的模型。但是因为你还没有把它变成一个类,它会引导你在你的视图中执行模型逻辑,而不是在模型中。
所以,我冒昧地重新安排了一些事情。首先,您的模型:
final class MenuModel: ObservableObject {
@Published var sections: [MenuSection]
init() {
sections = Bundle.main.decode([MenuSection].self, from: "menu.json")
}
}
接下来ItemDetail
现在使用绑定:
struct ItemDetail: View {
@Binding var item: MenuItem
let colors: [String: Color] = ["E": .green, "M": .yellow, "D": .red]
var body: some View {
ScrollView {
VStack(){
Text("Hello, World!")
}
.navigationTitle(item.name)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
FavoriteButton(isSet: $item.isFavorite) //<here gets nil for 'itemIndex'>
}
}
}
}
func setFavorite() {}
func report() {}
}
所以ContentView
现在必须提供一个:
struct ContentView: View {
@EnvironmentObject var menu: MenuModel
//create search string
@State private var searchString = ""
//search result - search in "SearchModel"
var searchResult : [MenuSection] {
if searchString.isEmpty { return menu.sections }
return menu.sections.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 {
//Because we need to drill down, we need the index. But ForEach by indicies doesn't play well
//with List( rearranging, etc.) so we zip the array items with their indicies and make them an array
//again, and id them by the items, not the index. This allows us to use both the item and the index later.
ForEach(Array(zip(searchResult, searchResult.indices)), id:\.0) { section, sectionIndex in
Section(header: Text(section.name)) {
ForEach(Array(zip(section.items, section.items.indices)), id: \.0) { item, index in
NavigationLink(destination: ItemDetail(item: $menu.sections[sectionIndex].items[index])) {
Text(item.name)
}
}
}
}
}
.navigationTitle("Menu")
}
.searchable(text: $searchString)
}
}
您现在有了一个可以使用的视图模型,它是您唯一的事实来源。顺便说一句,您FavoriteButton
正在导致立即驳回您的ItemDetail
观点。您需要将其作为一个单独的问题提出。
ContentView
最后,如果你有收藏夹按钮,整个ItemView
管道会更简单ContentView
。然后,您只需将非绑定发送MenuItem
到ItemDetail
.
编辑:我忘了把@Main放在:
@main
struct UnexpectedNilAppApp: App {
let menu = MenuModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(menu)
}
}
}
有了它,您只需调用ContentView()
预览提供程序即可。
因为ItemDetail
我使用了这样的中间结构:
struct itemDetailPreviewIntermediary: View {
@State var menu = MenuModel()
var body: some View {
NavigationView {
ItemDetail(item: $menu.sections[0].items[0])
}
}
}
struct ItemDetail_Previews: PreviewProvider {
static var previews: some View {
itemDetailPreviewIntermediary()
}
}
使用绑定和预览提供程序通过另一个声明@State
变量传递给视图的结构要容易得多。
推荐阅读
- java - 如何在java中以并行数组加载文件
- javascript - UWP WebView:在没有源的情况下最小化窗口时播放音频
- python - 在 Pygame 中,图像不会出现在屏幕上
- spring-boot - Katalon disqus textArea 对象无法识别
- javascript - 如何在 Vigenere Cipher 中维护大小写并忽略空格
- java - Vert.x POST 回调未触发
- cron - Ubuntu EasyEngine 条件执行
- excel - 在 Excel 宏上实时运行函数
- visual-studio - 双击 Visual Studio 2019 时如何禁用编辑项目?
- c# - 从继承 networkBehviour 的类继承