Reputation: 125
In my SwiftUI app I am using a custom toolbar in a parent view. This toolbar is also present for the child view too. When the user is 'done' editing I call a function in the parent to perform some tasks. However in the child I don't know how to get access to this event to also call some logic. Using the done button doesn't call the onSubmit of the child so I don't finish my editing correctly. Can you only access a toolbar's button event in the view it's created from?
If I use another toolbar on the child it creates display errors on the toolbar itself.
There are bindings from the parent to the child so I can't create the child view as a property sadly. (i.e. let child = ChildView(), then use child.alsoFinishedEditing())
The closest solution I could think of was creating a bool flag in the parent and just toggling in, listening for an onChange event in the child but this seemed a little hacky.
Any help to approach this problem would be appreciated. Minimal reproduction code below.
import SwiftUI
// In my parent view I'm handling some logic that needs a toolbar action
// when 'done'
struct ParentView: View {
// parent logic omitted
var body: some View {
VStack {
Text("Parent View")
.padding()
// child view that handles it's own state
ChildView()
.padding()
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
HStack {
Button(action: {
// how would I also call a function in the child?
finishEditing()
}, label: {
Text("Done").bold()
})
}
}
}
}
// Parent's function that gets called when Done is pressed
func finishEditing() {
// Parent logic omitted
print("Done Editing")
// How would I call a method in the child view?
}
}
struct ChildView: View {
@State var inputText: String = ""
var body: some View {
VStack {
// for this field the 'done' but is also present
// but when you tap it the onSubmit get's bypassed
// and we don't correctly finish our editing logic
TextField("Child Input", text: $inputText)
.padding()
.onSubmit {
alsoFinishedEditing()
}
}
}
func alsoFinishedEditing() {
// how can I access the 'done' button
// event from this child
}
}
Upvotes: 1
Views: 126
Reputation: 36792
You could try this simple approach passing the function to the child view, such as:
// In my parent view I'm handling some logic that needs a toolbar action
// when 'done'
struct ParentView: View {
// parent logic omitted
var body: some View {
VStack {
Text("Parent View")
.padding()
// child view that handles it's own state
ChildView(action: finishEditing) // <--- here
.padding()
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
HStack {
Button(action: {
// how would I also call a function in the child?
finishEditing()
}, label: {
Text("Done").bold()
})
}
}
}
}
// Parent's function that gets called when Done is pressed
func finishEditing() {
// Parent logic omitted
print("----> finishEditing Done Editing")
// How would I call a method in the child view?
}
}
struct ChildView: View {
@State var inputText: String = ""
let action: () -> Void // <--- here
var body: some View {
VStack {
// for this field the 'done' but is also present
// but when you tap it the onSubmit get's bypassed
// and we don't correctly finish our editing logic
TextField("Child Input", text: $inputText)
.padding()
.onSubmit {
print("----> ChildView onSubmit")
action() // <--- here
}
}
}
}
Upvotes: 0
Reputation: 273540
If I understand correctly, you want each view to have their own way of handling the tap of the "Done" button added by the parent view.
This can be done with a custom PreferenceKey
. Each view will have its own preference of "what to do when Done is tapped".
struct DoneActionKey: PreferenceKey {
static let defaultValue: @MainActor () -> Void = {}
static func reduce(value: inout @MainActor () -> Void, nextValue: () -> @MainActor () -> Void) {
let curr = value
let next = nextValue()
value = {
curr()
next()
}
}
}
Note the order of curr()
and next()
. This determines the order in which sibling views' preferences of "what to do when Done is tapped" are executed.
Then you can write a view modifier that changes that preference so that child views can each specify what they want to do.
extension View {
func onDone(_ action: @MainActor @escaping () -> Void) -> some View {
self.onSubmit {
action()
}
.transformPreference(DoneActionKey.self) { value in
let curr = value
value = {
action()
curr()
}
}
}
}
I invoked the action in onSubmit
as well. You can remove that if this is not what you want. Again, note the order of action()
and curr()
. This determines the order in which the preferences of parent and child views are run. By putting action()
first, the parent view's action is run before the child view's, because the value
parameter represents the child's preference, not the parent's.
Usage:
struct ParentView: View {
var body: some View {
VStack {
Text("Parent View")
.padding()
ChildView()
.padding()
}
.onDone {
finishEditing()
}
// add the toolbar via a backgroundPreferenceValue, so we have access to the preference value
.backgroundPreferenceValue(DoneActionKey.self) { doneAction in
Color.clear
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
HStack {
Button(action: {
doneAction()
}, label: {
Text("Done").bold()
})
}
}
}
}
}
func finishEditing() {
print("Done Editing")
}
}
struct ChildView: View {
@State var inputText: String = ""
var body: some View {
VStack {
TextField("Child Input", text: $inputText)
.padding()
.onDone {
alsoFinishedEditing()
}
}
}
func alsoFinishedEditing() {
print("Child Finishes Editing")
}
}
Now tapping on "Done" would print
Done Editing
Child Finishes Editing
Upvotes: 1