SwiftUI: Get notified when #Binding value changes - binding

I wrote a view to create a typewriter effect in SwiftUI - when I pass in a binding variable it works fine the first time, e.g.: TypewriterTextView($textString)
However, any subsequent time the textString value changes it will not work (since the binding value isn't directly being placed in the body). Am interested in any ideas on how to manually be notified when the #Binding var is changed within the view.
struct TypewriterTextView: View {
#Binding var textString:String
#State private var typingInterval = 0.3
#State private var typedString = ""
var body: some View {
Text(typedString).onAppear() {
Timer.scheduledTimer(withTimeInterval: self.typingInterval, repeats: true, block: { timer in
if self.typedString.length < self.textString.length {
self.typedString = self.typedString + self.textString[self.typedString.length]
}
else { timer.invalidate() }
})
}
}
}

Use the onChange modifier instead of onAppear() to watch the textString binding.
struct TypewriterTextView: View {
#Binding var textString:String
#State private var typingInterval = 0.3
#State private var typedString = ""
var body: some View {
Text(typedString).onChange(of: textString) {
typedString = ""
Timer.scheduledTimer(withTimeInterval: self.typingInterval, repeats: true, block: { timer in
if self.typedString.length < self.textString.length {
self.typedString = self.typedString + self.textString[self.typedString.length]
}
else { timer.invalidate() }
})
}
}
}
Compatibility
The onChange modifier was introduced at WWDC 2020 and is only available on
macOS 11+
iOS 14+
tvOS 14+
watchOS 7+
If you want to use this functionality on older systems you can use the following shim. It is basically the onChange method reimplemented using an older SwiftUI:
import Combine
import SwiftUI
/// See `View.onChange(of: value, perform: action)` for more information
struct ChangeObserver<Base: View, Value: Equatable>: View {
let base: Base
let value: Value
let action: (Value)->Void
let model = Model()
var body: some View {
if model.update(value: value) {
DispatchQueue.main.async { self.action(self.value) }
}
return base
}
class Model {
private var savedValue: Value?
func update(value: Value) -> Bool {
guard value != savedValue else { return false }
savedValue = value
return true
}
}
}
extension View {
/// Adds a modifier for this view that fires an action when a specific value changes.
///
/// You can use `onChange` to trigger a side effect as the result of a value changing, such as an Environment key or a Binding.
///
/// `onChange` is called on the main thread. Avoid performing long-running tasks on the main thread. If you need to perform a long-running task in response to value changing, you should dispatch to a background queue.
///
/// The new value is passed into the closure. The previous value may be captured by the closure to compare it to the new value. For example, in the following code example, PlayerView passes both the old and new values to the model.
///
/// ```
/// struct PlayerView : View {
/// var episode: Episode
/// #State private var playState: PlayState
///
/// var body: some View {
/// VStack {
/// Text(episode.title)
/// Text(episode.showTitle)
/// PlayButton(playState: $playState)
/// }
/// }
/// .onChange(of: playState) { [playState] newState in
/// model.playStateDidChange(from: playState, to: newState)
/// }
/// }
/// ```
///
/// - Parameters:
/// - value: The value to check against when determining whether to run the closure.
/// - action: A closure to run when the value changes.
/// - newValue: The new value that failed the comparison check.
/// - Returns: A modified version of this view
func onChange<Value: Equatable>(of value: Value, perform action: #escaping (_ newValue: Value)->Void) -> ChangeObserver<Self, Value> {
ChangeObserver(base: self, value: value, action: action)
}
}

Copy and paste solution based on #Damiaan Dufaux's answer.
Use it just like the system .onChange API. It prefers to use the system-provided .onChange on iOS 14 and uses the backup plan on lower.
action will not be called when changed to the same value. (If you use #Damiaan Dufaux's answer, you may find the action being called even if data changes to same value, because model is recreated every time.)
struct ChangeObserver<Content: View, Value: Equatable>: View {
let content: Content
let value: Value
let action: (Value) -> Void
init(value: Value, action: #escaping (Value) -> Void, content: #escaping () -> Content) {
self.value = value
self.action = action
self.content = content()
_oldValue = State(initialValue: value)
}
#State private var oldValue: Value
var body: some View {
if oldValue != value {
DispatchQueue.main.async {
oldValue = value
self.action(self.value)
}
}
return content
}
}
extension View {
func onDataChange<Value: Equatable>(of value: Value, perform action: #escaping (_ newValue: Value) -> Void) -> some View {
Group {
if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) {
self.onChange(of: value, perform: action)
} else {
ChangeObserver(value: value, action: action) {
self
}
}
}
}
}

You can use textString.wrappedValue like this:
struct TypewriterTextView: View {
#Binding var textString: String
#State private var typingInterval = 0.3
#State private var typedString = ""
var body: some View {
Text(typedString).onAppear() {
Timer.scheduledTimer(withTimeInterval: self.typingInterval, repeats: true, block: { timer in
if self.typedString.length < self.textString.length {
self.typedString = self.typedString + self.textString[self.typedString.length]
}
else { timer.invalidate() }
})
}
.onChange(of: $textString.wrappedValue, perform: { value in
print(value)
})
}
}

You can use onReceive with Just wrapper to use it in iOS 13.
struct TypewriterTextView: View {
#Binding var textString:String
#State private var typingInterval = 0.3
#State private var typedString = ""
var body: some View {
Text(typedString)
.onReceive(Just(textString)) {
typedString = ""
Timer.scheduledTimer(withTimeInterval: self.typingInterval, repeats: true, block: { timer in
if self.typedString.length < self.textString.length {
self.typedString = self.typedString + self.textString[self.typedString.length]
}
else { timer.invalidate() }
})
}
}
}

#Binding should only be used when your child View needs to write the value. In your case, you only need to read it so change it to let textString:String and body will run every time it changes. Which is when this View is recreated with the new value in the parent View. This is how SwiftUI works, it only runs body if the View struct vars (or lets) have changed since the last time the View was created.

You can use a so called publisher for this:
public let subject = PassthroughSubject<String, Never>()
Then, inside your timer block you call:
self.subject.send()
Typically you want the above code to be outside your SwiftUI UI declaration.
Now in your SwiftUI code you need to receive this:
Text(typedString)
.onReceive(<...>.subject)
{ (string) in
self.typedString = string
}
<...> need to be replaced by where your subject object is. For example (as a hack on AppDelegate):
.onReceive((UIApplication.shared.delegate as! AppDelegate).subject)
I know the above should work when typedString is a #State:
#State private var typedString = ""
but I guess it should also work with a #Binding; just haven't tried that yet.

Related

How to pass Binding of #published variable in function from ObservedObject

I want to pass a binding of a #Published variable from within my ObservableObject to a struct so that its value can be changed inside a closure. I can't quite get it to work. Here is a simplified version of my code below:
final class OnboardingStateController: ObservableObject {
#Published var shouldHide: Bool = false
func go() {
MyLogic.fooBar(
shouldHide: shouldHide // error appears here Cannot convert value of type 'Bool' to expected argument type 'Binding<Bool>'
)
}
}
struct MyLogic {
static func fooBar(shouldHide: Binding<Bool>) {
... SomeClass({ shouldHide.wrappedValue = true })
}
}
How do I do this?
Here is an alternative, Binding needs a SwiftUI View to stay updated because of its DynamicProperty conformance
import SwiftUI
struct OnboardingStateView: View {
#StateObject var vm: OnboardingStateController = OnboardingStateController()
var body: some View {
VStack{
Button("go", action: {
vm.go()
})
Text(vm.shouldHide.description)
}
}
}
final class OnboardingStateController: ObservableObject {
#Published var shouldHide: Bool = false
func go() {
//This uses a completion handler vs passing the `Binding`
MyLogic.fooBar(
shouldHide: { shouldHide in
self.shouldHide = shouldHide
}
)
}
}
struct MyLogic {
static func fooBar(shouldHide: (Bool) -> Void) {
let value = Bool.random() //.. SomeClass({ shouldHide.wrappedValue = true })
shouldHide(value)
}
}
struct OnboardingStateView_Previews: PreviewProvider {
static var previews: some View {
OnboardingStateView()
}
}
It is not really clear why do you need Binding there, but if it is really still needed there, then you can generate it on the fly, like
func go() {
MyLogic.fooBar(
shouldHide: Binding(get: { self.shouldHide }, set: { self.shouldHide = $0 })
)
}
Note: it is simplified variant, in which self is captured, if you need to avoid it then you take into account using [weak self] in each closure.

Perform action when user change Picker value

I have a piece of sample code that shows picker. It is a simplified version of project that I'm working on. I have a view model that can be updated externally (via bluetooth - in example it's simulated) and by user using Picker. I would like to perform an action (for example an update) when user changes the value. I used onChange event on binding that is set on Picker and it works but the problem is that it also is called when value is changed externally which I don't want. Does anyone knows how to make it work as I expect?
enum Type {
case Type1, Type2
}
class ViewModel: ObservableObject {
#Published var type = Type.Type1
init() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.type = Type.Type2
}
}
}
struct ContentView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Row(type: $viewModel.type) {
self.update()
}
}
}
func update() {
// some update logic that should be called only when user changed value
print("update")
}
}
struct Row: View {
#Binding var type: Type
var action: () -> Void
var body: some View {
Picker("Type", selection: $type) {
Text("Type1").tag(Type.Type1)
Text("Type2").tag(Type.Type2)
}
.onChange(of: type, perform: { newType in
print("changed: \(newType)")
action()
})
}
}
EDIT:
I found a solution but I'm not sure if it's good one. I had to use custom binding like this:
struct Row: View {
#Binding var type: Type
var action: () -> Void
var body: some View {
let binding = Binding(
get: { self.type },
set: { self.type = $0
action()
}
)
return Picker("Type", selection: binding) {
Text("Type1").tag(Type.Type1)
Text("Type2").tag(Type.Type2)
}
}
}

