I have created a View that provides a convinient save button and a save method. Both can then be used inside a parent view.
The idea is to provide these so that the navigation bar items can be customized, but keep the original implementation.
Inside the view there is one Textfield which is bound to a #State variable. If the save method is called from within the same view everthing works as expected. If the parent view calls the save method on the child view, the changes to the #State variable are not applied.
Is this a bug in SwiftUI, or am I am missing something? I've created a simple playbook implementation that demonstrates the issue.
Thank you for your help.
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
// Create the child view to make the save button available inside this view
var child = Child()
var body: some View {
NavigationView {
NavigationLink(
destination: child.navigationBarItems(
// Set the trailing button to the one from the child view.
// This is required as this view might be inside a modal
// sheet, and we need to add the cancel button as a leading
// button:
// leading: self.cancelButton
trailing: child.saveButton
)
) {
Text("Open")
}
}
}
}
struct Child: View {
// Store the value from the textfield
#State private var value = "default"
// Make this button available inside this view, and inside the parent view.
// This makes sure the visibility of this button is always the same.
var saveButton: some View {
Button(action: save) {
Text("Save")
}
}
var body: some View {
VStack {
// Simple textfield to allow a string to change.
TextField("Value", text: $value)
// Just for the playground to change the value easily.
// Usually it would be chnaged through the keyboard input.
Button(action: {
self.value = "new value"
}) {
Text("Update")
}
}
}
func save() {
// This always displays the default value of the state variable.
// Even after the Update button was used and the value did change inside
// the textfield.
print("\(value)")
}
}
PlaygroundPage.current.setLiveView(ContentView())
I think a more SwiftUi way of doing it:
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
var body: some View {
return NavigationView {
// tell the child view where to render it's navigation item
// Instead of configuring navigation items.
NavigationLink(destination: Child(navigationSide: .left)) {
Text("Open")
}
}
}
}
struct Child: View {
enum NavigationSide { case left, right }
// If you really want to encapsulate all state in this view then #State
// is a good choice.
// If the parent view needs to read it, too, #Binding would be your friend here
#State private var value: String = "default"
// no need for #State as it's never changed from here.
var navigationSide = NavigationSide.right
// wrap in AnyView here to make ternary in ui code easier readable.
var saveButton: AnyView {
AnyView(Button(action: save) {
Text("Save")
})
}
var emptyAnyView: AnyView { AnyView(EmptyView()) }
var body: some View {
VStack {
TextField("Value", text: $value)
Button(action: {
self.value = "new value"
}) {
Text("Update")
}
}
.navigationBarItems(leading: navigationSide == .left ? saveButton : emptyAnyView,
trailing: navigationSide == .right ? saveButton : emptyAnyView)
}
func save() {
print("\(value)")
}
}
TextField will only update your value binding when the return button is pressed. To get text changes that occur during editing, set up an observed object on Child with didSet. This was the playground I altered used from your example.
struct ContentView: View {
var child = Child()
var body: some View {
NavigationView {
NavigationLink(
destination: child.navigationBarItems(
trailing: child.saveButton
)
) {
Text("Open")
}
}
}
}
class TextChanges: ObservableObject {
var completion: (() -> ())?
#Published var text = "default" {
didSet {
print(text)
}
}
}
struct Child: View {
#ObservedObject var textChanges = TextChanges()
var saveButton: some View {
Button(action: save) {
Text("Save")
}
}
var body: some View {
VStack {
TextField("Value", text: $textChanges.text).multilineTextAlignment(.center)
Button(action: {
print(self.textChanges.text)
}) {
Text("Update")
}
}
}
func save() {
print("\(textChanges.text)")
}
}
PlaygroundPage.current.setLiveView(ContentView())
Inside Child: value is mutable because it's wrapped with #State.
Inside ContentView: child is immutable because it's not wrapped with #State.
Your issue can be fixed with this line: #State var child = Child()
Good luck.
Child view needs to keep its state as a #Binding. This works:
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
#State var v = "default"
var body: some View {
let child = Child(value: $v)
return NavigationView {
NavigationLink(
destination: child.navigationBarItems(trailing: child.saveButton)
) {
Text("Open")
}
}
}
}
struct Child: View {
#Binding var value: String
var saveButton: some View {
Button(action: save) {
Text("Save")
}
}
var body: some View {
VStack {
TextField("Value", text: $value)
Button(action: {
self.value = "new value"
}) {
Text("Update")
}
}
}
func save() {
print("\(value)")
}
}
PlaygroundPage.current.setLiveView(ContentView())
Based on this commend from #nine-stones (thank you!) I implemented a more SwiftUI way so solve my problem. It does not allow the customization of the navigation items as I planned, but that was not the problem that needed to be solved. I wanted to use the Child view in a navigation link, as well as inside a modal sheet. The problem was how to perform custom cancel actions. This is why I removed the button implementation and replaced it with a cancelAction closure. Now I can display the child view wherever and however I want.
One thing I still do not know why SwiftUI is not applying the child context to the button inside the saveButton method.
Still, here is the code, maybe it helps someone in the future.
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
NavigationLink(
destination: Child(
// Instead of defining the buttons here, I send an optional
// cancel action to the child. This will make it possible
// to use the child view on navigation links, as well as in
// modal dialogs.
cancelAction: {
self.presentationMode.wrappedValue.dismiss()
}
)
) {
Text("Open")
}
}
}
}
struct Child: View {
// Store the value from the textfield
#State private var value = "default"
#Environment(\.presentationMode) var presentationMode
var cancelAction: (() -> Void)?
// Make this button available inside this view, and inside the parent view.
// This makes sure the visibility of this button is always the same.
var saveButton: some View {
Button(action: save) {
Text("Save")
}
}
var body: some View {
VStack {
// Simple textfield to allow a string to change.
TextField("Value", text: $value)
// Just for the playground to change the value easily.
// Usually it would be chnaged through the keyboard input.
Button(action: {
self.value = "new value"
}) {
Text("Update")
}
}
.navigationBarItems(
leading: self.cancelAction != nil ? Button(action: self.cancelAction!, label: {
Text("Cancel")
}) : nil,
trailing: self.saveButton
)
}
func save() {
// This always displays the default value of the state variable.
// Even after the Update button was used and the value did change inside
// the textfield.
print("\(value)")
}
}
PlaygroundPage.current.setLiveView(ContentView())
Related
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!
I have a simple View showing a list of 3 items. When the user taps on an item, it navigates to the next view. This works fine. However, I would like to also perform an action (set a variable in a View Model) when a list item is tapped.
Is this possible? Here's the code:
import SwiftUI
struct SportSelectionView: View {
#EnvironmentObject var workoutSession: WorkoutManager
let sports = ["Swim", "Bike", "Run"]
var body: some View {
List(sports, id: \.self) { sport in
NavigationLink(destination: ContentView().environmentObject(workoutSession)) {
Text(sport)
}
}.onAppear() {
// Request HealthKit store authorization.
self.workoutSession.requestAuthorization()
}
}
}
struct DisciplineSelectionView_Previews: PreviewProvider {
static var previews: some View {
SportSelectionView().environmentObject(WorkoutManager())
}
}
The easiest way I've found to get around this issue is to add an .onAppear call to the destination view of the NavigationLink. Technically, the action will happen when the ContentView() appears and not when the NavigationLink is clicked.. but the difference will be milliseconds and probably irrelevant.
NavigationLink(destination:
ContentView()
.environmentObject(workoutSession)
.onAppear {
// add action here
}
)
Here's a solution that is a little different than the onAppear approach. By creating your own Binding for isActive in the NavigationLink, you can introduce a side effect when it's set. I've implemented it here all within the view, but I would probably do this in an ObservableObject if I were really putting it into practice:
struct ContentView: View {
#State var _navLinkActive = false
var navLinkBinding : Binding<Bool> {
Binding<Bool> { () -> Bool in
return _navLinkActive
} set: { (newValue) in
if newValue {
print("Side effect")
}
_navLinkActive = newValue
}
}
var body: some View {
NavigationView {
NavigationLink(
destination: Text("Dest"),
isActive: navLinkBinding,
label: {
Text("Navigate")
})
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
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 = ""
}
}
So I have a ParentView which contains a FilterBar and a List. It looks something like this:
struct ParentView: View {
#State var listCellModels: [ListCellModel]
// Both these vars are passed to the FilterBar and adjusted by the FilterBar
#State var isEditing: Bool = false
#State var selectedType: FilterType = .none
// When selected type is changed, I need to reload the models in the list with the new filter
private var filteredModels: [ListCellModel] {
return listCellModels.filter{
(selectedType.rawValue == 0 || $0.approved.rawValue == selectedType.rawValue)
}
}
var body: some View {
VStack {
FilterBar(isEditing: $isEditing, selectedType: $selectedType)
// All the items in the list are buttons that display a custom view I had
// this works fine, and when isEditing is changed the view DOES update
List(filteredModels) { model in
Button(action: {
// Does a thing
}, label: {
ListViewCell(model: model, isEditing: self.$isEditing)
})
}
}
}
}
My Filter bar is just a simple HStack with a couple buttons that modify the variables
struct FilterBar: View {
#Binding var isEditing: Bool
#Binding var selectedType: FilterType
var body: some View {
HStack(alignment: .center) {
Button(action: {
self.selectedType = FilterType.init(rawValue: (self.selectedType.rawValue + 1) % 4)!
}, label: {
Text("Filter: \(selectedType.name)")
}).padding(.top).padding(.leading)
Spacer()
Button(action: {
self.isEditing = !self.isEditing
}, label: {
Text(!isEditing ? "Edit" : "Done")
}).padding(.top).padding(.trailing)
}
}
}
When I tap the button that changes isEditing, all of the cells in the list update to show their "Editing" states, but when i tap the button to change selectedType, the variable in the parent view does get updated, as I've observed in the debugger - however the view does not reload. So it appears as if the old filter is still being applied.
Is there any reason why updating this #State var is not reloading the view?
Are there any workarounds?
Well, it is like... workaround... but for testing, try
FilterBar(isEditing: $isEditing, selectedType: $selectedType)
if selectedType != .none {
EmptyView()
}
In general, it would be correct to introduce view model as ObservableObject and have filteredModels in it as #Published, so your FilterBar changed that property, which will automatically refreshed the ParentView.
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")
}
}
}
}