简体   繁体   English

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

[英]SwiftUI: How to get continuous updates from Slider

I'm experimenting with SwiftUI and the Slider control like this:我正在试验 SwiftUI 和 Slider 控件,如下所示:

struct MyView: View {

    @State private var value = 0.5

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

I'm trying to get continuous updates from the Slider as the user drags it, however it appears that it only updates the value at the end of the value change.当用户拖动它时,我试图从Slider获取连续更新,但它似乎只在值更改结束时更新值。

Anyone played with this?有人玩过这个吗? know how to get a SwiftUI Slider to issue a stream of value changes?知道如何得到一个 SwiftUI Slider 发出一个 stream 的值变化吗? Combine perhaps?也许结合?

In SwiftUI, you can bind UI elements such as slider to properties in your data model and implement your business logic there.在 SwiftUI 中,您可以将滑块等 UI 元素绑定到数据模型中的属性,并在那里实现您的业务逻辑。

For example, to get continuous slider updates:例如,要获得连续的滑块更新:

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

Note that to have your scene use the data model object, you need to update your window.rootViewController to something like below inside SceneDelegate class, otherwise the app crashes.请注意,要让您的场景使用数据模型对象,您需要将window.rootViewController更新为如下所示的 SceneDelegate 类,否则应用程序会崩溃。

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

在此处输入图片说明

We can go without custom bindings, custom init s, ObservableObject s, PassthroughSubject s, @Published and other complications.我们可以 go 没有自定义绑定、自定义init s、 ObservableObject s、 PassthroughSubject s、 @Published和其他并发症。 Slider has .onChange(of: perform:) modifier which is perfect for this case. Slider 具有.onChange(of: perform:)修饰符,非常适合这种情况。

This answer can be rewritten as follows:这个答案可以重写如下:

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

After much playing around I ended up with the following code.经过多次玩耍,我最终得到了以下代码。 It's a little cut down to keep the answer short, but here goes.为了保持简短的答案,稍微减少了一点,但在这里。 There was a couple of things I needed:我需要一些东西:

  • To read value changes from the slider and round them to the nearest integer before setting an external binding.在设置外部绑定之前从滑块读取值更改并将它们四舍五入到最接近的整数。
  • To set a localized hint value based on the integer.根据整数设置本地化提示值。
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: $0) }
                    ),
                    through: 4.0) { if !$0 { 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
    }
}

In Version 11.4.1 (11E503a) & Swift 5. I didn't reproduce it.在版本 11.4.1 (11E503a) 和 Swift 5 中。我没有复制它。 By using Combine, I could get continuously update from slider changes.通过使用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))
    } 
  }
}  

I am not able to reproduce this issue on iOS 13 Beta 2. Which operating system are you targeting?我无法在 iOS 13 Beta 2 上重现此问题。您的目标是哪个操作系统?

Using a custom binding, the value is printed for every small change, not only after editing ended.使用自定义绑定,每个小的更改都会打印该值,而不仅仅是在编辑结束后。

Slider(value: Binding<Double>(getValue: {0}, setValue: {print($0)}))

Note, that the closure ( { pressed in } ) only reports when editing end starts and ends, the value stream is only passed into the binding.请注意,闭包( { pressed in } )仅在编辑结束开始和结束时报告,值流仅传递到绑定中。

iOS 13.4, Swift 5.x iOS 13.4,Swift 5.x

An answer based on Mohammid excellent solution, only I didn't want to use environmental variables.基于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))

With a small change to ContentView_Preview and the same in SceneDelegate.对 ContentView_Preview 和SceneDelegate 中的一个小改动。

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

Just use the onEditingChanged parameter of Slider.只需使用 Slider 的 onEditingChanged 参数即可。 The argument is true while the user is moving the slider or still in contact with it.当用户移动滑块或仍然与其接触时,该参数为真。 I do my updates when the argument changes from true to false.当参数从 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 }
    }
}

What about like this:像这样怎么样:

