SwiftUI:如何从 Slider 获取持续更新

SwiftUI: How to get continuous updates from Slider

我正在像这样试验 SwiftUI 和 Slider 控件:

struct MyView: View {

    @State private var value = 0.5

    var body: some View {
        Slider(value: $value) { pressed in
        }
    }
}

我试图在用户拖动时从 Slider 获取连续更新,但它似乎只在值更改结束时更新值。

有人玩过这个吗?知道如何让 SwiftUI Slider 发出一连串的价值变化?也许结合?

在 SwiftUI 中,您可以将 UI 元素(例如滑块)绑定到数据模型中的属性,并在那里实现您的业务逻辑。

例如,要获取连续的滑块更新:

import SwiftUI
import Combine

final class SliderData: BindableObject {

  let didChange = PassthroughSubject<SliderData,Never>()

  var sliderValue: Float = 0 {
    willSet {
      print(newValue)
      didChange.send(self)
    }
  }
}

struct ContentView : View {

  @EnvironmentObject var sliderData: SliderData

  var body: some View {
    Slider(value: $sliderData.sliderValue)
  }
}

请注意,要让您的场景使用数据模型对象,您需要在 SceneDelegate class 中将 window.rootViewController 更新为如下内容,否则应用程序会崩溃。

window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(SliderData()))

我无法在 iOS 13 Beta 2 上重现此问题。您的目标操作系统是什么?

使用自定义绑定,每个小更改都会打印值,而不仅仅是在编辑结束后。

Slider(value: Binding<Double>(getValue: {0}, setValue: {print([=10=])}))

请注意,闭包 ({ pressed in }) 仅在编辑结束开始和结束时报告,值流仅传递到绑定中。

经过多次尝试,我最终得到了以下代码。为了使答案简短,它被缩减了一些,但这里是。我需要一些东西:

  • 在设置外部绑定之前从滑块读取值更改并将它们四舍五入到最接近的整数。
  • 根据整数设置本地化提示值。
struct AspectSlider: View {

    // The first part of the hint text localization key.
    private let hintKey: String

    // An external integer binding to be set when the rounded value of the slider
changes to a different integer.
    private let value: Binding<Int>

    // A local binding that is used to track the value of the slider.
    @State var sliderValue: Double = 0.0

    init(value: Binding<Int>, hintKey: String) {
        self.value = value
        self.hintKey = hintKey
    }

    var body: some View {
        VStack(alignment: .trailing) {

            // The localized text hint built from the hint key and the rounded slider value.
            Text(LocalizedStringKey("\(hintKey).\(self.value.value)"))

            HStack {
                Text(LocalizedStringKey(self.hintKey))
                Slider(value: Binding<Double>(
                    getValue: { self.$sliderValue.value },
                    setValue: { self.sliderChanged(toValue: [=10=]) }
                    ),
                    through: 4.0) { if ![=10=] { self.slideEnded() } }
            }
        }
    }

    private func slideEnded() {
        print("Moving slider to nearest whole value")
        self.sliderValue = self.sliderValue.rounded()
    }

    private func sliderChanged(toValue value: Double) {
        $sliderValue.value = value
        let roundedValue = Int(value.rounded())
        if roundedValue == self.value.value {
            return
        }

        print("Updating value")
        self.value.value = roundedValue
    }
}

iOS13.4,Swift5.x

基于Mohammid优秀解决方案的答案,只是我不想使用环境变量。

class SliderData: ObservableObject {
let didChange = PassthroughSubject<SliderData,Never>()

@Published var sliderValue: Double = 0 {
  didSet {
    print("sliderValue \(sliderValue)")
    didChange.send(self)
   }
  }
}

@ObservedObject var sliderData:SliderData

Slider(value: $sliderData.sliderValue, in: 0...Double(self.textColors.count))

对 ContentView_Preview 稍作改动,在 SceneDelegate 中也是如此。

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
      ContentView(sliderData: SliderData.init())
    }
}

在版本 11.4.1 (11E503a) & Swift 5. 我没有复制它。 通过使用 Combine,我可以从滑块变化中不断更新。

class SliderData: ObservableObject {

  @Published var sliderValue: Double = 0
  ...

}

struct ContentView: View {

  @ObservedObject var slider = SliderData()

  var body: some View {
    VStack {
      Slider(value: $slider.sliderValue)
      Text(String(slider.sliderValue))
    } 
  }
}  

只需使用Slider 的onEditingChanged 参数即可。当用户移动滑块或仍与滑块接触时,该参数为真。当参数从 true 变为 false 时,我会进行更新。

struct MyView: View {

    @State private var value = 0.5

    func update(changing: Bool) -> Void {
        // Do whatever
    }

    var body: some View {
        Slider(value: $value, onEditingChanged: {changing in self.update(changing) }) 
        { pressed in }
    }
}

这样怎么样:

(1) 首先你需要可观察的 ...

import SwiftUI
import PlaygroundSupport

// make your observable double for the slider value:
class SliderValue: ObservableObject {
    @Published var position: Double = 11.0
}

(2) 当你制作滑块时,你必须传入一个可观察对象的实例:

所以在HandySlider中它被声明为一个ObservedObject。 (别忘了,你不是在那里“制作”它。只在你“制作”它的地方将它声明为 StateObject。)

(3) 并且您像往常一样在滑块中使用“$”作为滑块值

(似乎语法是在“整个事物”上使用它,如“$sliderValue.position”,而不是在值本身上,如“sliderValue.$position”。)

