SwiftUI: How to get continuous updates from Slider - ios

I'm experimenting with SwiftUI and the Slider control like this:
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.
Anyone played with this? know how to get a SwiftUI Slider to issue a stream of value changes? Combine perhaps?

In SwiftUI, you can bind UI elements such as slider to properties in your data model and implement your business logic there.
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 = UIHostingController(rootView: ContentView().environmentObject(SliderData()))

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

We can go without custom bindings, custom inits, ObservableObjects, PassthroughSubjects, #Published and other complications. Slider has .onChange(of: perform:) modifier which is perfect for this case.
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
}
}

In Version 11.4.1 (11E503a) & Swift 5. I didn't reproduce it.
By using Combine, I could get continuously update from slider changes.
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?
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.

What about like this:
(1) First you need the observable ...
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:
So in HandySlider it is declared as an ObservedObject. (Don't forget, you're not "making" it there. Only declare it as a StateObject where you are "making" it.)
(3) AND you use the "$" for the Slider value as usual in a slider
(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".)
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.
(So, you use "StateObject" to do that, not "ObservedObject".)
And then
(5) use it freely where you want to display the value.
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.
Somewhere (obviously one place only) you will literally make that observable object class, and that's StateObject
Finally you can use that observable object class anywhere you want (as many places as needed), and that's 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
pitooc - a property in that observable object class
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.)

iOS 13.4, Swift 5.x
An answer based on Mohammid excellent solution, only I didn't want to use environmental variables.
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.
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(sliderData: SliderData.init())
}
}

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. Just use an #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.
Somewhat confusingly, you can't use the simple EnvironmentObject system if the items in question are in the same "environment!" EnvironmentObject should perhaps be named something like "ParentChainObject" or "NavigationViewChainObject".
EnvironmentObject is only used when you are using 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.
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
}

Just use the onEditingChanged parameter of Slider. 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.
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 }
}
}

Related

Change the value of passed object in View

I wonder is it possible to change the passed value and update the view's body itself wherein. ViewModel.value is updating correctly, but the text is constant forever. I tried with #State attribute for value in ViewModel, but doesn't work.
class ViewModel {
var value: Double = 10
lazy var view: SomeView = {
let bind = Binding<Double>(get: {
self.value
}, set: {
self.value = $0
})
return SomeView(value: bind)
}()
}
struct SomeView: View {
#Binding
var value: Double
var body: some View {
VStack {
Text("\(value)") // this text is constant
Slider(value: $value, in: 1...100)
}
}
}
The correct pattern with pair should be as follows
class ViewModel: ObservableObject {
#Published var value: Double = 10
}
struct SomeView: View {
#ObservedObject var vm: ViewModel
var body: some View {
VStack {
Text("\(vm.value)") // this text is constant
Slider(value: $vm.value, in: 1...100)
}
}
}
Now view model is a source of truth for view and (being dynamic property) updates view whenever published value is changed.
And we create view with view model, but not vice versa.

'No exact matches in call to initializer' error on #AppStorage variable?

