Bridging Optional Binding to Non-Optional Child (SwiftUI) - swift

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

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

Conditional reaction to update a Binding value in onChange in SwiftUI?

I am updating State value in 2 Views via State and Binding, my goal is that onChange would fire to work only if the update happens from ContentView, I already using this down code for this job, it works but I have to use an extra Variable for this job, I want to know if there is any better way of doing this work with more advanced and better way, also I am not interested to using combine for this.
struct ContentView: View {
#State private var stringValue: String = "Hello"
var body: some View {
Text(stringValue).padding()
Button("update stringValue from ContentView") {
stringValue += " updated from ContentView!"
}
TextView(stringValue: $stringValue)
}
}
struct TextView: View {
#Binding var stringValue: String
#State private var internalStringValue: String = String()
var body: some View {
Text(stringValue).padding()
.onChange(of: stringValue) { newValue in
if newValue != internalStringValue {
print("stringValue updated!")
}
}
Button("update stringValue from TextView") {
internalStringValue = stringValue + " updated from TextView!"
stringValue = internalStringValue
}
}
}

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

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

Why isn't a closure in #State array property triggering state change?

A test view has #State showTitle, title and items where the title value text is controlled by a closure assigned to a CTA show title.
When the showTitle state changes, the value presented in the body Content of test view changes accordingly:
Text({ self.showTitle ? "Yes, showTitle!" : "No, showTitle!" }())
While the case where the closure is a value in the array items does not change. Why isn't the closure triggering the title state?
NestedView(title: $0.title())
I've done tests with both Foobar as Struct and Class.
import SwiftUI
struct Foobar: Identifiable {
var id: UUID = UUID()
var title: () -> String
init (title: #escaping () -> String) {
self.title = title
}
}
struct test: View {
#State var showTitle: Bool = true
#State var title: String
#State var items: [Foobar]
var body: some View {
VStack {
Group {
Text("Case 1")
Text({ self.showTitle ? "Yes, showTitle!" : "No, showTitle!" }())
}
Group {
Text("Case 2")
ForEach (self.items, id: \.id) {
NestedView(title: $0.title())
}
}
Button("show title") {
print("show title cb")
self.showTitle.toggle()
}
}.onAppear {
let data = ["hello", "world", "test"]
for title in data {
self.items.append(Foobar(title: { self.showTitle ? title : "n/a" }))
}
}
}
}
struct NestedView: View {
var title: String
var body: some View {
Text("\(title)")
}
}
What's expected is that "Case 2" to have a similar side-effect we have in "Case 1" that should display "n/a" on showTitle toggle.
Output:
From what I understand, the reason why the initial code does not work is related to the showTitle property that is passed to the Array and holds a copy of the value
You were right to blame the closure capturing the value at the onAppear time. Basically due to this, SwiftUI doesn't know to refresh the list when the showTitle value changes, as there's no Binding involved that SwiftUI can use to know when to re-render the list.
I can provide two alternative solutions, that don't require another class just to hold the bool value. Both solutions involve communicating to SwiftUI that you need the showTitle binding to refresh the titles.
Don't use a closure for title, defer the title computation to the list builder:
struct Foobar: Identifiable {
var id: UUID = UUID()
var title: String
init (title: String) {
self.title = title
}
}
...
ForEach (self.items, id: \.id) {
NestedView(title: self.showTitle ? $0.title : "n/a" )
}
...
.onAppear {
let data = ["hello", "world", "test"]
self.items = data.map { Foobar(title: $0) }
}
Convert the title closure to a (Binding<Bool>) -> String one, inject the $showTitle binding from the view:
struct Foobar: Identifiable {
var id: UUID = UUID()
var title: ((Binding<Bool>) -> String)
init (title: #escaping (Binding<Bool>) -> String) {
self.title = title
}
}
...
ForEach (self.items, id: \.id) {
// here we pass the $showTitle binding, thus SwiftUI knows to re-render
// the view when the binding value is updated
NestedView(title: $0.title(self.$showTitle))
}
...
.onAppear {
let data = ["hello", "world", "test"]
self.items = data.map { Foobar(title: { $0.wrappedValue ? title : "n/a" })) }
}
Personally, I'd go with the first solution, since it better transmit the intent.
From what I understand, the reason why the initial code does not work is related to the showTitle property that is passed to the Array and holds a copy of the value (creates a unique copy of the data).
I did think #State would make it controllable and mutable, and the closure would capture and store the reference (create a shared instance). In other words, to have had a reference, instead of a copied value! Feel free to correct me, if that's not the case, but that's what it looks like based on my analysis.
With that being said, I kept the initial thought process, I still want to pass a closure to the Array and have the state changes propagated, cause side-effects, accordingly to any references to it!
So, I've used the same pattern but instead of relying on a primitive type for showTitle Bool, created a Class that conforms to the protocol ObservableObject: since Classes are reference types.
So, let's have a look and see how this worked out:
import SwiftUI
class MyOption: ObservableObject {
#Published var option: Bool = false
}
struct Foobar: Identifiable {
var id: UUID = UUID()
var title: () -> String
init (title: #escaping () -> String) {
self.title = title
}
}
struct test: View {
#EnvironmentObject var showTitle: MyOption
#State var title: String
#State var items: [Foobar]
var body: some View {
VStack {
Group {
Text("Case 1")
Text(self.showTitle.option ? "Yes, showTitle!" : "No, showTitle!")
}
Group {
Text("Case 2")
ForEach (self.items, id: \.id) {
NestedView(title: $0.title())
}
}
Button("show title") {
print("show title cb")
self.showTitle.option.toggle()
print("self.showTitle.option: ", self.showTitle.option)
}
}.onAppear {
let data = ["hello", "world", "test"]
for title in data {
self.items.append(Foobar(title: { self.showTitle.option ? title : "n/a" }))
}
}
}
}
struct NestedView: View {
var title: String
var body: some View {
Text("\(title)")
}
}
The result as expected:

