簡體   English   中英

在 SwiftUI 中,我如何知道選擇器選擇何時更改? 為什么 didSet 不起作用?

[英]In SwiftUI, how do I know when a Picker selection was changed? Why doesn't didSet work?

I have a Picker in a SwiftUI Form , and I'm trying to process the value whenever the picker changes. 我希望能夠在表示當前選定值的變量上使用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(大部分)按預期工作:我可以看到選擇了默認值,點擊選擇器,它會打開一個新視圖,我可以 select 另一個值,然后它返回主窗體並向我顯示新值被選中。 但是,從不調用didSet

我看到了這個問題,但在我的View代碼中添加更多內容而不是在變量更改時僅處理新值(如果可能的話)對我來說似乎很奇怪。 即使會導致更復雜的視圖,使用onReceive會更好嗎? 我的主要問題是:阻止didSet的代碼有什么問題?

我用這個例子來達到這一點。

除了我的常規問題之外,我還有一些關於這個例子的其他問題:

A)我有一個enum和一個Array來表示相同的兩個值,這似乎很奇怪。 有人還可以建議一種更好的方法來構建它以避免這種冗余嗎? 我考慮了一個TransmissionType object,但與enum相比,這似乎有點矯枉過正......也許不是?

B)當點擊選擇器時,帶有選擇器選項的屏幕會滑過,然后兩個選項會向上跳躍一點。 這讓人感覺刺耳、緊張和糟糕的用戶體驗。 我在這里做錯了什么導致糟糕的用戶體驗嗎? 或者它可能是一個 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 (eg 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.

我早些時候開始輸入這個,然后回來發現 LuLuGaGa 已經打敗了我。 :D 但既然我有這個反正......

主要問題:來自Swift 語言指南

“當您為存儲的屬性分配默認值,或在初始化程序中設置其初始值時,該屬性的值會直接設置,而無需調用任何屬性觀察者。”

因此,在構造視圖時,屬性觀察者不會觸發。 但是當@State變量發生變化時,會構造一個新的視圖實例(請記住,視圖是結構或值類型)。 因此, didSet屬性觀察器實際上對@State屬性沒有用處。

您要做的是創建一個符合ObservableObject的 class ,並使用@ObservedObject屬性包裝器從您的視圖中引用它。 因為 class 存在於結構之外,所以您可以在其屬性上設置屬性觀察器,它們會像您期望的那樣觸發。

問題 A:如果你使它符合CaseIterable ,你可以只使用枚舉(見下面的例子)

問題 B:據我所知,這似乎是一個 SwiftUI 錯誤,因為它發生在NavigationView / Form組合內的任何Picker上。 我建議向 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)
                        }
                    }
                }
            }
        }
    }
}

這里有兩個問題。

1)如果標題樣式為“.inline”,則可以解決跳躍問題。

2) OnReceive() 是用組合框架方法替換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.
    }
   }


}

didSet 不會在 @State 上被調用,因為它是一個包裝器 - 您在 state 而不是 state 本身中設置一個值。 重要的問題是你為什么想知道它已經設置好?

您的視圖可以簡化一點:

如果您將枚舉聲明為具有原始類型的字符串,則不需要名稱數組。 如果您將其聲明為 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)
                            }
                    }
                }
            }
        }
    }
}

我看不出奇怪的跳躍從何而來——你是否也在設備上復制了它?

嘿,沒有閱讀全文,但如果您想知道選擇器的更改值是什么。 只需使用.onChange(of: YourPicker) { changedValue in }

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM