SwiftUI选择器的onChange或等效功能?

65

Picker变化时,我希望改变另一个无关的@State变量,但是Picker没有onChanged事件,也不可能在@State中使用didSet。有其他解决方法吗?

9个回答

159

iOS 14或更高版本的部署目标

苹果公司为View提供了内置的onChange扩展,可以像这样使用:

struct MyPicker: View {
    @State private var favoriteColor = 0

    var body: some View {
        Picker(selection: $favoriteColor, label: Text("Color")) {
            Text("Red").tag(0)
            Text("Green").tag(1)
        }
        .onChange(of: favoriteColor) { tag in print("Color tag: \(tag)") }
    }
}

iOS 13或更早版本的部署目标

struct MyPicker: View {
    @State private var favoriteColor = 0

    var body: some View {
        Picker(selection: $favoriteColor.onChange(colorChange), label: Text("Color")) {
            Text("Red").tag(0)
            Text("Green").tag(1)
        }
    }

    func colorChange(_ tag: Int) {
        print("Color tag: \(tag)")
    }
}

使用这个辅助工具

extension Binding {
    func onChange(_ handler: @escaping (Value) -> Void) -> Binding<Value> {
        return Binding(
            get: { self.wrappedValue },
            set: { selection in
                self.wrappedValue = selection
                handler(selection)
        })
    }
}

1
这是一个不错的解决方案,但随着SwiftUI的发展成熟,我期望有一个本地化的解决方案。 - William Grand
这是一个非常出色的解决方案!比.onReceive好多了,因为在我的情况下它会在每次视图渲染时被调用。谢谢。 - ixany
@WilliamGrand 你是对的,iOS 14引入了.onChange - Jeremy
iOS 14的解决方案完美地运行了! - dbDev
谢谢,解决了我的问题,帮助我更好地理解了 .tag() 和 .onChange()。我的问题是,我想让我的选择器绑定到视图模型中的一个 Int。.tag() 和 .onChange() 帮助我将 Int 与 Picker 的选择同步更新。在 .onChange 中执行 { tag in vm.foo = tag }。 - Tim Sonner
显示剩余2条评论

33

首先,完全感谢ccwasden提供的最佳答案。我稍微修改了一下使其适用于我的情况,因此我回答这个问题是希望其他人也能从中受益。

以下是我最终使用的代码(在iOS 14 GM和Xcode 12 GM上进行了测试)

struct SwiftUIView: View {
    @State private var selection = 0

    var body: some View {
        Picker(selection: $selection, label: Text("Some Label")) {
            ForEach(0 ..< 5) {
                Text("Number \($0)") }
        }.onChange(of: selection) { _ in
            print(selection)
        }
        
    }
}

加上"_ in"是我需要的。没有它,我会得到"Cannot convert value of type 'Int' to expected argument type '()'"错误。


5
应该接受这个解决方案,简单明了,直戳要害。 - Joe Scotto
简单而优雅的解决方案 - Nizami
这是我唯一有效的解决方案。 - Vladimir Despotovic
这是一种“修改”吗?它使用extension Binding ...吗? - GoZoner

15

我认为这是更简单的解决方案:

@State private var pickerIndex = 0
var yourData = ["Item 1", "Item 2", "Item 3"]

// USE this if needed to notify parent
@Binding var notifyParentOnChangeIndex: Int    

var body: some View {

   let pi = Binding<Int>(get: {

            return self.pickerIndex

        }, set: {

            self.pickerIndex = $0

            // TODO: DO YOUR STUFF HERE
            // TODO: DO YOUR STUFF HERE
            // TODO: DO YOUR STUFF HERE

            // USE this if needed to notify parent
            self.notifyParentOnChangeIndex = $0

        })

   return VStack{

            Picker(selection: pi, label: Text("Yolo")) {
                ForEach(self.yourData.indices) {
                    Text(self.yourData[$0])
                }
            }
            .pickerStyle(WheelPickerStyle())
            .padding()

   }

}

9

我知道这是一篇一年前的帖子,但我认为这个解决方案可能会帮助到其他需要解决问题的人。希望它能帮到其他人。

import Foundation
import SwiftUI

struct MeasurementUnitView: View {
    
    @State var selectedIndex = unitTypes.firstIndex(of: UserDefaults.standard.string(forKey: "Unit")!)!
    var userSettings: UserSettings
    
    var body: some View {
        
        VStack {
            Spacer(minLength: 15)
            Form {
                Section {
                    Picker(selection: self.$selectedIndex, label: Text("Current UnitType")) {
                        
                        ForEach(0..<unitTypes.count, id: \.self) {
                            Text(unitTypes[$0])
                        }
                    }.onReceive([self.selectedIndex].publisher.first()) { (value) in
                        self.savePick()
                    }
                    .navigationBarTitle("Change Unit Type", displayMode: .inline)
                }
            }
        }
    }
    
    func savePick() {
        
        if (userSettings.unit != unitTypes[selectedIndex]) {
            userSettings.unit = unitTypes[selectedIndex]
        }
    }
}



