【问题标题】:SwiftUI Picker onChange or equivalent?SwiftUI Picker onChange 还是等效的?
【发布时间】:2019-12-22 10:29:13
【问题描述】:

Picker 发生更改时,我想更改另一个不相关的@State 变量,但没有onChanged 并且无法在选择器@State 上放置didSet。有没有其他方法可以解决这个问题?

【问题讨论】:

    标签: swiftui


    【解决方案1】:

    这是我刚刚决定使用的... (部署目标 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)
                Text("Blue").tag(2)
            }
        }
    
        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)
            })
        }
    }
    

    编辑:

    如果您的部署目标设置为 iOS 14 或更高版本——Apple 为 View 提供了内置的 onChange 扩展,可以像这样使用(感谢 @Jeremy) :

    Picker(selection: $favoriteColor, label: Text("Color")) {
        // ..
    }
    .onChange(of: favoriteColor) { tag in print("Color tag: \(tag)") }
    

    【讨论】:

    • 这是一个很好的解决方案,但是随着 SwiftUI 的成熟,我希望有一个原生的解决方案。
    • 这是一个绝妙的解决方案!比.onReceive 好得多,因为在我的情况下每次渲染视图时都会调用它。谢谢。
    • @WilliamGrand 你说得对,iOS 14 引入了.onChange
    • 应该提到tag在这里很重要,否则onChange修饰符不会在后一种情况下被调用。虽然我没有尝试过第一个
    • iOS 14 解决方案完美运行!
    【解决方案2】:

    首先,感谢 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”是我所需要的。没有它,我收到错误“无法将'Int'类型的值转换为预期的参数类型'()'”

    【讨论】:

    • 这应该是公认的解决方案,简单直接。
    • 简单优雅的解决方案
    【解决方案3】:

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

    @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()
    
       }
    
    }
    

    【讨论】:

      【解决方案4】:

      我知道这是一年前的帖子,但我认为此解决方案可能会帮助其他需要解决方案的人停下来访问。希望对其他人有所帮助。

      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 },它就不用管它了。
      • 你是怎么得到oldValue的?
      • 这很好,但每次我点击选择器时,它都会获得默认的第一个选择器状态。在此过程中,我丢失了最后一个选择器值。如何将 .publisher.first() 修改为其他内容以避免此问题。
      • .onChange(of: self.selectedStatusIndex) { newValue in self.editItem(account: account) }
      【解决方案5】:

      我使用分段选择器并且有类似的要求。在尝试了一些事情之后,我只使用了一个对象,该对象同时具有 ObservableObjectPublisherPassthroughSubject 发布者作为选择。这让我满足了 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&lt;Int, Never&gt; 发布者,想象中称为发布者。

      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
           }
        })
        
        

        【讨论】:

        • 如果 value 是 bool,iOS 13 的后备解决方案对我不起作用。
        【解决方案7】:

        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)")
                }
            }
        }
        

        【讨论】:

          【解决方案8】:

          具有关系的 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 类是 @ObservedObject var contact: Contact

          saveContactdo-catch 函数到 try viewContext.save()...

          【讨论】:

            【解决方案9】:

            非常重要的问题:我们必须向 Picker 项目视图(在 ForEach 内部)的“标记”修饰符传递一些东西,让它“识别”项目并触发选择更改事件。而我们传递的值将返回到带有“选择”的 Picker 的 Binding 变量。

            例如:

            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)
                        
                    })
            

            【讨论】:

              猜你喜欢
              • 2019-10-22
              • 2021-06-05
              • 2020-09-22
              • 1970-01-01
              • 1970-01-01
              • 2022-08-14
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多