SwiftUI Combine Debounce TextField

I have a SwiftUI app with SwiftUI App life cycle. I'm trying to setup a standard way to add
typing debounce to TextFields. Ideally, I'd like to create my own TextField modifier that
can easily be applied to views that have many textfields to edit. I've tried a bunch of
ways to do this but I must be missing something fundamental. Here's one example. This
does not work:
struct ContentView: View {
#State private var searchText = ""
var body: some View {
VStack {
Text("You entered: \(searchText)")
.padding()
TextField("Enter Something", text: $searchText)
.frame(height: 30)
.padding(.leading, 5)
.overlay(
RoundedRectangle(cornerRadius: 6)
.stroke(Color.blue, lineWidth: 1)
)
.padding(.horizontal, 20)
.onChange(of: searchText, perform: { _ in
var subscriptions = Set<AnyCancellable>()
let pub = PassthroughSubject<String, Never>()
pub
.debounce(for: .seconds(1), scheduler: DispatchQueue.main)
.collect()
.sink(receiveValue: { t in
self.searchText = t.first ?? "nothing"
} )
.store(in: &subscriptions)
})
}
}
}
Any guidance would be appreciated. Xcode 12.4, iOS 14.4
I think you'll have to keep two variables: one for the text in the field as the user is typing and one for the debounced text. Otherwise, the user wouldn't see the typing coming in in real-time, which I'm assuming isn't the behavior you want. I'm guessing this is probably for the more standard use case of, say, performing a data fetch once the user has paused their typing.
I like ObservableObjects and Combine to manage this sort of thing:
class TextFieldObserver : ObservableObject {
#Published var debouncedText = ""
#Published var searchText = ""
private var subscriptions = Set<AnyCancellable>()
init() {
$searchText
.debounce(for: .seconds(1), scheduler: DispatchQueue.main)
.sink(receiveValue: { [weak self] t in
self?.debouncedText = t
} )
.store(in: &subscriptions)
}
}
struct ContentView: View {
#StateObject var textObserver = TextFieldObserver()
#State var customText = ""
var body: some View {
VStack {
Text("You entered: \(textObserver.debouncedText)")
.padding()
TextField("Enter Something", text: $textObserver.searchText)
.frame(height: 30)
.padding(.leading, 5)
.overlay(
RoundedRectangle(cornerRadius: 6)
.stroke(Color.blue, lineWidth: 1)
)
.padding(.horizontal, 20)
Divider()
Text(customText)
TextFieldWithDebounce(debouncedText: $customText)
}
}
}
struct TextFieldWithDebounce : View {
#Binding var debouncedText : String
#StateObject private var textObserver = TextFieldObserver()
var body: some View {
VStack {
TextField("Enter Something", text: $textObserver.searchText)
.frame(height: 30)
.padding(.leading, 5)
.overlay(
RoundedRectangle(cornerRadius: 6)
.stroke(Color.blue, lineWidth: 1)
)
.padding(.horizontal, 20)
}.onReceive(textObserver.$debouncedText) { (val) in
debouncedText = val
}
}
}
I included two examples -- the top, where the container view (ContentView) owns the ObservableObject and the bottom, where it's made into a more-reusable component.
A little simplified version of text debouncer from #jnpdx
Note that .assign(to: &$debouncedText) doesn't create a reference cycle and manages subscription for you automatically
class TextFieldObserver : ObservableObject {
#Published var debouncedText = ""
#Published var searchText = ""
init(delay: DispatchQueue.SchedulerTimeType.Stride) {
$searchText
.debounce(for: delay, scheduler: DispatchQueue.main)
.assign(to: &$debouncedText)
}
}
If you are not able to use an ObservableObject (ie, if your view is driven by a state machine, or you are passing the input results to a delegate, or are simply publishing the input), there is a way to accomplish the debounce using only view code. This is done by forwarding text changes to a local Publisher, then debouncing the output of that Publisher.
struct SomeView: View {
#State var searchText: String = ""
let searchTextPublisher = PassthroughSubject<String, Never>()
var body: some View {
TextField("Search", text: $searchText)
.onChange(of: searchText) { searchText in
searchTextPublisher.send(searchText)
}
.onReceive(
searchTextPublisher
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
) { debouncedSearchText in
print(debouncedSearchText)
}
}
}
Or if broadcasting the changes:
struct DebouncedSearchField: View {
#Binding var debouncedSearchText: String
#State private var searchText: String = ""
private let searchTextPublisher = PassthroughSubject<String, Never>()
var body: some View {
TextField("Search", text: $searchText)
.onChange(of: searchText) { searchText in
searchTextPublisher.send(searchText)
}
.onReceive(
searchTextPublisher
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
) { debouncedSearchText in
self.debouncedSearchText = debouncedSearchText
}
}
}
However, if you have the choice, it may be more "correct" to go with the ObservableObject approach.
Tunous on GitHub added a debounce extension to onChange recently. https://github.com/Tunous/DebouncedOnChange that is super simple to use. Instead of adding .onChange(of: value) {newValue in doThis(with: newValue) } you can add .onChange(of: value, debounceTime: 0.8 /sec/ ) {newValue in doThis(with: newValue) }
He sets up a Task that sleeps for the debounceTime but it is cancelled and reset on every change to value. The view modifier he created uses a State var debounceTask. It occurred to me that this task could be a Binding instead and shared amount multiple onChange view modifiers allowing many textfields to be modified on the same debounce. This way if you programmatically change a bunch of text fields using the same debounceTask only one call to the action is made, which is often what one wants to do. Here is the code with a simple example.
//
// Debounce.swift
//
// Created by Joseph Levy on 7/11/22.
// Based on https://github.com/Tunous/DebouncedOnChange
import SwiftUI
import Combine
extension View {
/// Adds a modifier for this view that fires an action only when a time interval in seconds represented by
/// `debounceTime` elapses between value changes.
///
/// Each time the value changes before `debounceTime` passes, the previous action will be cancelled and the next
/// action /// will be scheduled to run after that time passes again. This mean that the action will only execute
/// after changes to the value /// stay unmodified for the specified `debounceTime` in seconds.
///
/// - Parameters:
/// - value: The value to check against when determining whether to run the closure.
/// - debounceTime: The time in seconds to wait after each value change before running `action` closure.
/// - action: A closure to run when the value changes.
/// - Returns: A view that fires an action after debounced time when the specified value changes.
public func onChange<Value>(
of value: Value,
debounceTime: TimeInterval,
perform action: #escaping (_ newValue: Value) -> Void
) -> some View where Value: Equatable {
self.modifier(DebouncedChangeViewModifier(trigger: value, debounceTime: debounceTime, action: action))
}
/// Same as above but adds before action
/// - debounceTask: The common task for multiple Values, but can be set to a different action for each change
/// - action: A closure to run when the value changes.
/// - Returns: A view that fires an action after debounced time when the specified value changes.
public func onChange<Value>(
of value: Value,
debounceTime: TimeInterval,
task: Binding< Task<Void,Never>? >,
perform action: #escaping (_ newValue: Value) -> Void
) -> some View where Value: Equatable {
self.modifier(DebouncedTaskBindingChangeViewModifier(trigger: value, debounceTime: debounceTime, debouncedTask: task, action: action))
}
}
private struct DebouncedChangeViewModifier<Value>: ViewModifier where Value: Equatable {
let trigger: Value
let debounceTime: TimeInterval
let action: (Value) -> Void
#State private var debouncedTask: Task<Void,Never>?
func body(content: Content) -> some View {
content.onChange(of: trigger) { value in
debouncedTask?.cancel()
debouncedTask = Task.delayed(seconds: debounceTime) { #MainActor in
action(value)
}
}
}
}
private struct DebouncedTaskBindingChangeViewModifier<Value>: ViewModifier where Value: Equatable {
let trigger: Value
let debounceTime: TimeInterval
#Binding var debouncedTask: Task<Void,Never>?
let action: (Value) -> Void
func body(content: Content) -> some View {
content.onChange(of: trigger) { value in
debouncedTask?.cancel()
debouncedTask = Task.delayed(seconds: debounceTime) { #MainActor in
action(value)
}
}
}
}
extension Task {
/// Asynchronously runs the given `operation` in its own task after the specified number of `seconds`.
///
/// The operation will be executed after specified number of `seconds` passes. You can cancel the task earlier
/// for the operation to be skipped.
///
/// - Parameters:
/// - time: Delay time in seconds.
/// - operation: The operation to execute.
/// - Returns: Handle to the task which can be cancelled.
#discardableResult
public static func delayed(
seconds: TimeInterval,
operation: #escaping #Sendable () async -> Void
) -> Self where Success == Void, Failure == Never {
Self {
do {
try await Task<Never, Never>.sleep(nanoseconds: UInt64(seconds * 1e9))
await operation()
} catch {}
}
}
}
// MultiTextFields is an example
// when field1, 2 or 3 change the number times is incremented by one, one second later
// when field changes the three other fields are changed too but the increment task only
// runs once because they share the same debounceTask
struct MultiTextFields: View {
#State var debounceTask: Task<Void,Never>?
#State var field: String = ""
#State var field1: String = ""
#State var field2: String = ""
#State var field3: String = ""
#State var times: Int = 0
var body: some View {
VStack {
HStack {
TextField("Field", text: $field).padding()
.onChange(of: field, debounceTime: 1) { newField in
field1 = newField
field2 = newField
field3 = newField
}
Text(field+" \(times)").padding()
}
Divider()
HStack {
TextField("Field1", text: $field1).padding()
.onChange(of: field1, debounceTime: 1, task: $debounceTask) {_ in
times+=1 }
Text(field1+" \(times)").padding()
}
HStack {
TextField("Field2", text: $field2).padding()
.onChange(of: field2, debounceTime: 1, task: $debounceTask) {_ in
times+=1 }
Text(field2+" \(times)").padding()
}
HStack {
TextField("Field3", text: $field3).padding()
.onChange(of: field3, debounceTime: 1, task: $debounceTask) {_ in
times+=1 }
Text(field3+" \(times)").padding()
}
}
}
}
struct View_Previews: PreviewProvider {
static var previews: some View {
MultiTextFields()
}
}
I haven't tried the shared debounceTask binding using an ObservedObject or StateObject, just a State var as yet. If anyone tries that please post the result.

SwiftUI: How to get continuous updates from Slider

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

Is there a way to bind an optional to a Toggle / Slider with SwiftUI

I have a UInt? (optional) property in my data model that I am trying to bind to a Toggle and Slider with SwiftUI. I am trying to get to something like this:
maximumRingsShownCount have value 4 (not nil), then the toggle should be on and the value bound to the slider.
maximumExpandedRingsShownCount value is nil, then the toggle should off and the slider is not shown.
I am facing 2 issues here:
It looks like we cannot have optional bindings (for the Slider)
Is it possible to have a transformer to transform the optional to boolean (for the Toggle)?
So far, in my view I declared 2 properties in addition to my model:
#ObjectBinding var configuration: SunburstConfiguration
#State private var haveMaximumRingsShownCount: Bool = false
#State private var haveMaximumExpandedRingsShownCount: Bool = false
And my view body contains (for each property):
Toggle(isOn: $haveMaximumRingsShownCount) {
Text("maximumRingsShownCount")
}
if haveMaximumRingsShownCount {
VStack(alignment: .leading) {
Text("maximumRingsShownCount = \(config.maximumRingsShownCount!)")
Slider(value: .constant(4 /* no binding here :( */ ))
}
}
}
The UI layout is correct but I still have the issues mentioned above because:
The haveMaximumRingsShownCount state is not bound to my config.maximumRingsShownCount model being nil or not
The slider is currently just displaying a constant and not bound to the unwrapped config.maximumRingsShownCount property
Any ideas on how to solve these issue with optionals?
[ This can be reproduced in the sample code at https://github.com/lludo/SwiftSunburstDiagram ]
I found it convenient to overload the nil coalescing operator (??) to work with this situation.
// OptionalBinding.swift
import Foundation
import SwiftUI
func OptionalBinding<T>(_ binding: Binding<T?>, _ defaultValue: T) -> Binding<T> {
return Binding<T>(get: {
return binding.wrappedValue ?? defaultValue
}, set: {
binding.wrappedValue = $0
})
}
func ??<T> (left: Binding<T?>, right: T) -> Binding<T> {
return OptionalBinding(left, right)
}
You can then use it as follows:
struct OptionalSlider: View {
#Binding var optionalDouble: Double?
var body: some View {
Slider(value: $optionalDouble ?? 0.0, in: 0.0...1.0)
}
}
It's a bit tricky, but creating manually the Binding (by providing the getter and the setter) required for the view is the best solution I have found so far.
class DataModel: BindableObject {
public let didChange = PassthroughSubject<Void, Never>()
var maximumRingsShownCount: UInt? = 50 {
didSet {
didChange.send(())
}
}
lazy private(set) var sliderBinding: Binding<Double> = {
return Binding<Double>(getValue: {
return Double(self.maximumRingsShownCount ?? 0) / 100.0
}) { (value) in
self.maximumRingsShownCount = UInt(value * 100)
}
}()
lazy private(set) var toggleBinding: Binding<Bool> = {
return Binding<Bool>(getValue: { () -> Bool in
return self.maximumRingsShownCount != nil
}, setValue: { (value) in
self.maximumRingsShownCount = value ? 0 : nil
})
}()
}
struct ContentView : View {
#ObjectBinding var model = DataModel()
var body: some View {
VStack {
Toggle(isOn: model.toggleBinding) {
Text("Enable")
}
if model.maximumRingsShownCount != nil {
Text("max rings: \(model.maximumRingsShownCount!)")
Slider(value: model.sliderBinding)
}
}.padding()
}
}
As the Slider can only accept floating point numbers, the Binding handle the conversion between UInt and Double values.
Note : There is still a weird behaviour with the Toggle the first time its state is updated by a view event. I couldn't find a way to avoid this for now, but the code might still help you.
I've found another way without changing the model, basically, you just create a Binding object without the optional inside the view you are interested and then move your unwrapping logic there
ie
struct OptionalSlider: View {
private var optionalValue: Binding<Double>
private var label: String
private var hide: Bool
var body: some View {
Group {
if hide {
EmptyView()
} else {
HStack() {
Text(label)
Slider(value: optionalValue, in: 0.0...1.0)
}
}
}
}
init(value: Binding<Double?>, label: String) {
self.label = label
self.optionalValue = Binding<Double>(get: {
return value.wrappedValue ?? 0.0
}, set: {
value.wrappedValue = $0
})
self.hide = value.wrappedValue == nil
}
}

Resources