I'm getting the following error: No exact matches in call to initializer on my #AppStorage variable below:
Model.swift
class UserSettings: ObservableObject {
#AppStorage("minAge") var minAge: Float = UserDefaults.standard.float(forKey: "minAge")
This variable is meant to bind to a Slider value below.
Settings.swift
import SwiftUI
struct Settings: View {
let auth: UserAuth
init(auth: UserAuth) {
self.auth = auth
}
#State var minAge = UserSettings().minAge
let settings = UserSettings()
var body: some View {
VStack {
NavigationView {
Form {
Section {
Text("Min age")
Slider(value: $minAge, in: 18...99, step: 1, label: {Text("Label")})
.onReceive([self.minAge].publisher.first()) { (value) in
UserDefaults.standard.set(self.minAge, forKey: "minAge")
}
Text(String(Int(minAge)))
}
Any idea what the problem is?
I am also seeing the same error with the following:
#AppStorage ("FavouriteBouquets") var favouriteBouquets: [String] = []()
Perhaps an array of Strings is not supported by AppStorage.
Edit Found the answer here.
You don't need intermediate state and UserDefaults, because you can bind directly to AppStorage value and it is by default uses UserDefaults.standard. Also you need to use same types with Slider which is Double.
So, here is a minimal demo solution. Tested with Xcode 12.
struct Settings: View {
#AppStorage("minAge") var minAge: Double = 18
var body: some View {
VStack {
NavigationView {
Form {
Section {
Text("Min age")
Slider(value: $minAge, in: 18...99, step: 1, label: {Text("Label")})
Text(String(Int(minAge)))
}
}
}
}
}
}

Is there a way to call a function when a SwiftUI Picker selection changes an EnvironmentObject?

I have a SwiftUI Form I'm working with, which updates values of an EnvironmentObject. I need to call a function when the value of a Picker changes, but every way I've found is either really messy, or causes other problems. What I'm looking for is a similar way in which the Slider works, but there doesn't seem to be one. Basic code without any solution is here:
class ValueHolder : ObservableObject {
#Published var sliderValue : Float = 0.5
static let segmentedControlValues : [String] = ["one", "two", "three", "four", "five"]
#Published var segmentedControlValue : Int = 3
}
struct ContentView: View {
#EnvironmentObject var valueHolder : ValueHolder
func sliderFunction() {
print(self.valueHolder.sliderValue)
}
func segmentedControlFunction() {
print(ValueHolder.segmentedControlValues[self.valueHolder.segmentedControlValue])
}
var body: some View {
Form {
Text("\(self.valueHolder.sliderValue)")
Slider(value: self.$valueHolder.sliderValue, onEditingChanged: {_ in self.sliderFunction()
})
Text("\(ValueHolder.segmentedControlValues[self.valueHolder.segmentedControlValue])")
Picker("", selection: self.$valueHolder.segmentedControlValue) {
ForEach(0..<ValueHolder.segmentedControlValues.count) {
Text("\(ValueHolder.segmentedControlValues[$0])")
}
}.pickerStyle(SegmentedPickerStyle())
}
}
}
After reviewing this similar (but different) question here: Is there a way to call a function when a SwiftUI Picker selection changes? I have tried using onReceive() as below, but it is also called when the Slider value changes, resulting in unwanted behavior.
.onReceive([self.valueHolder].publisher.first(), perform: {_ in
self.segmentedControlFunction()
})
I have tried changing the parameter for onReceive to filter it by only that value. The value passed is correct, but the segmentedControlFunction still gets called when the slider moves, not just when the picker changes.
.onReceive([self.valueHolder.segmentedControlValue].publisher.first(), perform: {_ in
self.segmentedControlFunction()
})
How can I get the segmentedControlFunction to be called in a similar way to the sliderFunction?
There is much simpler approach, which looks more appropriate for me.
The generic schema as follows
Picker("Label", selection: Binding( // proxy binding
get: { self.viewModel.value }, // get value from storage
set: {
self.viewModel.value = $0 // set value to storage
self.anySideEffectFunction() // didSet function
}) {
// picker content
}
As I was proofing the original question and tinkering with my code I accidentally stumbled across what I think is probably the best solution. So, in case it will help anyone else, here it is:
struct ContentView: View {
#EnvironmentObject var valueHolder : ValueHolder
#State private var sliderValueDidChange : Bool = false
func sliderFunction() {
if self.sliderValueDidChange {
print("Slider value: \(self.valueHolder.sliderValue)\n")
}
self.sliderValueDidChange.toggle()
}
var segmentedControlValueDidChange : Bool {
return self._segmentedControlValue != self.valueHolder.segmentedControlValue
}
#State private var _segmentedControlValue : Int = 0
func segmentedControlFunction() {
self._segmentedControlValue = self.valueHolder.segmentedControlValue
print("SegmentedControl value: \(ValueHolder.segmentedControlValues[self.valueHolder.segmentedControlValue])\n")
}
var body: some View {
Form {
Text("\(self.valueHolder.sliderValue)")
Slider(value: self.$valueHolder.sliderValue, onEditingChanged: {_ in self.sliderFunction()
})
Text("\(ValueHolder.segmentedControlValues[self.valueHolder.segmentedControlValue])")
Picker("", selection: self.$valueHolder.segmentedControlValue) {
ForEach(0..<ValueHolder.segmentedControlValues.count) {
Text("\(ValueHolder.segmentedControlValues[$0])")
}
}.pickerStyle(SegmentedPickerStyle())
.onReceive([self._segmentedControlValue].publisher.first(), perform: {_ in
if self.segmentedControlValueDidChange {
self.segmentedControlFunction()
}
})
}
}
}
Note that the onReceive() is for the a new #State property, which pairs with a Bool evaluating if it's the same as the #EnvironmentObject counterpart. The #State value is then updated on the function call. Also note that in the solution I've changed how the sliderFunction works so that it is only called once, when the #EnvironmentObject value actually changes. It's a bit hacky, but the Slider and Picker both work in the same way when updating values and calling their respective functions.

SwiftUI: observe #Environment property changes

I was trying to use the SwiftUI #Environment property wrapper, but I can't manage to make it work as I expected. Please, help me understanding what I'm doing wrong.
As an example I have an object that produces an integer once per second:
class IntGenerator: ObservableObject {
#Published var newValue = 0 {
didSet {
print(newValue)
}
}
private var toCanc: AnyCancellable?
init() {
toCanc = Timer.TimerPublisher(interval: 1, runLoop: .main, mode: .default)
.autoconnect()
.map { _ in Int.random(in: 0..<1000) }
.assign(to: \.newValue, on: self)
}
}
This object works as expected since I can see all the integers generated on the console log. Now, let's say we want this object to be an environment object accessible from all over the app and from whoever. Let's create the related environment key:
struct IntGeneratorKey: EnvironmentKey {
static let defaultValue = IntGenerator()
}
extension EnvironmentValues {
var intGenerator: IntGenerator {
get {
return self[IntGeneratorKey.self]
}
set {
self[IntGeneratorKey.self] = newValue
}
}
}
Now I can access this object like this (for example from a view):
struct TestView: View {
#Environment(\.intGenerator) var intGenerator: IntGenerator
var body: some View {
Text("\(intGenerator.newValue)")
}
}
Unfortunately, despite the newValue being a #Published property I'm not receiving any update on that property and the Text always shows 0. I'm sure I'm missing something here, what's going on? Thanks.
Environment gives you access to what is stored under EnvironmentKey but does not generate observer for its internals (ie. you would be notified if value of EnvironmentKey changed itself, but in your case it is instance and its reference stored under key is not changed). So it needs to do observing manually, is you have publisher there, like below
#Environment(\.intGenerator) var intGenerator: IntGenerator
#State private var value = 0
var body: some View {
Text("\(value)")
.onReceive(intGenerator.$newValue) { self.value = $0 }
}
and all works... tested with Xcode 11.2 / iOS 13.2
I don't have a definitive answer for how exactly Apple dynamically sends updates to it's standard Environment keys (colorScheme, horizontalSizeClass, etc) but I do have a solution and I suspect Apple does something similar behind the scenes.
Step One) Create an ObservableObject with an #Published properties for your values.
class IntGenerator: ObservableObject {
#Published var int = 0
private var cancellables = Set<AnyCancellable>()
init() {
Timer.TimerPublisher(interval: 1, runLoop: .main, mode: .default)
.autoconnect()
.map { _ in Int.random(in: 0..<1000) }
.assign(to: \.int, on: self)
.store(in: &cancellables)
}
}
Step Two) Create a custom Environment key/value for your property. Here is the first difference between your existing code. Instead of using IntGenerator you'll have an EnvironmentKey for each individual #Published property from step 1.
struct IntKey: EnvironmentKey {
static let defaultValue = 0
}
extension EnvironmentValues {
var int: Int {
get {
return self[IntKey.self]
}
set {
self[IntKey.self] = newValue
}
}
}
Step Three - UIHostingController Approach) This is if you are using an App Delegate as your life cycle (aka a UIKit app w/ Swift UI features). Here is the secret to how we'll be able to dynamically update our Views when our #Published properties change. This simple wrapper View will retain an instance of IntGenerator and update our EnvironmentValues.int when our #Published property value changes.
struct DynamicEnvironmentView<T: View>: View {
private let content: T
#ObservedObject var intGenerator = IntGenerator()
public init(content: T) {
self.content = content
}
public var body: some View {
content
.environment(\.int, intGenerator.int)
}
}
Let us make it easy to apply this to an entire feature's view hierarchy by creating a custom UIHostingController and utilizing our DynamicEnvironmentView. This subclass automatically wraps your content inside a DynamicEnvironmentView.
final class DynamicEnvironmentHostingController<T: View>: UIHostingController<DynamicEnvironmentView<T>> {
public required init(rootView: T) {
super.init(rootView: DynamicEnvironmentView(content: rootView))
}
#objc public required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Here is how we use of new DynamicHostingController
let contentView = ContentView()
window.rootViewController = DynamicEnvironmentHostingController(rootView: contentView)
Step Three - Pure Swift UI App Approach) This is if you are using a pure Swift UI app. In this example our App retains the reference to the IntGenerator but you can play around with different architectures here.
#main
struct MyApp: App {
#ObservedObject var intGenerator = IntGenerator()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.int, intGenerator.int)
}
}
}
Step Four) Lastly here is how we actually use our new EnvironmentKey in any View we need access to the int. This View will automatically be rebuilt any time the int value updates on our IntGenerator class!
struct ContentView: View {
#Environment(\.int) var int
var body: some View {
Text("My Int Value: \(int)")
}
}
Works/Tested in iOS 14 on Xcode 12.2

