How to pass a FocusState variable to other views and allow those views to update the variable? [Swift] - swift

Currently, I have something like the following:
struct ViewA: View {
#FocusState private var focusedField: Bool
var body: some View {
ViewB(focusedField: $focusedField)
// some other views that read focusedField...
}
}
struct ViewB: View {
#State var focusedField: FocusState<Bool>.Binding
var body: some View {
Button(action: {
focusedField = true // ERROR: Cannot assign value of type 'Bool' to type 'FocusState<Bool>'
})
// ...
}
}
Seems like I can pass down focusedField with no problem but unable to update its value. How to solve this?

For consistency, maybe it's better to use the property wrapper #FocusedState.Binding instead.
struct ViewA: View {
#FocusState private var focusedField: Bool
var body: some View {
ViewB(focusedField: $focusedField)
// some other views that read focusedField...
}
}
struct ViewB: View {
#FocusState.Binding var focusedField: Bool
var body: some View {
Button(action: {
focusedField = true
}) {
Text("Tap me")
}
}
}
This allows to do focusedField = true instead of focusedField.wrappedValue = true

Instead of directly setting focusedField, use the wrappedValue property.
struct ViewA: View {
#FocusState private var focusedField: Bool
var body: some View {
ViewB(focusedField: $focusedField)
// some other views that read focusedField...
}
}
struct ViewB: View {
var focusedField: FocusState<Bool>.Binding
var body: some View {
Button(action: {
focusedField.wrappedValue = true /// assign `wrappedValue`
}) {
Text("Click me!") /// Side note: you need a label for the button too, otherwise your code won't compile
}
// ...
}
}

Related

SwiftUI polymorphic behaviour not working for View

protocol BackgroundContent: View{
}
struct BlueDivider: BackgroundContent {
var body: some View {
Divider()
.frame(minHeight: 1)
.background(.blue)
}
}
struct RedDivider: BackgroundContent {
var body: some View {
Divider()
.frame(minHeight: 1)
.background(.red)
}
}
var p: BackgroundContent = BlueDivider()
// Use of protocol 'BackgroundContent' as a type must be written 'any BackgroundContent'
p = RedDivider()
This always ask me to use
var p: any BackgroundContent = BlueDivider()
Is there any way to use generic type which accept any kind view?
Actually, I want to use view as a state like #State private var bgView: BackgroundContent = BlueDivider() which i want to change at runtime like bgView = RedDivider()
I have made my custome view to place some other view at runtime by using this state.
For your specific problem you can do something like this here:
struct SwiftUIView: View {
#State var isRed = false
var body: some View {
Devider()
.frame(height: 1)
.background(isRed ? Color.red : Color.blue)
}
}
It is complicated but i have found a solution of this problem. First thing i have done with ObservableObject. Here is my example.
protocol BaseBackgroundContent {
var color: Color { get set }
}
class BlueContent: BaseBackgroundContent {
var color: Color = .blue
}
class RedContent: BaseBackgroundContent {
var color: Color = .red
}
And i created a custom view for Divider in this case.
struct CustomDivider: View {
var backgroundContent: any BaseBackgroundContent
var body: some View {
Divider()
.background(backgroundContent.color)
}
}
And now i used a viewModel which can be observable, and the protocol has to be Published.
class ExampleViewModel: ObservableObject {
#Published var backgroundContent: any BaseBackgroundContent = RedContent()
func change() {
backgroundContent = BlueContent()
}
}
Final step is the view. This is a exampleView. If you click the button you will see the BlueContent which was RedContent
struct Example: View {
#ObservedObject var viewModel = ExampleViewModel()
init() {
}
var body: some View {
VStack {
Text("Test")
CustomDivider(backgroundContent: viewModel.backgroundContent)
Button("Change") {
viewModel.change()
}
}
}
}

How to correctly handle Picker in Update Views (SwiftUI)