struct HandySlider: View {

    // don't forget to PASS IN a state object when you make a HandySlider
    @ObservedObject var sliderValue: SliderValue

    var body: some View {
        HStack {
            Text("0")
            Slider(value: $sliderValue.position, in: 0...20)
            Text("20")
        }
    }
}

(4) 实际上在某处创建状态对象。

(因此,您使用“StateObject”而不是“ObservedObject”。)

然后

(5)在你想显示数值的地方随意使用。

struct ContentView: View {

    // here we literally make the state object
    // (you'd just make it a "global" but not possible in playground)
    @StateObject var sliderValue = SliderValue()

    var body: some View {

        HandySlider(sliderValue: sliderValue)
            .frame(width: 400)

        Text(String(sliderValue.position))
    }
}

PlaygroundPage.current.setLiveView(ContentView())

测试一下...

这是要粘贴在操场上的全部内容...

import SwiftUI
import PlaygroundSupport

class SliderValue: ObservableObject {
    @Published var position: Double = 11.0
}

struct HandySlider: View {
    @ObservedObject var sliderValue: SliderValue
    var body: some View {
        HStack {
            Text("0")
            Slider(value: $sliderValue.position, in: 0...20)
            Text("20")
        }
    }
}

struct ContentView: View {
    @StateObject var sliderValue = SliderValue()
    var body: some View {
        HandySlider(sliderValue: sliderValue)
            .frame(width: 400)
        Text(String(sliderValue.position))
    }
}

PlaygroundPage.current.setLiveView(ContentView())

总结...

  • 你需要一个 ObservableObject class:那些包含 Published 变量。

  • 某处(很明显,只有 一个 地方),你会从字面上制作那个可观察对象 class,那就是 StateObject

  • 最后你可以在任何你想要的地方使用那个可观察对象class(需要多少地方),那就是ObservedObject

并在滑块中...

特别是在滑块的棘手情况下,所需的语法似乎是

Slider(value: $ooc.pitooc, in: 0...20)

ooc - 您的可观察对象 class

pitooc - 属性 在那个可观察对象 class

不会在滑块内部创建可观察对象class,你在别处创建它并且将其传入到滑块。 (所以确实在滑块 class 中它是一个观察对象, 而不是 一个状态对象。)

如果值在导航中,视图:

如果滑块是一个 弹出窗口,您可以调整值。

其实更简单,滑块什么都不用传入。只需使用 @EnvironmentObject.

不要忘记环境对象必须在祖先链中(不幸的是你不能“侧身”)。

EnvironmentObject 仅适用于父子链。

有点令人困惑,如果有问题的项目在同一个“环境!”中,则不能使用简单的 EnvironmentObject 系统! EnvironmentObject 或许应该命名为“ParentChainObject”或“NavigationViewChainObject”之类的名称。

EnvironmentObject 仅在您使用 NavigationView 时使用。

import SwiftUI
import PlaygroundSupport
// using ancestor views ...

class SliderValue: ObservableObject {
    @Published var position: Double = 11.0
}

struct HandySliderPopUp: View {
    @EnvironmentObject var sv: SliderValue
    var body: some View {
        Slider(value: $sv.position, in: 0...10)
    }
}

struct ContentView: View {
    @StateObject var sliderValue = SliderValue()
    var body: some View {
        NavigationView {
            VStack{
                NavigationLink(destination:
                  HandySliderPopUp().frame(width: 400)) {
                    Text("click me")
                }
                Text(String(sliderValue.position))
            }
        }
        .environmentObject(sliderValue) //HERE
    }
}
PlaygroundPage.current.setLiveView(ContentView())

请注意,//HERE 是您“设置”环境对象的位置。

对于“通常”的情况,即“相同”的视图,请参阅其他答案。

派对迟到了,这就是我所做的:

struct DoubleSlider: View {
  @State var value: Double
  
  let range: ClosedRange<Double>
  let step: Double
  let onChange: (Double) -> Void
  
  init(initialValue: Double, range: ClosedRange<Double>, step: Double, onChange: @escaping (Double) -> Void) {
    self.value = initialValue
    self.range = range
    self.step = step
    self.onChange = onChange
  }
  
  var body: some View {
    let binding = Binding<Double> {
      return value
    } set: { newValue in
      value = newValue
      onChange(newValue)
    }
    
    Slider(value: binding, in: range, step: step)
  }
}

用法:

  DoubleSlider(initialValue: state.tipRate, range: 0...0.3, step: 0.01) { rate in
    viewModel.state.tipRate = rate
}

我们可以不使用自定义绑定、自定义 inits、ObservableObjects、PassthroughSubjects、@Published 和其他并发症。滑块具有 .onChange(of: perform:) 修饰符,非常适合这种情况。

可以改写为:

struct AspectSlider2: View {

    @Binding var value: Int
    let hintKey: String
    @State private var sliderValue: Double = 0.0

    var body: some View {
        VStack(alignment: .trailing) {

            Text(LocalizedStringKey("\(hintKey)\(value)"))

            HStack {
                Slider(value: $sliderValue, in: 0...5)
                    .onChange(of: sliderValue, perform: sliderChanged)
            }
        }
    }

    private func sliderChanged(to newValue: Double) {
        sliderValue = newValue.rounded()
        let roundedValue = Int(sliderValue)
        if roundedValue == value {
            return
        }

        print("Updating value")
        value = roundedValue
    }
}