SwiftUI @State 数组在 init() 中添加元素时未更新值

4

我是一名初学者程序员,正在使用运行在 iMac Big Sur 11.2上的 Xcode 12.4,我的项目类型是“多平台应用”。

我正在尝试创建一个图像数组以在网格中移动。我将“My Image.png”复制到了 Xcode 项目中,并成功使用了它,但无论我如何尝试将它放入数组中,它都返回一个空数组。我在另一台电脑上尝试过,似乎可以工作。Xcode 中可能有什么影响吗?(还是我在另一台机器上幻想它可以工作了? :-p)

代码:

@State var stringURL: URL = Bundle.main.url(forResource: "My Image", withExtension: "png")!
Image(nsImage: NSImage(byReferencing: stringURL))

@State var arrayURLs = [URL]()
arrayURLs.append(Bundle.main.url(forResource: "My Image", withExtension: "png")!)
Image(nsImage: NSImage(byReferencing: arrayURLs[0]))

第一和第二行可以正常运行,但最后三行在同一个应用程序中失败。我收到了“索引超出范围”的运行时错误,我认为这是因为bundle调用返回了nil。

上下文基本相同。我刚开始这个项目,从未超过前几行......

import SwiftUI

struct ContentView: View {
@State var pieceImageURLs = [URL]()


init() {
    self.pieceImageURLs.append(Bundle.main.url(forResource: "Red Warrior", withExtension: "png")!)
}

var body: some View {
    HStack {
         Image(nsImage: NSImage(byReferencing: pieceImageURLs[0]))
    }
}}



    import SwiftUI

struct ContentView: View {
    @State var stringURL: URL = Bundle.main.url(forResource: "My Image", withExtension: "png")!
    @State var arrayURLs = [URL]()
init() {
        stringURL = Bundle.main.url(forResource: "My Image", withExtension: "png")!
        arrayURLs.append(Bundle.main.url(forResource: "My Image", withExtension: "png")!)
    }

    var body: some View {
        HStack {
            Image(nsImage: NSImage(byReferencing: stringURL))
            Image(nsImage: NSImage(byReferencing: arrayURLs[0]))
        }
        Button(action: {
            arrayURLs.append(Bundle.main.url(forResource: "My Image", withExtension: "png")!)
            Image(nsImage: NSImage(byReferencing: arrayURLs[0]))
        }) {
            Text("add image")
        }
    }
}

或者像这样全部一起,无论是图像行还是数组,都会失败。


你能在代码上下文中展示给我们看吗?比如说,在真实的文件中,这些行并不会紧挨着放置,因为你无法在属性和逻辑旁边同时声明它们。这样可能有助于排除故障。 - jnpdx
1个回答

4

这是一个有趣的问题,我自己也没有预料到这种行为。看起来SwiftUI在视图的init阶段不喜欢你设置@State变量。有几种方法可以解决这个问题。

选项1

你不能设置新值,但你可以在init中显式地初始化它作为State

struct ContentView: View {
    @State var pieceImageURLs = [URL]()
    
    init() {
        if let url = Bundle.main.url(forResource: "Frame 1-2", withExtension: "png") {
            _pieceImageURLs = State(initialValue: [url]) // <--- Here
        }
    }
    
    var body: some View {
        HStack {
            if let imgUrl = pieceImageURLs.first, let nsImage = NSImage(byReferencing: imgUrl) {
                Image(nsImage: nsImage)
            } else {
                Text("HI")
            }
        }
        .frame(minWidth: 300, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
    }
}

选项2

不要使用@State,因为它在您的示例中是静态的(至少在这个例子里) - 您只是在init中设置一个值。

struct ContentView: View {
    var pieceImageURLs = [URL]()  // <--- Here
    
    init() {
        if let url = Bundle.main.url(forResource: "Frame 1-2", withExtension: "png") {
            pieceImageURLs.append(url)
        }
    }
    
    var body: some View {
        HStack {
            if let imgUrl = pieceImageURLs.first, let nsImage = NSImage(byReferencing: imgUrl) {
                Image(nsImage: nsImage)
            } else {
                Text("HI")
            }
        }
        .frame(minWidth: 300, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
    }
}

选项三

使用state,但在onAppear中设置:

struct ContentView: View {
    @State var pieceImageURLs = [URL]()
    
    var body: some View {
        HStack {
            if let imgUrl = pieceImageURLs.first, let nsImage = NSImage(byReferencing: imgUrl) {
                Image(nsImage: nsImage)
            } else {
                Text("HI")
            }
        }
        .onAppear {  // <--- Here
            if let url = Bundle.main.url(forResource: "Frame 1-2", withExtension: "png") {
                pieceImageURLs.append(url)
            }
        }
        .frame(minWidth: 300, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
    }
}

选项4

在视图模型(ObservableObject)中进行初始化

class ViewModel : ObservableObject {  // <--- Here
    @Published var pieceImageURLs = [URL]()
    
    init() {
        if let url = Bundle.main.url(forResource: "Frame 1-2", withExtension: "png") {
            pieceImageURLs.append(url)
        }
    }
}

struct ContentView: View {
    @ObservedObject var viewModel = ViewModel()  // <--- Here
    
    var body: some View {
        HStack {
            if let imgUrl = viewModel.pieceImageURLs.first, let nsImage = NSImage(byReferencing: imgUrl) {
                Image(nsImage: nsImage)
            } else {
                Text("HI")
            }
        }
        .frame(minWidth: 300, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
    }
}

非常感谢。要是没有你,我可能永远都想不出来。我正在尝试制作一个类似于国际象棋的棋盘游戏。有什么教程或样例可以推荐吗? - Douglas Curran
@DouglasCurran -- 如果这个回答对您有帮助并解决了您的问题,请考虑接受此答案。我没有针对这种特定情况建议的具体教程。总的来说,我会始终建议尽可能多地阅读 https://www.hackingwithswift.com - jnpdx
谢谢,我一直在通过Hacking with Swift进行深入学习。 - Douglas Curran
选项1和2会产生编译错误:“逃逸闭包捕获可变的'self'参数”,这是一个架构问题,不要在视图渲染周期期间改变视图状态,而是在渲染周期外更改视图的数据模型,并让视图的重新渲染反映该更改。这就是为什么选项3和4更受欢迎,无论哪种方式,都意味着在第一次视图渲染时数据将缺失/为空/或部分缺失,您必须处理它(添加一些if语句来检查数组是否为空或添加一些空数据,一旦对象初始化后就会运行)。 - Shaybc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接