How to initialize derived variables in body methods in SwiftUI (or alternate approach)

I'm trying to figure out the right way to initialized derived variables in the body method for a SwiftUI view. An example would the string value for an editable integer which would then be edited in a TextField. The integer could for example be part of an #ObservedObject. I cannot figure out any remotely clean way to do this.
I've looked into using custom initializers but this does not seem like the right thing to do. I'm not even sure this code would be run at the appropriate time.
I've also tried using the .onAppear method for TextField, but this method does not appear to be re-executed when the view is rebuilt.
simplified example:
final class Values : ObservableObject {
#Published var count: Int = 0;
}
var sharedValues = Values()
struct ContentView : View {
#ObservedObject var values = sharedValues
var body: some View {
VStack {
Button(
action: { self.add() },
label: { Text("Plus")}
)
InnerView()
}
}
func add() { values.count += 1 }
}
struct InnerView : View {
#ObservedObject var values = sharedValues
#State private var text = ""
var body: some View {
// text = String(value.count) - what I want to do
TextField("", text: $text, onEditingChanged: updateCount)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
func updateCount(updated: Bool) { /* this isn't important in this context */}
}
I would hope to be able to update sharedValues externally and see the update in MyView. In this example, I would want pressing the button to update the text field with the updated text value. But I can't figure a way to have the string representation of the count value computed at the appropriate point in the execution of the code.
I've tried multiple approaches to achieving this type of result, but have come up short.
I'm not sure if I'm understanding your question correctly, but if you are just trying to be able to change a number with a button, have the number be displayed in a text field, and then be able to edit it there, you don't need an ObserverableObject or multiple views.
Here is an example of how you can do it:
struct ContentView: View {
#State var count = 0
#State var countStr = ""
var body: some View {
VStack {
Button(action: {
self.count += 1
self.countStr = "\(self.count)"
}) {
Text("Plus")
}
TextField("", text: $countStr, onEditingChanged: updateCount)
}
}
func updateCount(updated: Bool) { /* this isn't important in this context */ }
}
Use value init method of TextField. This take the value as 2 way Binding. So it automatically update count from both text field and buttons.
import SwiftUI
import Combine
final class Values : ObservableObject {
#Published var count: Int = 0;
}
var sharedValues = Values()
struct AndrewVoelkel : View {
#ObservedObject var values = sharedValues
var body: some View {
HStack {
InnerView()
VStack{
Button(
action: { self.add() },
label: { Text("+")}
)
Button(
action: { self.sub() },
label: { Text("-")}
)
}.font(.headline)
}.padding()
}
func add() { values.count += 1 }
func sub() { values.count -= 1 }
}
struct InnerView : View {
#ObservedObject var values = sharedValues
var body: some View {
TextField("", value: $values.count, formatter: NumberFormatter())
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}

Resources