SwiftUI Can I use Binding get set custom binding with #Binding property wrapper? - binding

How can I use Binding(get: { }, set: { }) custom binding with #Binding property on SwiftUI view. I have used this custom binding successfully with #State variable but doesn't know how to apply it to #Binding in subview initializer. I need it to observe changes assigned to #Binding property by parent class in order to execute code with some side effects!

Here is possible approach. The demo shows two-directional channel through adapter binding between main & dependent views. Due to many possible callback on update there might be needed to introduce redundancy filtering, but that depends of what is really required and out of scope.
Demo code:
struct TestBindingIntercept: View {
#State var text = "Demo"
var body: some View {
VStack {
Text("Current: \(text)")
TextField("", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
Divider()
DependentView(value: $text)
}
}
}
struct DependentView: View {
#Binding var value: String
private var adapterValue: Binding<String> {
Binding<String>(get: {
self.willUpdate()
return self.value
}, set: {
self.value = $0
self.didModify()
})
}
var body: some View {
VStack {
Text("In Next: \(adapterValue.wrappedValue)")
TextField("", text: adapterValue)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
private func willUpdate() {
print(">> run before UI update")
}
private func didModify() {
print(">> run after local modify")
}
}

Related

How to observe change in #StateObject in SwiftUI?

I am having property with #StateObject, I am trying to observe change in viewmodel, I am able to print correct result but can not able to show on screen as view is not refreshing.
Tried using binding but not worked because of #StateObject
import SwiftUI
struct AbcView: View {
#StateObject var abcViewModel: AbcViewModel
init(abcViewModel: AbcViewModel) {
self._abcViewModel = StateObject(wrappedValue: abcViewModel)
}
var body: some View {
VStack(alignment: .leading) {
ZStack(alignment: .top) {
ScrollView {
Text("some txt")
}
.overlay(
VStack {
TopView(content: classViews(data: $abcViewModel.somedata, abcViewModel: abcViewModel))
Spacer()
}
)
}
}
}
}
func classViews(data: Binding<[SomeData]>, abcViewModel: AbcViewModel) -> [AnyView] {
var views: [AnyView] = []
for element in data {
views.append(
VStack(spacing: 0) {
HStack {
print("\(abcViewModel.title(Id: Int(element.dataId.wrappedValue ?? "")) )") // printing correct value
Text(abcViewModel.title(Id: Int(element.dataId.wrappedValue ?? ""))) // want to observe change here
}
}
.convertToAnyView())
}
return views
}
If you are injecting your AbcViewModel into AbcView you should use #ObserverdObject instead of #StateObject , full explanation here Also you should conform tour AbcViewModel to ObservableObject and make your desired property #Published if you want to trigger the change in View . Here is simplified code example:
Making AbcViewModel observable:
class AbcViewModel: ObservableObject {
#Published var dataID: String = "" //by changing the #Published proprty you trigger change in View using it
}
store AbcViewModel as #ObserverdObject:
struct AbcView: View {
#ObservedObject var abcViewModel: AbcViewModel
init(abcViewModel: AbcViewModel) {
self.abcViewModel = abcViewModel
}
var body: some View {
//...
}
}
If you now use your AbcViewModel dataID property anywhere in the project, and you change its value, the property will publish the change and your View (struct) will be rebuilded. Use the same pattern for creating TopView and assigning AbcViewModel to it the same way.

SwiftUI: Why onReceive run duplicate when binding a field inside ObservableObject?

This is my code and "print("run to onReceive (text)")" run twice when text change (like a image). Why? and thank you!
import SwiftUI
class ContentViewViewModel : ObservableObject {
#Published var text = ""
}
struct ContentView: View {
#StateObject private var viewModel = ContentViewViewModel()
var body: some View {
ZStack {
TextField("pla", text: $viewModel.text)
.padding()
}
.onReceive(viewModel.$text) { text in
print("run to onReceive \(text)")
}
}
}
I think it's because the view is automatically updated as your #Published property in your ViewModel changes and the .onReceive modifier updates the view yet again due to the 2 way binding created by viewModel.$text resulting in the view being updated twice each time.
If you want to print the text as it changes you can use the .onChange modifier instead.
class ContentViewViewModel: ObservableObject {
#Published var text = ""
}
struct ContentView: View {
#StateObject private var viewModel = ContentViewViewModel()
var body: some View {
ZStack {
TextField("pla", text: $viewModel.text)
.padding()
}.onChange(of: viewModel.text) { newValue in
print("run to onChange \(newValue)")
}
}
}
onChanged in SwiftUI
Because you have a #Published variable inside the #StateObject of your view, the changes in that variable will automatically update the view.
If you add the .onReceive() method, you will:
update the view because you have the #Published var
update it again when the .onReceive() method listens to the change
Just delete the .onReceive() completely and it will work:
class ContentViewViewModel : ObservableObject {
#Published var text = ""
}
struct ContentView: View {
#StateObject private var viewModel = ContentViewViewModel()
var body: some View {
ZStack {
TextField("pla", text: $viewModel.text)
.padding()
}
// It still works without this modifier
//.onReceive(viewModel.$text) { text in
// print("run to onReceive \(text)")
//}
}
}

How to best pass data for form editing in swuiftui (while having that data available to parent view and other subviews)?

I have a SwiftUI form in a MyFormSubView.swift where I have multiple #State variables representing individual fields like Text, etc. My issue is my parent view "ContentView.swift" also needs access to this information, and other subviews "OtherView.swift" also would benefit from access for display or editing. My current approach, is to change all the #State to #Binding, which creates a headache because some forms could have up to 20 fields with some optional... what is the best way to handle this? Is there a way to simply pass an object and have that be 'editable'?
Approaches:
(Current, problem approach) Have multiple individual variables declared as #State
in the ContentView.swift, and pass each individual variable into
MyFormSubView.swift with those variables having #Binding in front of
them that are mapped to swiftui elements to show up as 'placeholder
text' in textboxes, etc. This is bad as I have potentially up to 30 fields with some being optional.
(What I Think I Desire) Have identifiable model with all the
fields (and maybe pass this model into the MyFormSubView.swift, and
if it's possible, bind to it and just have it such that each field
is $mymodel.field1, $mymodel.field2, etc... which eliminates the
need to have 30+ variables passed into this thing.
(Maybe Better?) Use an #ObservableObject.
Is #2 possible? Or is there an even better way? Sample code would be great!
There are several ways to pass data like this across Views. Here is a quick implementation outlining 4 approaches.
You can use an #ObservableObject to reference a class with all of your data inside. The variables are #Published, which allows the View to update in the same way a #State variable would.
You can use an #StateObject. This is the same as #ObservableObject, except it will only initialize once and if the view re-renders the variable will persist (whereas an #ObservedObject would reinitialize). Read more about the difference here.
You can use an #EnvironmentObject. This is the same as #ObservedObject, except it is stored in the Environment, so you don't have to manually pass it between views. This is best when you have a complex view hierarchy and not every view needs a reference to the data.
You can create a custom Model and use a #State variable.
All of these methods work, but based on your description, I'd say the 2nd method is probably best for your situation.
class DataViewModel: ObservableObject {
#Published var text1: String = "One"
#Published var text2: String = "Two"
#Published var text3: String = "Three"
}
struct DataModel {
var text1: String = "Uno"
var text2: String = "Dos"
var text3: String = "Tres"
}
struct AppView: View {
var body: some View {
MainView()
.environmentObject(DataViewModel())
}
}
struct MainView: View {
#StateObject var dataStateViewModel = DataViewModel()
#ObservedObject var dataObservedViewModel = DataViewModel()
#EnvironmentObject var dataEnvironmentViewModel: DataViewModel
#State var dataStateModel = DataModel()
#State var showSheet: Bool = false
#State var showOtherView: Bool = false
var body: some View {
VStack(spacing: 20) {
Text(dataStateViewModel.text1)
.foregroundColor(.red)
Text(dataObservedViewModel.text2)
.foregroundColor(.blue)
Text(dataEnvironmentViewModel.text3)
.foregroundColor(.green)
Text(dataStateModel.text1)
.foregroundColor(.purple)
Button(action: {
showSheet.toggle()
}, label: {
Text("Button 1")
})
.sheet(isPresented: $showSheet, content: {
FormView(dataStateViewModel: dataStateViewModel, dataObservedViewModel: dataObservedViewModel, dataStateModel: $dataStateModel)
.environmentObject(dataEnvironmentViewModel) // Sheet is a new environment
})
Button(action: {
showOtherView.toggle()
}, label: {
Text("Button 2")
})
if showOtherView {
ThirdView(dataStateViewModel: dataStateViewModel, dataObservedViewModel: dataObservedViewModel, dataStateModel: $dataStateModel)
}
}
}
}
struct FormView: View {
#StateObject var dataStateViewModel: DataViewModel
#ObservedObject var dataObservedViewModel: DataViewModel
#EnvironmentObject var dataEnvironmentViewModel: DataViewModel
#Binding var dataStateModel: DataModel
#Environment(\.presentationMode) var presentationMode
var body: some View {
Form(content: {
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("BACK")
})
Text("EDIT TEXT FIELDS:")
TextField("Placeholder 1", text: $dataStateViewModel.text1)
.foregroundColor(.red)
TextField("Placeholder 2", text: $dataObservedViewModel.text2)
.foregroundColor(.blue)
TextField("Placeholder 3", text: $dataEnvironmentViewModel.text3)
.foregroundColor(.green)
TextField("Placeholder 4", text: $dataStateModel.text1)
.foregroundColor(.purple)
})
}
}
struct ThirdView: View {
#StateObject var dataStateViewModel: DataViewModel
#ObservedObject var dataObservedViewModel: DataViewModel
#EnvironmentObject var dataEnvironmentViewModel: DataViewModel
#Binding var dataStateModel: DataModel
var body: some View {
VStack(spacing: 20) {
Text(dataStateViewModel.text1)
.foregroundColor(.red)
Text(dataObservedViewModel.text2)
.foregroundColor(.blue)
Text(dataEnvironmentViewModel.text3)
.foregroundColor(.green)
Text(dataStateModel.text1)
.foregroundColor(.purple)
}
}
}
Use ObservalbeObject, here is simple example how you can share data:
Step 1 - Add some kind of state:
class AppState: ObservableObject {
#Published var value: String = ""
}
Step 2 - Pass state to ContentView via setting enviromentObject
ContentView()
.environmentObject(AppState())
Step 3 - Now AppState will be available in all child views of ContentView, so here is the code for ContentView and OtherView. OtherView has the TextField, text from will be saved to AppState and you can be able to see it, when you press 'back' from OtherView.
ContentView:
import SwiftUI
struct ContentView: View {
#EnvironmentObject var appState: AppState
var body: some View {
NavigationView {
VStack {
Text("Content View")
.padding()
NavigationLink(
destination: OtherView()
) {
Text("Open other view")
}
Text("Value from other view: \(appState.value)")
}
}
}
}
OtherView:
import SwiftUI
struct OtherView: View {
#EnvironmentObject var appState: AppState
#State var value: String = ""
var body: some View {
VStack {
Text("Other View")
.padding()
TextField("Enter value", text: Binding<String>(
get: { self.value },
set: {
self.value = $0
appState.value = $0
}
))
.frame(width: 200)
.padding()
}
}
}
This is just simple example, for more complex cases you can take a look on VIPER or MVVM patterns in Swift UI. For example, here:
https://www.raywenderlich.com/8440907-getting-started-with-the-viper-architecture-pattern
https://www.raywenderlich.com/4161005-mvvm-with-combine-tutorial-for-ios

How do I resolve Picker selection via Proxy?

Scenario: I want to handle the selection event whilst harvesting the selected item via a Picker.
This is in reference to an introduction discussion to the Picker Proxy.
This is what I have so far. I can't get the event handler to fire/activate/run doSomething().
import SwiftUI
struct ContentView: View {
var body: some View {
GeometryReader { _ in
VStack {
Text("PickerView")
.font(.headline)
.foregroundColor(.gray)
.padding(.top, 10)
Picker("test", selection: Binding(get: { "" }, set: { _ in
doSomething()
})) {
Text("Hello").id("1")
Text("Uncle").id("2")
Text("Ric").id("3")
}.labelsHidden()
}.background(RoundedRectangle(cornerRadius: 10)
.foregroundColor(Color.white).shadow(radius: 1))
}
.padding()
}
func doSomething() {
print("Hello Something!")
}
}
Note:
I don't know what to do with the get{} so I put a null string there to satisfy the compiler.
I tried evaluating the closure parameter (via print(*closure parameter*)) but got no value, so I placed a _ placeholder to satisfy the compiler.
How to I harvest the selection?
The closure parameter doesn't appear to work here; hence the placeholder. There aren't many examples to follow.
Here is a possible solution:
struct ContentView: View {
// extract picker values for easier access
private let items = ["Hello", "Uncle", "Ric"]
// store the currently selected value
#State private var selection = 0
// custom binding for the `selection`
var binding: Binding<Int> {
.init(get: {
selection
}, set: {
selection = $0
doSomething() // call another function after the `selection` is set
})
}
var body: some View {
Picker("test", selection: binding) {
ForEach(0 ..< items.count) { index in // use `ForEach` to quickly generate picker values
Text(items[index])
.tag(index) // use `tag` instead of `id`
}
}
.labelsHidden()
}
func doSomething() {
print("Hello Something!")
}
}
Discussion:
You can use onChange to trigger a side effect as the result of a value changing, such as an Environment key or a Binding...
(Apple doc.)
struct ContentView: View {
#State
private var selection = 0
let items = ["Hello", "Uncle", "Ric"]
var body: some View {
VStack {
Picker("", selection: $selection) {
ForEach(0..<items.count) {
Text(items[$0])
.tag($0)
}
}
.onChange(of: selection) { _ in
print("Hello Something!")
}
}
}
}

Do something when Toggle state changes

I am using SwiftUI and want to do something when toggle state changes but I cannot find a solution for this.
How can I call a function or do whatever when toggle state changes ?
#State var condition = false
Toggle(isOn: $condition) {
Text("Toggle text here")
}
If you want to do something whenever the toggle state change, you may use didSet.
struct ContentView: View {
#State var condition = false{
didSet{
print("condition changed to \(condition)")
}
}
var body: some View {
let bind = Binding<Bool>(
get:{self.condition},
set:{self.condition = $0}
)
return Toggle(isOn: bind){
Text("Toggle text here")
}
}
}
When we say #State var condition = true, it's type is State<Bool> and not Bool.
when we use binding in toggle ($condition in this case), toggle directly changes the stored value. Meaning it never changes the #State, so didSet will never be triggered.
So here we create and assign to let bind an instance of Binding<Value> struct, and use that Binding struct directly in toggle. Than when we set self.condition = $0, it will trigger didSet.
You can use the .onChange modifier like in the first example below.
However, keep in mind that in most cases we don't need it.
In case of a view setting, you bind to a #State bool value, then you use this bool to modify the view hierarchy, as in the first example.
In case of a change in your model, you bind the toggle to a #Published value in your model or model controller. In this case, the model or model controller receive the change from the switch, do whatever is requested, and the view is rerendered. It must have an #ObservedObject reference to your model.
I show how to use the .onChange modifier in the first example. It simply logs the state.
Remember there should be as few logic as possible in SwiftUI views.
By the way that was already true far before the invention of SwiftUI ;)
Example 1 - #State value. ( Local UI settings )
import SwiftUI
struct ToggleDetailView: View {
#State var isDetailVisible: Bool = true
var body: some View {
VStack {
Toggle("Display Detail", isOn: $isDetailVisible)
.onChange(of: self.isDetailVisible, perform: { value in
print("Value has changed : \(value)")
})
if isDetailVisible {
Text("Detail is visible!")
}
}
.padding(10)
.frame(width: 300, height: 150, alignment: .top)
}
}
struct ToggleDetailView_Previews: PreviewProvider {
static var previews: some View {
ToggleDetailView()
}
}
Example 2 - #ObservedObject value. ( Action on model )
import SwiftUI
class MyModel: ObservableObject {
#Published var isSolid: Bool = false
var string: String {
isSolid ? "This is solid" : "This is fragile"
}
}
struct ToggleModelView: View {
#ObservedObject var model: MyModel
var body: some View {
VStack {
Toggle("Solid Model", isOn: $model.isSolid)
Text(model.string)
}
.padding(10)
.frame(width: 300, height: 150, alignment: .top)
}
}
struct ToggleModelView_Previews: PreviewProvider {
static var previews: some View {
ToggleModelView(model: MyModel())
}
}
Try
struct ContentView: View {
#State var condition = false
var body: some View {
if condition {
callMe()
}
else {
print("off")
}
return Toggle(isOn: $condition) {
Text("Toggle text here")
}
}
func callMe() {
}
}
Karen please check
#State var isChecked: Bool = true
#State var index: Int = 0
Toggle(isOn: self.$isChecked) {
Text("This is a Switch")
if (self.isChecked) {
Text("\(self.toggleAction(state: "Checked", index: index))")
} else {
CustomAlertView()
Text("\(self.toggleAction(state: "Unchecked", index: index))")
}
}
Add function like this
func toggleAction(state: String, index: Int) -> String {
print("The switch no. \(index) is \(state)")
return ""
}
I tend to use:
Toggle(isOn: $condition, label: {
Text("Power: \(condition ? "On" : "Off")")
})
.padding(.horizontal, 10.0)
.frame(width: 225, height: 50, alignment: .center)

Resources