I'm quite new to SwiftUI and I'm wondering how I should use a picker in an update view correctly.
At the moment I have a form and load the data in with .onAppear(). That works fine but when I try to pick something and go back to the update view the .onAppear() gets called again and I loose the picked value.
In the code it looks like this:
import SwiftUI
struct MaterialUpdateView: View {
// Bindings
#State var material: Material
// Form Values
#State var selectedUnit = ""
var body: some View {
VStack(){
List() {
Section(header: Text("MATERIAL")){
// Picker for the Unit
Picker(selection: $selectedUnit, label: Text("Einheit")) {
ForEach(API().units) { unit in
Text("\(unit.name)").tag(unit.name)
}
}
}
}
.listStyle(GroupedListStyle())
}
.onAppear(){
prepareToUpdate()
}
}
func prepareToUpdate() {
self.selectedUnit = self.material.unit
}
}
Does anyone has experience with that problem or am I doing something terribly wrong?
You need to create a custom binding which we will implement in another subview. This subview will be initialised with the binding vars selectedUnit and material
First, make your MaterialUpdateView:
struct MaterialUpdateView: View {
// Bindings
#State var material : Material
// Form Values
#State var selectedUnit = ""
var body: some View {
NavigationView {
VStack(){
List() {
Section(header: Text("MATERIAL")) {
MaterialPickerView(selectedUnit: $selectedUnit, material: $material)
}
.listStyle(GroupedListStyle())
}
.onAppear(){
prepareToUpdate()
}
}
}
}
func prepareToUpdate() {
self.selectedUnit = self.material.unit
}
}
Then, below, add your MaterialPickerView, as shown:
Disclaimer: You need to be able to access your API() from here, so move it or add it in this view. As I have seen that you are re-instanciating it everytime, maybe it is better that you store its instance with let api = API() and then refer to it with api, and even pass it to this view as such!
struct MaterialPickerView: View {
#Binding var selectedUnit: String
#Binding var material : Material
#State var idx: Int = 0
var body: some View {
let binding = Binding<Int>(
get: { self.idx },
set: {
self.idx = $0
self.selectedUnit = API().units[self.idx].name
self.material.unit = self.selectedUnit
})
return Picker(selection: binding, label: Text("Einheit")) {
ForEach(API().units.indices) { i in
Text(API().units[i].name).tag(API().units[i].name)
}
}
}
}
That should do,let me know if it works!

Why doesn't calling method of child view from parent view update the child view?

I'm trying to call a method of a child view which includes clearing some of its fields. When the method is called from a parent view, nothing happens. However, calling the method from the child view will clear its field. Here is some example code:
struct ChildView: View {
#State var response = ""
var body: some View {
TextField("", text: $response)
}
func clear() {
self.response = ""
}
}
struct ParentView: View {
private var child = ChildView()
var body: some View {
HStack {
self.child
Button(action: {
self.child.clear()
}) {
Text("Clear")
}
}
}
}
Can someone tell me why this happens and how to fix it/work around it? I can't directly access the child view's response because there are too many fields in my actual code and that would clutter it up too much.
SwiftUI view is not a reference-type, you cannot create it once, store in var, and then access it - SwiftUI view is a struct, value type, so storing it like did you work with copies it values, ie
struct ParentView: View {
private var child = ChildView() // << original value
var body: some View {
HStack {
self.child // created copy 1
Button(action: {
self.child.clear() // created copy 2
}) {
Here is a correct SwiftUI approach to construct parent/child view - everything about child view should be inside child view or injected in it via init arguments:
struct ChildView: View {
#State private var response = ""
var body: some View {
HStack {
TextField("", text: $response)
Button(action: {
self.clear()
}) {
Text("Clear")
}
}
}
func clear() {
self.response = ""
}
}
struct ParentView: View {
var body: some View {
ChildView()
}
}
Try using #Binding instead of #State. Bindings are a way of communicating state changes down to children.
Think of it this way: #State variables are used for View specific state. They are usually made private for this reason. If you need to communicate anything down, then #Binding is the way to do it.
struct ChildView: View {
#Binding var response: String
var body: some View {
TextField("", text: $response)
}
}
struct ParentView: View {
#State private var response = ""
var body: some View {
HStack {
ChildView(response: $response)
Button(action: {
self.clear()
}) {
Text("Clear")
}
}
}
private func clear() {
self.response = ""
}
}

How do I switch views with swiftUI MacOS?

I found this question - What is the best way to switch views in SwiftUI? - but I have not been able to get the answer to work for me.
struct view4x: View {
#State var goView: Bool = false
var body: some View {
if goView {
view5x(goView1: self.$goView)
} else {
Form {
/* ... */
}
}
}
}
and the button is inside the form:
Button(action: {
self.goView.toggle()
}) {
Text("Catalog")
}
and for my other view I have:
struct view5x: View {
#Binding var goView1: Bool
var body: some View {
Text("TEST")
Button(action: {
self.goView1.toggle()
}) {
Text("Return")
}
}
}
I just get errors that both bodies declare an opaque return type. It does not preview.
Ok, here are similar mistakes in your views. To understand them better to look at View protocol:
#available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol View {
/// The type of view representing the body of this view.
///
/// When you create a custom view, Swift infers this type from your
/// implementation of the required `body` property.
associatedtype Body : View
/// Declares the content and behavior of this view.
var body: Self.Body { get }
}
so, body is just a computed variable and it should return some View. Mistake in your view5x is that you put into it 2 different views instead 1. The solution here is to embed them into VStack for example:
struct view5x: View{
#Binding var goView1: Bool
var body: some View{
VStack {
Text("TEST")
Button(action: {
self.goView1.toggle()
}) {
Text("Return")
}
}
}
}
The problem it the view4x is similar - it's unclear what view returns body because of if...else statements, I think. You can fix it in the same way:
struct view4x: View {
#State var goView: Bool = false
var body: some View {
VStack {
if goView {
view5x(goView1: $goView)
} else {
Button(action: {
self.goView.toggle()
}) {
Text("Catalog")
}
}
}
}
}
The other way is to say what view should body return if you wrap each of them into AnyView and type return before. In this example changes of goView don't switch views, but you can see the other syntax:
struct view4x: View {
#State var goView: Bool = false
var body: some View {
if goView {
return AnyView(view5x(goView1: $goView))
} else {
return AnyView(Button(action: {
self.goView.toggle()
}) {
Text("Catalog")
})
}
}
}

SwiftUI store View as a variable while passing it some bindings

I want to store a View as a variable for later use, while passing that View some Bindings.
Here's what I've tried:
struct Parent: View {
#State var title: String? = ""
var child: Child!
init() {
self.child = Child(title: self.$title)
}
var body: some View {
VStack {
child
//...
Button(action: {
self.child.f()
}) {
//...
}
}
}
}
struct Child: View {
#Binding var title: String?
func f() {
// complex work from which results a string
self.title = <that string>
}
var body: some View {
// ...
}
}
It compiles correctly and the View shows as expected, however when updating from the child the passed Binding from the parent, the variable never gets updated. You can even do something like this (from the child):
self.title = "something"
print(self.title) // prints the previous value, in this case nil
I don't know if this is a bug or not, but directly initializing the child in the body property does the trick. However, I need that child as a property to access its methods.
If you want to change something from Parent for the child, binding is the right way. If that's complicated, you have to use DataModel.
struct Parent: View {
#State var title: String? = ""
var body: some View {
VStack {
Child(title: $title)
Button(action: {
self.title = "something"
}) {
Text("click me")
}
}
}
}
struct Child: View {
#Binding var title: String?
var body: some View {
Text(title ?? "")
}
}
This is counter to the design of the SwiftUI framework. You should not have any persistent view around to call methods on. Instead, views are created and displayed as needed in response to your app's state changing.
Encapsulate your data in an ObservableObject model, and implement any methods you need to call on that model.
Update
It is fine to have such a function defined in Child, but you should only be calling it from within the Child struct definition. For instance, if your child view contains a button, that button can call the child's instance methods. For example,
struct Parent: View {
#State private var number = 1
var body: some View {
VStack {
Text("\(number)")
Child(number: $number)
}
}
}
struct Child: View {
#Binding var number: Int
func double() {
number *= 2
}
var body: some View {
HStack {
Button(action: {
self.double()
}) {
Text("Double")
}
}
}
}
But you wouldn't try to call double() from outside the child struct. If you wanted a function that can be called globally, put it in a data model. This is especially true if the function call is making network requests, as the model will stick around outside your child view, even if it is recreated due to layout changing.
class NumberModel: ObservableObject {
#Published var number = 1
func double() {
number *= 2
}
}
struct Parent: View {
#ObservedObject var model = NumberModel()
var body: some View {
VStack {
Text("\(model.number)")
Button(action: {
self.model.double()
}) {
Text("Double from Parent")
}
Child(model: model)
}
}
}
struct Child: View {
#ObservedObject var model: NumberModel
var body: some View {
HStack {
Button(action: {
self.model.double()
}) {
Text("Double from Child")
}
}
}
}