SwiftUI using Environment Object on multiple views giving issues with Navigation - asynchronous

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.

Related

How to bind an action to the navigationview back button?

Hi I would like to bind an action to the Back button in the navigationview toolbar, is it possible?
picture about the situation
var body: some View {
List {
ForEach(mainViewModel.items) { item in
NavigationLink(destination: EditTaskView(item: item)) {
HStack {
ListRowView(item: item)
You can't bind directly to the back button, but you can have the navigation link itself be activated based on state, and then listen to the change of the state value like so. Do note that this requires that you manage the setting of state to true (no auto tap like with the default initializer)
struct ContentView: View {
#State private var showingNavView = false
var body: some View {
NavigationView {
List {
NavigationLink("Sub View", isActive: $showingNavView) {
SubView()
}.onTapGesture {
showingNavView = true
}.onChange(of: showingNavView) { newValue in
print(newValue) // Will change to false when back is pressed
}
}
}
}
}
struct SubView: View {
var body: some View {
ZStack {
Color.green
Text("Cool Beans")
}
}
}

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: 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
}
}

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

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")
}
}

Resources