(1) First you need the observable ... (1)首先你需要可观察的......

import SwiftUI
import PlaygroundSupport

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

(2) When you make the slider, you have to PASS IN an instance of the observable: (2)当你制作slider时,你必须PASS IN一个observable的实例:

So in HandySlider it is declared as an ObservedObject.所以在 HandySlider 中它被声明为一个 ObservedObject。 (Don't forget, you're not "making" it there. Only declare it as a StateObject where you are "making" it.) (不要忘记,你不是在那里“制造”它。只在你“制造”它的地方将它声明为一个 StateObject。)

(3) AND you use the "$" for the Slider value as usual in a slider (3)并且您像往常一样在滑块中使用“$”作为滑块值

(It seems the syntax is to use it on the "whole thing" like this "$sliderValue.position" rather than on the value per se, like "sliderValue.$position".) (语法似乎是在“整个事物”上使用它,例如“$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) Actually make the state object somewhere. (4)实际上在某处制作状态对象。

(So, you use "StateObject" to do that, not "ObservedObject".) (因此,您使用“StateObject”来做到这一点,而不是“ObservedObject”。)

And then进而

(5) use it freely where you want to display the value. (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())

Test it ...测试一下...

Here's the whole thing to paste in a playground ...这是粘贴在操场上的全部内容......

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

Summary ...概括 ...

  • You'll need an ObservableObject class: those contain Published variables.你需要一个ObservableObject类:那些包含已发布的变量。

  • Somewhere (obviously one place only) you will literally make that observable object class, and that's StateObject在某个地方(显然只有一个地方)你会真正地制作那个可观察的对象类,那就是StateObject

  • Finally you can use that observable object class anywhere you want (as many places as needed), and that's ObservedObject最后,你可以在任何你想要的地方(根据需要尽可能多的地方)使用该 observable 对象类,那就是ObservedObject

And in a slider ...在滑块中......

In the tricky case of a slider in particular, the desired syntax seems to be特别是在滑块的棘手情况下,所需的语法似乎是

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

ooc - your observable object class ooc - 您的可观察对象类

pitooc - a property in that observable object class ptooc - 该可观察对象类中的属性

You would not create the observable object class inside the slider, you create it elsewhere and pass it in to the slider.不会在滑块内部创建可观察对象类,而是在其他地方创建它并将其传递给滑块。 (So indeed in the slider class it is an observed object, not a state object.) (所以确实在滑块类中它是一个观察对象,而不是状态对象。)

If the value is in a navigation, child view:如果该值在导航中,则视图:

Here's the case if the slider is, say, a popup which allows you to adjust a value.如果滑块是一个允许您调整值的弹出窗口,就会出现这种情况。

It's actually simpler, nothing needs to be passed in to the slider.其实更简单,什么都不需要传入slider。 Just use an @EnvironmentObject .只需使用@EnvironmentObject

Don't forget environment objects must be in the ancestor chain (you can't unfortunately go "sideways").不要忘记环境对象必须在祖先链中(不幸的是,您不能“侧身”)。

EnvironmentObject is only for parent-child chains. EnvironmentObject 仅用于父子链。

Somewhat confusingly, you can't use the simple EnvironmentObject system if the items in question are in the same "environment!"有点令人困惑的是,如果有问题的项目在同一个“环境”中,您就不能使用简单的 EnvironmentObject 系统! EnvironmentObject should perhaps be named something like "ParentChainObject" or "NavigationViewChainObject". EnvironmentObject 或许应该命名为“ParentChainObject”或“NavigationViewChainObject”。

EnvironmentObject is only used when you are using NavigationView. 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())

Note that //HERE is where you "set" the environment object.请注意, //HERE是您“设置”环境对象的地方。

For the "usual" situation, where it's the "same" view, see other answer.对于“通常”情况,即“相同”视图,请参阅其他答案。

Late to the party, this is what I did:派对迟到了,这就是我所做的:

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

Usage:用法:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM