Passing a bound value as an argument to a SwiftUI 2 View - swift

I've got a radial slider which works with this somewhat wordy code:
struct MyView: View {
#ObservedObject var object: Object
#State var parameter: Double = 0 {
didSet {
object.some.nested?.parameter = Int(parameter)
}
}
var body: some View {
RadialSlider(value: $parameter, label: "my parameter")
.onAppear { parameter = Double(object.some.nested?.parameter ?? 0) }
.onChange(of: parameter) { object.some.nested?.parameter = Int($0) }
}
}
As I need to use this slider multiple times, I'd like to omit the onAppear and onChange lines (with the help of a ViewModifier for example). But I don't know how to go about passing my parameter to the RadialSlider so that it maintains the binding. The type of object.some.nested?.parameter can vary between being an Int and a String.
Alternately, Is there a simpler way to bind the value from my object to my radial slider's UI?

You can initialize parameter in init, like
struct MyView: View {
#ObservedObject var object: Object
#State var parameter: Double { // << no initial value !!
didSet {
object.some.nested?.parameter = Int(parameter)
}
}
init(object: Object) {
self.object = object
_parameter = State(initialValue: Double(object.some.nested?.parameter ?? 0))
}
var body: some View {
RadialSlider(value: $parameter, label: "my parameter")
.onChange(of: parameter) { object.some.nested?.parameter = Int($0) }
}
}

Related

Passing a Binding variable to a SwiftUI view from a function causes didSet observer to no longer fire

I want to be able to dynamically return the right variable to be used as a #Binding in a SwiftUI view however when I create a function to return something as Binding I no longer receive the didSet call on that variable. I'm not sure if this is unsupported behavior or I'm doing something wrong.
Here is an example
struct ContentView: View {
#StateObject var dataStore = DataStore()
var body: some View {
VStack {
Toggle("val1", isOn: $dataStore.val1)
Toggle("val2", isOn: dataStore.boundVal2())
}
}
}
class DataStore: ObservableObject {
#AppStorage("val1")
var val1: Bool = false {
didSet {
print("did set val1")
}
}
#AppStorage("val2")
var val2: Bool = false {
didSet {
print("did set val2")
}
}
func boundVal2() -> Binding<Bool> {
return $val2
}
}
When you toggle the first value you get the didSet call, but when you toggle the second value you don't get it.
It turns out that you need to use a Binding object to pass it back, like so:
func boundVal2() -> Binding<Bool> {
Binding(
get: { self.val2 },
set: { self.val2 = $0 }
)
}

Is didSet on a #Binding file specific?

Essentially, I'm nesting #Binding 3 layers deep.
struct LayerOne: View {
#State private var doubleValue = 0.0
var body: some View {
LayerTwo(doubleValue: $doubleValue)
}
}
struct LayerTwo: View {
#Binding var doubleValue: Double {
didSet {
print(doubleValue)
}
}
var body: some View {
LayerThree(doubleValue: $doubleValue)
}
}
struct LayerThree: View {
#Binding var doubleValue: Double {
didSet {
print(doubleValue) // Only this print gets run when doubleValue is updated from this struct
}
}
var body: Some view {
// Button here changes doubleValue
}
}
Whichever struct I change doubleValue in is the one where the didSet will get run, so for example if I change it in LayerThree only that one will print, none of the others will.
I am able to watch for changes with .onChange(of: doubleValue) which will then get run when it changes but it's not making sense to me why didSet won't get run except on the struct where it's changed from.
Is #Binding struct specific?
Using property observers like didSet on values wrapped in PropertyWrappers will not have the "normal" effect because the value is being set inside the wrapper.
In SwiftUI, if you want to trigger an action when a value changes, you should use the onChange(of:perform:) modifier.
struct LayerTwo: View {
#Binding var doubleValue: Double
var body: some View {
LayerThree(doubleValue: $doubleValue)
.onChange(of: doubleValue) { newValue
print(newValue)
}
}
}
To see why this happens, we can unveil the syntactic sugar of property wrappers. #Binding var doubleValue: Double translates to:
private var _doubleValue: Binding<Double>
var doubleValue: Double {
get { _doubleValue.wrappedValue }
set { _doubleValue.wrappedValue = newValue }
}
init(doubleValue: Binding<Double>) {
_doubleValue = doubleValue
}
Whatever you do in didSet will be put after the line _doubleValue.wrappedValue = newValue. It should be very obvious why when you update doubleValue in layer 3, The didSet of doubleValue in layer 2 or 1 doesn't get called. They are simply different computed properties!
swiftPunk's solution works by creating a new binding whose setter sets the struct's doubleValue, hence calling didSet:
Binding(get: { return doubleValue },
set: { newValue in doubleValue = newValue }
// ^^^^^^^^^^^^^^^^^^^^^^
// this will call didSet in the current layer
Now all working:
struct ContentView: View {
var body: some View {
LayerOne()
}
}
struct LayerOne: View {
#State private var doubleValue:Double = 0.0 {
didSet {
print("LayerOne:", doubleValue)
}
}
var body: some View {
LayerTwo(doubleValue: Binding(get: { return doubleValue }, set: { newValue in doubleValue = newValue } ))
}
}
struct LayerTwo: View {
#Binding var doubleValue: Double {
didSet {
print("LayerTwo:", doubleValue)
}
}
var body: some View {
LayerThree(doubleValue: Binding(get: { return doubleValue }, set: { newValue in doubleValue = newValue } ))
}
}
struct LayerThree: View {
#Binding var doubleValue: Double {
didSet {
print("LayerThree:", doubleValue)
}
}
var body: some View {
Text(String(describing: doubleValue))
Button("update value") {
doubleValue = Double.random(in: 0.0...100.0)
}
.padding()
}
}
print results:
LayerOne: 64.58963263686678
LayerTwo: 64.58963263686678
LayerThree: 64.58963263686678

Bridging Optional Binding to Non-Optional Child (SwiftUI)

I have a parent state that might exist:
class Model: ObservableObject {
#Published var name: String? = nil
}
If that state exists, I want to show a child view. In this example, showing name.
If name is visible, I'd like it to be shown and editable. I'd like this to be two-way editable, that means if Model.name changes, I'd like it to push to the ChildUI, if the ChildUI edits this, I'd like it to reflect back to Model.name.
However, if Model.name becomes nil, I'd like ChildUI to hide.
When I do this, via unwrapping of the Model.name, then only the first value is captured by the Child who is now in control of that state. Subsequent changes will not push upstream because it is not a Binding.
Question
Can I have a non-optional upstream bind to an optional when it exists? (are these the right words?)
Complete Example
import SwiftUI
struct Child: View {
// within Child, I'd like the value to be NonOptional
#State var text: String
var body: some View {
TextField("OK: ", text: $text).multilineTextAlignment(.center)
}
}
class Model: ObservableObject {
// within the parent, value is Optional
#Published var name: String? = nil
}
struct Parent: View {
#ObservedObject var model: Model = .init()
var body: some View {
VStack(spacing: 12) {
Text("Demo..")
// whatever Child loads the first time will retain
// even on change of model.name
if let text = model.name {
Child(text: text)
}
// proof that model.name changes are in fact updating other state
Text("\(model.name ?? "<waiting>")")
}
.onAppear {
model.name = "first change of optionality works"
loop()
}
}
#State var count = 0
func loop() {
async(after: 1) {
count += 1
model.name = "updated: \(count)"
loop()
}
}
}
func async(_ queue: DispatchQueue = .main,
after: TimeInterval,
run work: #escaping () -> Void) {
queue.asyncAfter(deadline: .now() + after, execute: work)
}
struct OptionalEditingPreview: PreviewProvider {
static var previews: some View {
Parent()
}
}
Child should take a Binding to the non-optional string, rather than using #State, because you want it to share state with its parent:
struct Child: View {
// within Child, I'd like the value to be NonOptional
#Binding var text: String
var body: some View {
TextField("OK: ", text: $text).multilineTextAlignment(.center)
}
}
Binding has an initializer that converts a Binding<V?> to Binding<V>?, which you can use like this:
if let binding = Binding<String>($model.name) {
Child(text: binding)
}
If you're getting crashes from that, it's a bug in SwiftUI, but you can work around it like this:
if let text = model.name {
Child(text: Binding(
get: { model.name ?? text },
set: { model.name = $0 }
))
}
Bind your var like this. Using custom binding and make your child view var #Binding.
struct Child: View {
#Binding var text: String //<-== Here
// Other Code
if model.name != nil {
Child(text: Binding($model.name)!)
}

Init for a SwiftUI class with a #Binding var

I have a class which I want to initialize with a Binding var that is set in another View.
View ->
struct CoverPageView: View {
#State var numberOfNumbers:Int
var body: some View {
NavigationView {
GeometryReader { geometry in
VStack(alignment: .center, spacing: 0){
TextField("Multiplication Upto:", value: self.$numberOfNumbers, formatter: NumberFormatter())
}
}
}
}
CLASS WHICH NEEDS TO BE INITIALIZED USING THE #Binding var $numberofNumbers -
import SwiftUI
class MultiplicationPractice:ObservableObject {
#Binding var numberOfNumbers:Int
var classNumofNumbers:Int
init() {
self.classNumofNumbers = self.$numberOfNumbers
}
}
The init statement obviously gives the error that self is not initialized and the instance var is being used to initialize which is not allowed.
How do I circumvent this? The class needs to be initialized with the number the user enters on the first view. I have written approx. code here so ignore any typos please.
Typically you'd initialize MultiplicationPractice in CoverPageView with a starting value:
#ObservedObject var someVar = MultiplicationPractice(NoN:123)
And of course, add a supporting init statement:
class MultiplicationPractice:ObservableObject {
init(NoN: Int) {
self.numberOfNumbers = val
}
and you wouldn't want to wrap your var with #Binding, instead wrap it with #Published:
class MultiplicationPractice:ObservableObject {
#Published var numberOfNumbers:Int
...
In your particular case I would even drop the numberOfNumbers var in your CoverPageView, and instead use the direct variable of the above someVar:
struct CoverPageView: View {
//removed #State var numberOfNumbers:Int
#ObservedObject var someVar = MultiplicationPractice(123)
...
TextField("Multiplication Upto:", value: self.$someVar.numberOfNumbers, formatter: NumberFormatter())
You'll notice that I passed in the sub-var of the #ObservedObject as a binding. We can do this with ObservableObjects.
Edit
I see now what you're trying to do, you want to pass a binding along across your ViewModel, and establish an indirect connection between your view and model. While this may not be the way I'd personally do it, I can still provide a working example.
Here is a simple example using your struct names:
struct MultiplicationGame {
#Binding var maxNumber:String
init(maxNumber: Binding<String>) {
self._maxNumber = maxNumber
print(self.maxNumber)
}
}
class MultiplicationPractice:ObservableObject {
var numberOfNumbers: Binding<String>
#Published var MulGame:MultiplicationGame
init(numberOfNumbers: Binding<String> ) {
self.numberOfNumbers = numberOfNumbers
self.MulGame = MultiplicationGame(maxNumber: numberOfNumbers)
}
}
struct ContentView: View {
#State var someText: String
#ObservedObject var mulPractice: MultiplicationPractice
init() {
let state = State(initialValue: "")
self._someText = state
self.mulPractice = MultiplicationPractice(numberOfNumbers: state.projectedValue)
}
var body: some View {
TextField("put your text here", text: $someText)
}
}
Okay, I don't really understand your question so I'm just going to list a few examples and hopefully one of them will be what you're looking for.
struct SuperView: some View {
#State var value: Int = 0
var body: some View {
SubView(value: self.$value)
}
}
struct SubView: View {
#Binding var value: Int
// This is the same as the compiler-generated memberwise initializer
init(value: Binding<Int>) {
self._value = value
}
var body: some View {
Text("\(value)")
}
}
If I misunderstood and you're just trying to get the current value, do this
struct SuperView: some View {
#State var value: Int = 0
var body: some View {
SubView(value: self.value)
}
}
struct SubView: View {
let value: Int
// This is the same as the compiler-generated memberwise initializer
init(value: Int) {
self.value = value
}
var body: some View {
Text("\(value)")
}
}

setting computed property in a SwiftUI view doesn't compile

Trying to set the computed property s in a SwiftUI view gets compiler error "Cannot assign to property: 'self' is immutable".
How do I have to I call the setter?
struct Test: View{
#State var _s = "test"
#State var _s2 = true
private var s : String
{ get { _s }
set (new)
{ _s = "no test"
_s2 = false
// do something else
}
}
var body: some View
{ Text("\(s)")
.onTapGesture {
self.s = "anyting" // compiler error
}
}
}
Aha... I see. Just use non mutating set
private var s : String
{ get { _s }
nonmutating set (new)
{ _s = "no test"
_s2 = false
// do something else
}
}
That is, why you already have #State property wrapper in your View.
struct Test: View{
#State var s = "test"
var body: some View {
Text("\(s)")
.onTapGesture {
self.s = "anyting" // compiler error
}
}
}
You able to change s directly from your code because s is wrapped with #State.
this is functional equivalent of the above
struct Test: View{
let s = State<String>(initialValue: "alfa")
var body: some View {
VStack {
Text("\(s.wrappedValue)")
.onTapGesture {
self.s.wrappedValue = "beta"
}
}
}
}
Or if Binding is needed
struct Test: View{
let s = State<String>(initialValue: "alfa")
var body: some View {
VStack {
TextField("label", text: s.projectedValue)
}
}
}