Binding to a read-only property in SwiftUI

I have a model type which looks like this:
enum State {
case loading
case loaded([String])
case failed(Error)
var strings: [String]? {
switch self {
case .loaded(let strings): return strings
default: return nil
}
}
}
class MyApi: ObservableObject {
private(set) var state: State = .loading
func fetch() {
... some time later ...
self.state = .loaded(["Hello", "World"])
}
}
and I'm trying to use this to drive a SwiftUI View.
struct WordListView: View {
#EnvironmentObject var api: MyApi
var body: some View {
ZStack {
List($api.state.strings) {
Text($0)
}
}
}
}
It's about here that my assumptions fail. I'm trying to get a list of the strings to render in my List when they are loaded, but it won't compile.
The compiler error is Generic parameter 'Subject' could not be inferred, which after a bit of googling tells me that bindings are two-way, so won't work with both my private(set) and the var on the State enum being read-only.
This doesn't seem to make any sense - there is no way that the view should be able to tell the api whether or not it's loading, that definitely should be a one-way data flow!
I guess my question is either
Is there a way to get a one-way binding in SwiftUI - i.e. some of the UI will update based on a value it cannot change.
or
How should I have architected this code! It's very likely that I'm writing code in a style which doesn't work with SwiftUI, but all the tutorials I can see online neatly ignore things like loading / error states.
You don't actually need a binding for this.
An intuitive way to decide if you need a binding or not is to ask:
Does this view need to modify the passed value ?
In your case the answer is no. The List doesn't need to modify api.state (as opposed to a textfield or a slider for example), it just needs the current value of it at any given moment. That is what #State is for but since the state is not something that belongs to the view (remember, Apple says that each state must be private to the view) you're correctly using some form of an ObservableObject (through Environment).
The final missing piece is to mark any of your properties that should trigger an update with #Published, which is a convenience to fire objectWillChange signals and instruct any observing view to recalculate its body.
So, something like this will get things done:
class MyApi: ObservableObject {
#Published private(set) var state: State = .loading
func fetch() {
self.state = .loaded(["Hello", "World"])
}
}
struct WordListView: View {
#EnvironmentObject var api: MyApi
var body: some View {
ZStack {
List(api.state.strings ?? [], id: \.self) {
Text($0)
}
}
}
}
Not exactly the same problem as I had, but the following direction can help you possibly find a good result when bindings are done with only reads.
You can create a custom binding using a computed property.
I needed to do exactly this in order to show an alert only when one was passed into an overlay.
Code looks something along these lines :
struct AlertState {
var title: String
}
class AlertModel: ObservableObject {
// Pass a binding to an alert state that can be changed at
// any time.
#Published var alertState: AlertState? = nil
#Published var showAlert: Bool = false
init(alertState: AnyPublisher<AlertState?, Never>) {
alertState
.assign(to: &$alertState)
alertState
.map { $0 != nil }
.assign(to: &$showAlert)
}
}
struct AlertOverlay<Content: View>: View {
var content: Content
#ObservedObject var alertModel: AlertModel
init(
alertModel: AlertModel,
#ViewBuilder content: #escaping () -> Content
) {
self.alertModel = alertModel
self.content = content()
}
var body: some View {
ZStack {
content
.blur(radius: alertModel.showAlert
? UserInterfaceStandards.blurRadius
: 0)
}
.alert(isPresented: $alertModel.showAlert) {
guard let alertState = alertModel.alertState else {
return Alert(title: Text("Unexected internal error as occured."))
}
return Alert(title: Text(alertState.title))
}
}
}