SwiftUI - Button - How to pass a function request to parent - button

How can I have a button perform an action which triggers a function in its 'parent' view? I'm trying to refactor my code so that components are as small as possible.
In this case, the button performs a few tasks, but one of them is to run a function:
Button(
action: {
self.setViewBackToNil()
}){
Text("Button")
}
// which triggers a function
func setViewBackToNil(){
self.userData.image = nil
self.isProcessing = false
.... etc
}
Now, if I turn the button into its own view, I can't pass self.setViewBackToNil because it's contained within the struct of the parent.
Is there a way for a component to trigger a function within its parent?

The best examples on closures and how they can be used is found in the official swift documentation.
This is a small example on how to pass a closure to your child view which then calls a function of the parent:
struct ChildView: View {
var function: () -> Void
var body: some View {
Button(action: {
self.function()
}, label: {
Text("Button")
})
}
}
struct ParentView: View {
var body: some View {
ChildView(function: { self.fuctionCalledInPassedClosure() })
}
func fuctionCalledInPassedClosure() {
print("I am the parent")
}
}
I hope this helps!
Pass a function
And here is an example to pass the function:
struct ChildView: View {
var function: () -> Void
var body: some View {
Button(action: {
self.function()
}, label: {
Text("Button")
})
}
}
struct ParentView: View {
var body: some View {
ChildView(function: self.passedFunction)
}
func passedFunction() {
print("I am the parent")
}
}
Pass a function with parameters
struct ChildView: View {
var myFunctionWithParameters: (String, Int) -> Void
var body: some View {
Button(action: {
self.myFunctionWithParameters("parameter", 1)
}, label: {
Text("Button")
})
}
}
struct ParentView: View {
var body: some View {
ChildView(myFunctionWithParameters: self.passedFunction)
}
func passedFunction(myFirstParameter: String, mySecondParameter: Int) {
print("I am the parent")
}
}

Related

Result of 'HymnLyrics' initializer is unused

