【问题标题】:SwiftUI, why a part of the view did not refresh? @State variable not updatingSwiftUI,为什么部分视图没有刷新? @State 变量未更新
【发布时间】:2020-04-08 01:15:04
【问题描述】:

这是我在 SwiftUI 代码中遇到的问题的再现。假设有一个设备列表,并且每个我都有一些连接信息(启用或未启用)。

使用 SwiftUI 中的经典列表,每个设备都会显示一个 DeviceView。 为方便起见,我想在顶部添加几个箭头来在设备之间切换,而无需每次都返回列表(理解,就像 iOS 中的邮件应用程序)。 因此,我没有从列表中调用 DeviceView,而是在调用存储当前设备信息的 MotherDeviceView 之前。当我点击顶部的箭头时, MotherDeviceView 再次调用 DeviceView 并更新视图。

struct Device: Identifiable {
    var id = Int()
    var deviceName = String()

    var isWifiOn = Bool()
    var isCellularOn = Bool()
    var isBluetoothOn = Bool()
}

let devices: [Device] = [
    Device(id: 0, deviceName: "iPhone", isWifiOn: true, isCellularOn: false, isBluetoothOn: true),
    Device(id: 1, deviceName: "iPod", isWifiOn: false, isCellularOn: false, isBluetoothOn: true),
    Device(id: 2, deviceName: "iPad", isWifiOn: false, isCellularOn: true, isBluetoothOn: false)
]

// main view, with list of devices
struct DeviceTab: View {
    var body: some View {
        NavigationView {
            List(devices) { device in
                NavigationLink(destination: MotherDeviceView(device: device)){
                    Text(device.deviceName)
                }
            }.navigationBarTitle(Text("Devices"))
        }
    }
}

// the list call this view and pass the device to DeviceView.
// Using this view is possible to go to the other device using the arrows on the top
// without returning to the list
struct MotherDeviceView: View {
    @State var device: Device

    var body: some View {
        VStack{
            DeviceView(device: $device)
        }
        .navigationBarItems(trailing: arrows)
    }

    private var arrows: some View {
        HStack{
            Button(action: { self.previous() },
                    label: { Image(systemName: "chevron.up") } )
                .padding(.horizontal, 10)
            Button(action: { self.next() },
                   label: { Image(systemName: "chevron.down") } )
        }
    }

    func previous() {
        if device.id == 0 { return }
        device = devices[device.id - 1]
    }

    func next() {
        if device.id == 2 { return }
        device = devices[device.id + 1]
    }

}

// DeviceView
struct DeviceView: View {
    @Binding var device: Device

    var body: some View {
        VStack{
            Spacer()
            Text("This is the device number: " + String(device.id))
            Spacer()
            ToggleSection(dev: $device)
            Spacer()
        }.navigationBarTitle(device.deviceName)
    }
}

// Toggle part of DeviceView
struct ToggleSection: View {
    @Binding var dev: Device

    @State var toggleOn: [Bool] = [false, false, false]

    var body: some View {

        VStack{

            Text(dev.deviceName)
                .fontWeight(.bold)

            Toggle(isOn: $toggleOn[0], label: { Text("Wifi") } )
                .padding()

            Toggle(isOn: $toggleOn[1], label: { Text("Cellular") } )
                .padding()

            Toggle(isOn: $toggleOn[2], label: { Text("Bluetooth") } )
                .padding()

        }
        .onAppear{ self.initialConfigToggle() }
        // with onAppear is okay, but when I use the top arrows, this section does not update
    }

    private func initialConfigToggle() {
        self.toggleOn[0] = self.dev.isWifiOn
        self.toggleOn[1] = self.dev.isCellularOn
        self.toggleOn[2] = self.dev.isBluetoothOn
    }

}

但是 DeviceView 中有一个部分 ToggleSection 不会更新,我不知道如何解决这个问题。 也许强制更新?但我认为这不是正确的答案(但我什至不知道如何强制更新)。

我认为如果在 ToggleSection 中有 @Binding 而不是 @State 可能是正确的答案,但这不起作用。 这个例子可以工作(除了未更新的切换)

提前致谢!

