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

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

Related

Manipulating binding variables in SwiftUI

Suppose I have a string that's binding:
#Binding var string: String
But, I want to manipulate that string, and then pass it to a child view.
struct ViewOne: View {
#State var string: String = "Hello"
var body: some View {
ViewTwo(string: string + " World") // this is invalid, as ViewTwo requires Binding<String>
}
}
struct ViewTwo: View {
#Binding var string: String
var body: some View {
Text(string)
}
}
How should I go about manipulating that string, such that it will update the UI when the state changes? Let's assume that I want to re-use ViewTwo for different components, and so I would want to manipulate the string before it is passed to the view.
A computed variable doesn't work, as it isn't Binding
private var fixedString: String {
return string + " World"
}
And I don't want to create a binding variable, because the setter makes no sense in this context
private var fixedString: Binding<String> {
Binding<String> (
get: {
string + " World"
}, set: {
// this function doesn't make sense here - how would it update the original variable?
}
)
}
Am I just using #State and #Binding wrong?
Just remove the #Binding inside ViewTwo:
struct ViewTwo: View {
var string: String /// no need for the `#Binding`!
var body: some View {
Text(string)
}
}
SwiftUI will update ViewTwo even if you don't have the #Binding (it basically re-renders the entire body whenever a #State or #Published var gets changed).
You only need Bindings when you need to update/set the original #State or #Published property. In that case you'd add something like the var fixedString: Binding<String> in your question.

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

Making custom get {} set{} to work like dynamic proxy/shortcut to different objects in Array. (SwiftUI) [duplicate]

This question already has answers here:
How to change a value of struct that is in array?
(2 answers)
Closed 1 year ago.
I'm trying to achieve a two way binding-like functionality.
I have a model with an array of identifiable Items, var selectedID holding a UUID of selected Item, and var proxy which has get{} that looks for an Item inside array by UUID and returns it.
While get{} works well, I can't figure out how to make proxy mutable to change values of selected Item by referring to proxy.
I have tried to implement set{} but nothing works.
import SwiftUI
var words = ["Aaaa", "Bbbb", "Cccc"]
struct Item: Identifiable {
var id = UUID()
var word: String
}
class Model: ObservableObject {
#Published var items: [Item] = [Item(word: "One"), Item(word: "Two"), Item(word: "Three")]
#Published var selectedID: UUID?
var proxy: Item? {
set {
// how to set one property of Item?, but not the whole Item here?
}
get {
let index = items.firstIndex(where: { $0.id == selectedID })
return index != nil ? items[index!] : nil
}
}
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
VStack {
// monitoring
MonitorkVue(model: model)
//selections
HStack {
ForEach(model.items.indices, id:\.hashValue) { i in
SelectionVue(item: $model.items[i], model: model)
}
}
}.padding()
}
}
struct MonitorkVue: View {
#ObservedObject var model: Model
var body: some View {
VStack {
Text(model.proxy?.word ?? "no proxy")
// 3rd: cant make item change by referring to proxy
// in order this to work, proxy's set{} need to be implemented somehow..
Button {
model.proxy?.word = words.randomElement()!
} label: {Text("change Proxy")}
}
}
}
struct SelectionVue: View {
#Binding var item: Item
#ObservedObject var model: Model
var body: some View {
VStack {
Text(item.word).padding()
// 1st: making selection
Button {
model.selectedID = item.id } label: {Text("SET")
}.disabled(item.id != model.selectedID ? false : true)
// 2nd: changing item affects proxy,
// this part works ok
Button {
item.word = words.randomElement()!
}label: {Text("change Item")}
}
}
}
Once you SET selection you can randomize Item and proxy will return new values.
But how to make it works the other way around when changing module.proxy.word = "Hello" would affect selected Item?
Does anyone knows how to make this two-way shortct?
Thank You
Here is a correction and some fix:
struct Item: Identifiable {
var id = UUID()
var word: String
}
class Model: ObservableObject {
#Published var items: [Item] = [Item(word: "One"), Item(word: "Two"), Item(word: "Three")]
#Published var selectedID: UUID?
var proxy: Item? {
get {
if let unwrappedIndex: Int = items.firstIndex(where: { value in (selectedID == value.id) }) { return items[unwrappedIndex] }
else { return nil }
}
set(newValue) {
if let unwrappedItem: Item = newValue {
if let unwrappedIndex: Int = items.firstIndex(where: { value in (unwrappedItem.id == value.id) }) {
items[unwrappedIndex] = unwrappedItem
}
}
}
}
}

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

Convert a #State into a Publisher

I want to use a #State variable both for the UI and for computing a value.
For example, let's say I have a TextField bound to #State var userInputURL: String = "https://". How would I take that userInputURL and connect it to a publisher so I can map it into a URL.
Pseudo code:
$userInputURL.publisher()
.compactMap({ URL(string: $0) })
.flatMap({ URLSession(configuration: .ephemeral).dataTaskPublisher(for: $0).assertNoFailure() })
.eraseToAnyPublisher()
You can't convert #state to publisher, but you can use ObservableObject instead.
import SwiftUI
final class SearchStore: ObservableObject {
#Published var query: String = ""
func fetch() {
$query
.map { URL(string: $0) }
.flatMap { URLSession.shared.dataTaskPublisher(for: $0) }
.sink { print($0) }
}
}
struct ContentView: View {
#StateObject var store = SearchStore()
var body: some View {
VStack {
TextField("type something...", text: $store.query)
Button("search") {
self.store.fetch()
}
}
}
}
You can also use onChange(of:) to respond to #State changes.
struct MyView: View {
#State var userInputURL: String = "https://"
var body: some View {
VStack {
TextField("search here", text: $userInputURL)
}
.onChange(of: userInputURL) { _ in
self.fetch()
}
}
func fetch() {
print("changed", userInputURL)
// ...
}
}
Output:
changed https://t
changed https://ts
changed https://tsr
changed https://tsrs
changed https://tsrst
The latest beta has changed how variables are published so I don't think that you even want to try. Making ObservableObject classes is pretty easy but you then want to add a publisher for your own use:
class ObservableString: Combine.ObservableObject, Identifiable {
let id = UUID()
let objectWillChange = ObservableObjectPublisher()
let publisher = PassthroughSubject<String, Never>()
var string: String {
willSet { objectWillChange.send() }
didSet { publisher.send(string) }
}
init(_ string: String = "") { self.string = string }
}
Instead of #State variables you use #ObservableObject and remember to access the property string directly rather than use the magic that #State uses.
After iOS 14.0, you can access to Publisher.
struct MyView: View {
#State var text: String?
var body: some View {
Text(text ?? "")
.onReceive($text.wrappedValue.publisher) { _ in
let publisher1: Optional<String>.Publisher = $text.wrappedValue.publisher
// ... or
let publisher2: Optional<String>.Publisher = _text.wrappedValue.publisher
}
}
}