I have the following error in my code. Please help me why I cannot use my HymnLyrics() struct inside button action. It works for NavigationLink of destination but I don't wanna use it.
import SwiftUI
struct ContentView: View {
#EnvironmentObject var tappingSwitches: TapToggle
let zoLyrics: [Lyric] = LyricList.hymnLa.sorted { lhs, rhs in
return lhs.zoTitle < rhs.zoTitle
}
var body: some View {
ScrollView {
ForEach(zoLyrics, id: \.id) { zoLyric in
VStack {
Button(action: {
HymnLyrics(lyrics: LyricList.hymnLa)// Here is the error, I can use directly by using NavagationLink
self.tappingSwitches.isHymnTapped.toggle()
}, label: {
HStack {
Text(zoLyric.zoTitle)
.foregroundColor(Color("bTextColor"))
.lineLimit(1)
.minimumScaleFactor(0.5)
Spacer()
Text("\(zoLyric.number)")
.foregroundColor(Color("bTextColor"))
}
})
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding([.leading, .bottom, .trailing])
}
}
}
}

SwiftUI using Environment Object on multiple views giving issues with Navigation

Don't know if I'm abusing the idea of environment object, but experiencing an issue when using an environment object that publishes a delayed async value. One view navigates to the next, but then the 'root' gets updated subsequently and as a result causes an 'echo', or even if that is handled a navigation problem. The issue becomes even more evident when using transitions between navigation.
Is there a correct use pattern to avoid this? Or some other solution maybe?
Any guidance will be appreciated.
Attached a condensed sample to illustrate the problem.
Xcode 12.4 ios 14.1
final class SetColor: ObservableObject {
#Published var asyncVal: Bool = false
func flipIt() {
DispatchQueue.main.asyncAfter(deadline: .now()+0.5, execute: {self.asyncVal.toggle()})
}
}
struct HomeView: View {
#StateObject var setCol: SetColor = SetColor()
#State private var navActive: Bool = false
var body: some View {
NavigationView {
ZStack {
Color(setCol.asyncVal ? .blue : .purple)
Button(action: {
setCol.flipIt()
navActive.toggle()
}, label: {
Text("Change and Move")
})
.navigationTitle("Home")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: NavChild1().environmentObject(setCol),isActive: $navActive, label: { Text("GoTo 1 >") })
}
}
}
}
}
}
struct NavChild1: View {
#EnvironmentObject var setCol: SetColor
#State private var navActive: Bool = false
var body: some View {
ZStack {
Color(setCol.asyncVal ? .yellow : .orange)
Button(action: {
setCol.flipIt()
navActive.toggle()
}, label: {
Text("Change and Move")
})
.navigationTitle("Nav 1")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: NavChild2().environmentObject(setCol),isActive: $navActive, label: { Text("GoTo 2 >") })
}
}
}
}
}
struct NavChild2: View {
#EnvironmentObject var setCol: SetColor
#State private var navActive: Bool = false
var body: some View {
ZStack {
Color(setCol.asyncVal ? .yellow : .orange)
Button(action: {
setCol.flipIt()
navActive.toggle()
}, label: {
Text("Change and Move")
})
.navigationTitle("Nav 2")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: NavChild3().environmentObject(setCol),isActive: $navActive, label: { Text("GoTo 3 >") })
}
}
}
}
}
struct NavChild3: View {
#EnvironmentObject var setCol: SetColor
#State private var navActive: Bool = false
var body: some View {
ZStack {
Color(setCol.asyncVal ? .yellow : .orange)
Button(action: {
setCol.flipIt()
navActive.toggle()
}, label: {
Text("Change and Move")
})
.navigationTitle("Nav 3")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(destination: NavChild3().environmentObject(setCol), isActive: .constant(false), label: { Text("Go Home") })
}
}
}
}
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
You do not need the deadline you put in GCD action. It causes navigation actions even if user does not press on navigation (I've tested the code in a project).
This is because you accumulate jobs in the GCD queue and when they are executed, you're in another View (due to the 0.5 stall). By the way, they cause navigation since the flip is Observed and therefore whoever listens , will execute the navigation.
Anyway, what you wanna do is change the dispatch command to this:
DispatchQueue.main.async { self.asyncVal.toggle() }
And navigation will be smoother with no extra navigation commands executed afterwards.

SwiftUI: Button in Form

I am creating a Form in SwiftUi with a section that is including a flexible number of instruction.
Next to the last instruction TextField, I am showing a "+"-Button that is extending the instructions array with a new member:
var body: some View {
NavigationView {
Form {
...
Section(header: Text("Instructions")) {
InstructionsSectionView(instructions: $recipeViewModel.recipe.instructions)
}
...
struct InstructionsSectionView: View {
#Binding var instructions: [String]
var body: some View {
ForEach(instructions.indices, id: \.self) { index in
HStack {
TextField("Instruction", text: $instructions[index])
if(index == instructions.count-1) {
addInstructionButton
}
}
}
}
var addInstructionButton: some View {
Button(action: {
instructions.append("")
}) {
Image(systemName: "plus.circle.fill")
}
}
}
Now the problem is, that the button click-area is not limited to the picture but to the whole last row. Precisely the part just around the textField, meaning if I click in it, I can edit the text, but if I click on the border somewhere, a new entry is added.
I assume that this is specific to Form {} (or also List{}), since it does not happen if I use a Button next to a text field in a "normal" set-up.
Is there something wrong with my code? Is this an expected behaviour?
I am not sure why border is getting tappable, but as a workaround I used plainButtonStyle and that seems to fix this issue, and keeps functionality intact .
struct TestView: View {
#State private var endAmount: CGFloat = 0
#State private var recipeViewModel = ["abc","Deef"]
var body: some View {
NavigationView {
Form {
Section(header: Text("Instructions")) {
InstructionsSectionView(instructions: $recipeViewModel)
}
}
}
}
}
struct InstructionsSectionView: View {
#Binding var instructions: [String]
var body: some View {
ForEach(instructions.indices, id: \.self) { index in
HStack {
TextField("Instruction", text: $instructions[index])
Spacer()
if(index == instructions.count-1) {
addInstructionButton
.buttonStyle(PlainButtonStyle())
.foregroundColor(.blue)
}
}
}
}
var addInstructionButton: some View {
Button(action: {
instructions.append("")
}) {
Image(systemName: "plus.circle.fill")
}
}
}

Calling a navigation view from another view Swift 5

I am trying to call a navigation view from another view using a navigation bar. However, when I try to call it it just comes up blank. I think something goes wrong if I call a view that has a navigation view on it. The view I'm trying to call is TeamList(). I tried calling other views and it works, but only TeamList() doesn't work since it has navigation view on it. Any ideas?
Here is the View I am trying to call
import SwiftUI
struct TeamList: View {
init() {
UITableView.appearance().backgroundColor = UIColor(.pink)
UITableViewCell.appearance().backgroundColor = UIColor(.pink)
UITableView.appearance().tableFooterView = UIView()
}
var body: some View {
NavigationView {
List(teamData) { team in
NavigationLink(destination: TeamDetail(team: team)) {
TeamRow(team: team)
}
}
.navigationBarTitle(Text("Club List"))
}
}
}
struct TeamList_Previews: PreviewProvider {
static var previews: some View {
TeamList()
}
}
And here is the view I am calling it from
import SwiftUI
struct ContentView: View {
var body: some View {
CustomNaviagtionBar()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct Home : View {
var body: some View {
VStack {
HStack {
Image("epl")
VStack(alignment: .leading) {
Text("Premier League")
Text("See the latest matches")
}
.padding(.top, 40)
Spacer()
}
Spacer()
}.background(Color.pink).edgesIgnoringSafeArea(.all).foregroundColor(.white)
}
}
var tabs = ["Home","Ranking","Clubs"]
struct BarButton : View {
var image : String
#Binding var Tab : String
var body: some View {
Button(action: {Tab = image}) {
Image(image)
.renderingMode(.template)
.foregroundColor(Tab == image ? Color(.blue) : Color.black.opacity(0.4))
.padding()
}
}
}
struct CustomNaviagtionBar : View {
#State var Tab = "Home"
#State var edge = UIApplication.shared.windows.first?.safeAreaInsets
var body: some View {
ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)){
TabView(selection: $Tab) {
Home()
.tag("Home")
Ranking()
.tag("Ranking")
Clubs()
.tag("Clubs")
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.ignoresSafeArea(.all, edges: .bottom)
HStack(spacing: 0){
ForEach(tabs, id: \.self){image in
BarButton(image: image, Tab: $Tab)
if image != tabs.last{
Spacer(minLength: 0)
}
}
}
}
}
}
struct Ranking : View {
var body: some View{
VStack{
Text("Ranking")
}
}
}
struct Clubs : View {
var body: some View{
TeamList() // This is the view that I am trying to call but come up blank
}
}

How do I use TextInput for WatchOS in swiftUI

Typically I would use presentTextInputControllerWithSuggestions() to show the TextInput field. But this isn't available in swiftUI because it is a function of WKInterfaceController. Do I have to use the WKInterfaceController for this?
I couldn't find anything in the documentation.
You can use extension for View in SwiftUI:
extension View {
typealias StringCompletion = (String) -> Void
func presentInputController(withSuggestions suggestions: [String], completion: #escaping StringCompletion) {
WKExtension.shared()
.visibleInterfaceController?
.presentTextInputController(withSuggestions: suggestions,
allowedInputMode: .plain) { result in
guard let result = result as? [String], let firstElement = result.first else {
completion("")
return
}
completion(firstElement)
}
}
}
Example:
struct ContentView: View {
var body: some View {
Button(action: {
presentInputController()
}, label: {
Text("Press this button")
})
}
private func presentInputController() {
presentInputController(withSuggestions: []) { result in
// handle result from input controller
}
}
}
This would be done through a TextField in SwiftUI.

Resources