在SwiftUI中,如何知道选择器选择何时更改?为什么Set无法运作?

时间:2019-11-05 22:08:59

标签: ios swift swiftui picker

我在SwiftUI Picker中有一个Form,并且每当选择器发生更改时,我都试图处理该值。我希望能够使用didSet在代表当前所选值的变量上执行此操作。

import SwiftUI

struct ContentView: View {

    enum TransmissionType: Int {
        case automatic
        case manual
    }

    @State private var selectedTransmissionType: Int = TransmissionType.automatic.rawValue {
        didSet {
            print("selection changed to \(selectedTransmissionType)")
        }
    }

    private let transmissionTypes: [String] = ["Automatic", "Manual"]

    var body: some View {
        NavigationView {
            Form {
                Section {
                    Picker(selection: $selectedTransmissionType,
                           label: Text("Transmission Type")) {
                        ForEach(0 ..< transmissionTypes.count) {
                            Text(self.transmissionTypes[$0])
                        }
                    }
                }
            }
        }
    }
}

Picker UI(通常)按预期工作:我可以看到已选择默认值,点击选择器,打开一个新视图,我可以选择其他值,然后返回到并向我显示新值已被选择。但是,didSet永远不会被调用。

我看到了this question,但对我来说,向变量View添加更多内容,而不是仅仅在变量更改时处理新值,即使有可能,这似乎很奇怪。使用onReceive会更好,尽管会导致更复杂的视图?我的主要问题是:我的代码有什么问题,可防止调用didSet

我使用this example到了这一点。

除了我的常规问题外,我还有其他一些有关此示例的信息:

A)看起来很奇怪,我有一个enum和一个Array来代表相同的两个值。有人还能建议一种更好的方法来构造它以避免这种冗余吗?我考虑过一个TransmissionType对象,但与enum相比似乎有点过分了……也许不是吗?

B)当点击选择器时,带有选择器选项的屏幕会滑过,然后两个选项会略微跳起来。这让人感到震撼,跳动,并带来糟糕的用户体验。我在这里做错了什么导致UX错误吗?还是可能是SwiftUI错误?每次更改选择器时都会出现此错误:

[TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes. Make a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch this in the debugger and see what caused this to occur, so you can avoid this action altogether if possible, or defer it until the table view has been added to a window.

3 个答案:

答案 0 :(得分:1)

didSet不会在@State上调用,因为它是包装器-您正在状态内设置一个值,而不是状态本身。他的重要问题是,为什么您想知道它已设置好?

您的视图可以稍微简化一下:

如果您将枚举声明为具有String的原始类型,则不需要名称数组。如果将其声明为CaseIterable,则可以通过调用Array(TransmissionType.allCases)获得所有案例。如果它也被声明为“可识别”,您将能够将所有情况直接传递到ForEach。接下来,您需要将rawValue传递到文本中,并记住将标签放在文本上,以便可以进行选择:

struct ContentView: View {

    enum TransmissionType: String, CaseIterable, Identifiable {

        case automatic
        case manual

        var id: String {
            return self.rawValue
        }
    }

    @State private var selectedTransmissionType = TransmissionType.automatic

    var body: some View {
        NavigationView {
            Form {
                Section {
                    Picker(selection: $selectedTransmissionType,
                           label: Text("Transmission Type")) {
                            ForEach(Array(TransmissionType.allCases)) {
                                Text($0.rawValue).tag($0)
                            }
                    }
                }
            }
        }
    }
}

我看不到奇怪的跳跃来自何处-您是否也在设备上复制了它?

答案 1 :(得分:1)

这里有两个问题。

1)如果标题样式为“ .inline”,则可以解决跳转问题。

2)OnReceive()是用CombineUI方法替换didSet请求的最简单方法之一,后者是SwiftUI的核心技术。

struct ContentView: View {



enum TransmissionType: Int {
    case automatic
    case manual
}

 @State private var selectedTransmissionType: Int =   TransmissionType.automatic.rawValue {
    didSet {
        print("selection changed to \(selectedTransmissionType)")
    }
}

private let transmissionTypes: [String] = ["Automatic", "Manual"]

var body: some View {
    NavigationView {
        Form {
            Section {
                Picker(selection: $selectedTransmissionType,
                       label: Text("Transmission Type")) {
                    ForEach(0 ..< transmissionTypes.count) {
                        Text(self.transmissionTypes[$0])
                    }
                }
            }
        }.navigationBarTitle("Title", displayMode: .inline) // this solves jumping and all warnings.
    }.onReceive(Just(selectedTransmissionType)) { value in
    print(value) // Just one step can monitor the @state value.
    }
   }


}

答案 2 :(得分:1)

我早些时候开始打这个,然后回去发现LuLuGaGa击败了我。 :D但是既然我有这个...

主要问题:来自Swift Language Guide

  

”当您为存储的属性分配默认值或设置其默认值时   初始化程序中的初始值,该属性的值已设置   直接调用,而无需调用任何属性观察器。”

因此,构造视图时,属性观察器将不会触发。但是,当@State变量更改时,将构造视图的新实例(请记住,视图是结构或值类型)。因此,实际上,didSet属性观察者对@State属性没有用。

您要做的是创建一个符合ObservableObject的类,并使用@ObservedObject属性包装器从您的视图中引用它。由于该类存在于结构外部,因此您可以在其属性上设置属性观察器,它们会像您期望的那样触发。

问题A:如果您使它符合CaseIterable(请参见下面的示例),则只能使用该枚举。

问题B:,这似乎是SwiftUI的错误,因为Picker / NavigationView组合中的任何Form我知道了我建议reporting it to Apple

这是删除枚举和数组冗余并将选择保存在UserDefaults中的方法:

extension ContentView {
    // CaseIterable lets us use .allCases property in ForEach
    enum TransmissionType: String, CaseIterable, Identifiable, CustomStringConvertible {
        case automatic
        case manual

        // This lets us omit 'id' parameter in ForEach
        var id: TransmissionType {
            self
        }

        // This just capitalizes the first letter for prettier printing
        var description: String {
            rawValue.prefix(1).uppercased() + rawValue.dropFirst()
        }
    }

    class SelectionModel: ObservableObject {
        // Save selected type to UserDefaults on change
        @Published var selectedTransmissionType: TransmissionType {
            didSet {
                UserDefaults.standard.set(selectedTransmissionType.rawValue, forKey: "TransmissionType")
            }
        }

        // Load selected type from UserDefaults on initialization
        init() {
            if let rawValue = UserDefaults.standard.string(forKey: "TransmissionType") {
                if let transmissionType = TransmissionType(rawValue: rawValue) {
                    self.selectedTransmissionType = transmissionType
                    return
                }
            }
            // Couldn't load from UserDefaults
            self.selectedTransmissionType = .automatic
        }
    }
}

然后您的视图就像

struct ContentView: View {
    @ObservedObject var model = SelectionModel()

    var body: some View {
        NavigationView {
            Form {
                Section {
                    Picker(selection: $model.selectedTransmissionType, label: Text("Transmission Type")) {
                        ForEach(TransmissionType.allCases) { type in
                            Text(type.description)
                        }
                    }
                }
            }
        }
    }
}