【问题讨论】:

    标签: swift swiftui swiftui-list swiftui-navigationlink


    【解决方案1】:

    这是一种可能的方法,它为设备存储添加了显式视图模型,并加入了所有视图以在该存储中进行精确修改(避免应对设备值)。

    使用 Xcode 11.4 / iOS 13.4 测试

    修改代码:

    import SwiftUI
    import Combine
    
    struct Device: Identifiable {
        var id = Int()
        var deviceName = String()
    
        var isWifiOn = Bool()
        var isCellularOn = Bool()
        var isBluetoothOn = Bool()
    }
    
    class DeviceStorage: ObservableObject {
        @Published var devices: [Device] = [
            Device(id: 0, deviceName: "iPhone", isWifiOn: true, isCellularOn: false, isBluetoothOn: true),
            Device(id: 1, deviceName: "iPod", isWifiOn: false, isCellularOn: false, isBluetoothOn: true),
            Device(id: 2, deviceName: "iPad", isWifiOn: false, isCellularOn: true, isBluetoothOn: false)
        ]
    }
    
    // main view, with list of devices
    struct DeviceTab: View {
        @ObservedObject var vm = DeviceStorage() // for demo, can be injected via init
        var body: some View {
            NavigationView {
                List(Array(vm.devices.enumerated()), id: \.element.id) { i, device in
                        NavigationLink(destination: MotherDeviceView(vm: self.vm, selectedDevice: i)) {
                            Text(device.deviceName)
                    }
                }.navigationBarTitle(Text("Devices"))
            }
        }
    }
    
    struct MotherDeviceView: View {
        @ObservedObject var vm: DeviceStorage // reference type, so have same
        @State private var selected: Int      // selection index
    
        init(vm: DeviceStorage, selectedDevice: Int) {
            self.vm = vm
            self._selected = State<Int>(initialValue: selectedDevice)
        }
    
        var body: some View {
            VStack{
                DeviceView(device: $vm.devices[selected]) // bind to selected
            }
            .navigationBarItems(trailing: arrows)
        }
    
        private var arrows: some View {
            HStack{
                Button(action: { self.previous() },
                        label: { Image(systemName: "chevron.up") } )
                    .padding(.horizontal, 10)
                Button(action: { self.next() },
                       label: { Image(systemName: "chevron.down") } )
            }
        }
    
        func previous() {
            if vm.devices[selected].id == 0 { return }
            selected -= 1                              // change selection
        }
    
        func next() {
            if vm.devices[selected].id == 2 { return }
            selected += 1                              // change selection
        }
    
    }
    
    // DeviceView
    struct DeviceView: View {
        @Binding var device: Device
    
        var body: some View {
            VStack{
                Spacer()
                Text("This is the device number: " + String(device.id))
                Spacer()
                ToggleSection(dev: $device)
                Spacer()
            }.navigationBarTitle(device.deviceName)
        }
    }
    
    // Toggle part of DeviceView
    struct ToggleSection: View {
        @Binding var dev: Device
    
        var body: some View {
    
            VStack{
    
                Text(dev.deviceName)
                    .fontWeight(.bold)
    
                // all below bound directly to model !!!
    
                Toggle(isOn: $dev.isWifiOn, label: { Text("Wifi") } )
                    .padding()
    
                Toggle(isOn: $dev.isCellularOn, label: { Text("Cellular") } )
                    .padding()
    
                Toggle(isOn: $dev.isBluetoothOn, label: { Text("Bluetooth") } )
                    .padding()
    
            }
        }
    }
    

    【讨论】:

    • 这是个好主意!但是在 ToggleSection 中,我需要计算 Toggle 的“isOn”状态。在这里我没有报告这一点,但实际上在 ToggleSection 中有另一个 @State var toggleDisabled: [Bool] 计算是否可以启用或禁用切换(用户无法与之交互)。使用这种方法无法将计算绑定传递给 Toggle。
    • 好的,我解决了(例如使用简单的计算值)Toggle(isOn: Binding( get: { return !self.dev.isCellularOn}, set: { (newvalue) in }), label: {Text("Cellular")} )。谢谢你的回答!
    猜你喜欢
    • 2021-08-06
    • 1970-01-01
    • 1970-01-01
    • 2020-04-10
    • 2021-09-23
    • 1970-01-01
    • 1970-01-01
    • 2020-09-05
    • 2015-06-13
    相关资源
    最近更新 更多