这实际上为我解决了另一个问题。我发现onReceive有点过于敏感,当它触发对@State变量的间接更改时,会进入无限循环。通过添加if value == oldValue { return },它就不再这样做了。 - Manngo
你是怎么获取 oldValue 的? - richy
很好,但每次我点击选择器时,它都会返回默认的第一个选择器状态。在此过程中,我丢失了最后一个选择器值。我该如何修改.publisher.first()以避免这个问题? - Mert Köksal
.onChange(of: self.selectedStatusIndex) { newValue in self.editItem(account: account) } 当self.selectedStatusIndex发生改变时,执行以下操作:{newValue in self.editItem(account: account) } - Mert Köksal

8

我使用了一个分段选择器,并有类似的要求。尝试了一些方法后,我只是使用了一个同时具有ObservableObjectPublisherPassthroughSubject publisher的对象作为选择。这让我满足了SwiftUI,并且使用onReceive()我还可以做其他的事情。

// Selector for the base and radix
Picker("Radix", selection: $base.value) {
    Text("Dec").tag(10)
    Text("Hex").tag(16)
    Text("Oct").tag(8)
}
.pickerStyle(SegmentedPickerStyle())
// receiver for changes in base
.onReceive(base.publisher, perform: { self.setRadices(base: $0) })

base拥有一个名为objectWillChange和一个名为PassthroughSubject<Int, Never>的发布者,幸运地被称为publisher。

class Observable<T>: ObservableObject, Identifiable {
    let id = UUID()
    let objectWillChange = ObservableObjectPublisher()
    let publisher = PassthroughSubject<T, Never>()
    var value: T {
        willSet { objectWillChange.send() }
        didSet { publisher.send(value) }
    }

    init(_ initValue: T) { self.value = initValue }
}

typealias ObservableInt = Observable<Int>

定义objectWillChange并不是必需的,但当我写下这个时,我喜欢提醒自己它的存在。


6

对于那些需要同时支持iOS 13和14的人,我添加了一个可以适用于两个版本的扩展。不要忘记导入Combine。

Extension View {
    @ViewBuilder func onChangeBackwardsCompatible<T: Equatable>(of value: T, perform completion: @escaping (T) -> Void) -> some View {
        if #available(iOS 14.0, *) {
            self.onChange(of: value, perform: completion)
        } else {
            self.onReceive([value].publisher.first()) { (value) in
                completion(value)
            }
        }
    }
}

用法:

Picker(selection: $selectedIndex, label: Text("Color")) {
    Text("Red").tag(0)
    Text("Blue").tag(1)
}.onChangeBackwardsCompatible(of: selectedIndex) { (newIndex) in
    print("Do something with \(newIndex)")
}

重要提示:如果您在完成块中更改观察对象内的已发布属性,则此解决方案将在iOS 13中引起无限循环。不过,只需添加一个检查即可轻松解决,类似于以下内容:

.onChangeBackwardsCompatible(of: showSheet, perform: { (shouldShowSheet) in
   if shouldShowSheet {
      self.router.currentSheet = .chosenSheet
      showSheet = false
   }
})


1
如果值是布尔值,那么iOS 13的回退方案对我来说不起作用。 - Dree

4

SwiftUI 1&2

使用 onReceiveJust

import Combine
import SwiftUI

struct ContentView: View {
    @State private var selection = 0

    var body: some View {
        Picker("Some Label", selection: $selection) {
            ForEach(0 ..< 5, id: \.self) {
                Text("Number \($0)")
            }
        }
        .onReceive(Just(selection)) {
            print("Selected: \($0)")
        }
    }
}

3

iOS 14和带有关系的CoreData实体

在尝试绑定到CoreData实体时,我遇到了这个问题,并发现以下方法可行:

Picker("Level", selection: $contact.level) {
                        ForEach(levels) { (level: Level?) in
                            HStack {
                                Circle().fill(Color.green)
                                    .frame(width: 8, height: 8)
                                Text("\(level?.name ?? "Unassigned")")
                            }
                            .tag(level)
                        }
                    }
.onChange(of: contact.level) { _ in savecontact() }

其中“contact”是与“level”有关系的实体。

Contact 类是一个 @ObservedObject var contact: Contact

saveContact 是一个 do-catch 函数,用于尝试保存 viewContext.save() ...


2
非常重要的问题:我们必须向Picker项目视图(在ForEach内部)的“tag”修饰符传递一些内容,以便让它“识别”项目并触发选择更改事件。我们传递的值将返回到Picker的“selection”的绑定变量中。
例如:
Picker(selection: $selected, label: Text("")){
            
            ForEach(data){item in //data's item type must conform Identifiable
                
                HStack{
                    
                    //item view
                    
                
                }
                .tag(item.property)
                
            }
            
        }
        
        .onChange(of: selected, perform: { value in
            
            //handle value of selected here (selected = item.property when user change selection)
            
        })

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