syp5w2qxe
syp5w2qxe

Reputation: 125

SwiftUI - Function called from parent view toolbar, how to also call a function on the child view too

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?

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

Answers (2)

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

Sweeper
Sweeper

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

Related Questions