SwiftUI bi-directional move transition moving the wrong way in certain cases - ios

I have four main functional areas of my app that can be accessed by the user via a custom tab bar at the bottom of the the ContentView. I want to use a slide transition to move between the views when the user taps the desired function in the tab bar.
I also want the direction of the slide to be based on the relative position of the options on the tab bar. That is, if going from tab 1 to tab 3, the views will slide from right to left, or if going from tab 3 to tab 2, the views will slide from left to right.
This works perfectly on the first change of view and for any subsequent change of view that changes direction of the slide. E.g., the following sequence of view changes work: 1->3, 3->2, 2->4, 4->1.
However, any time there is a change of view where the direction is the same as the previous direction, it doesn't work correctly. E.g., the bolded changes in the following sequence don't work properly. 1->2, 2->3, 3->4, 4->3, 3->2.
In the above-mentioned transitions that don't work properly, the incoming view enters from the appropriate direction, but the outgoing view departs in the wrong direction. For example, the image at the bottom of this post shows the new view moving in appropriately from right to left, but the departing view is moving from left to right, leaving the white space on the left (it should also be moving from right to left along with the incoming view).
Any thoughts on why this might be happening / how to correct it?
I'm using iOS 16 for my app.
Following is a complete code sample demonstrating this issue:
import SwiftUI
#main
struct TabBar_testingApp: App {
#StateObject var tabOption = TabOption()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(tabOption)
}
}
}
class TabOption: ObservableObject {
#Published var tab: TabItem = .tab1
#Published var slideLeft: Bool = true
}
enum TabItem: Int, CaseIterable {
// MARK: These are the four main elements of the app that are navigated to via the custom tab or sidebar controls
case tab1 = 0
case tab2 = 1
case tab3 = 2
case tab4 = 3
var description: String {
switch self {
case .tab1: return "Tab 1"
case .tab2: return "Tab 2"
case .tab3: return "Tab 3"
case .tab4: return "Tab 4"
}
}
var icon: String {
switch self {
case .tab1: return "1.circle"
case .tab2: return "2.circle"
case .tab3: return "3.circle"
case .tab4: return "4.circle"
}
}
}
struct ContentView: View {
#EnvironmentObject var tabOption: TabOption
var body: some View {
NavigationStack {
VStack {
// Content
Group {
switch tabOption.tab {
case TabItem.tab1:
SlideOneView()
case TabItem.tab2:
SlideTwoView()
case TabItem.tab3:
Slide3View()
case TabItem.tab4:
SlideFourView()
}
}
// Use a slide transition when changing the tab views
.transition(.move(edge: tabOption.slideLeft ? .leading : .trailing))
Spacer()
// Custom tab bar
HStack {
Spacer()
// Open tab 1
Button(action: {
withAnimation {
// Set the direction the tabs will slide when transitioning between the tabs
tabOption.slideLeft = true
// Change to the selected tab
tabOption.tab = TabItem.tab1
}
}) {
VStack {
Image(systemName: TabItem.tab1.icon).font(.title2)
Text(TabItem.tab1.description).font(.caption2)
}
.foregroundStyle(tabOption.tab == .tab1 ? .primary : .secondary)
.font(.title)
}
Spacer()
// Open tab 2
Button(action: {
withAnimation {
// Set the direction the tabs will slide when transitioning between the tabs
if tabOption.tab.rawValue == TabItem.tab1.rawValue {
tabOption.slideLeft = false
} else {
tabOption.slideLeft = true
}
// Change to the selected tab
tabOption.tab = TabItem.tab2
}
}) {
VStack {
Image(systemName: TabItem.tab2.icon).font(.title2)
Text(TabItem.tab2.description).font(.caption2)
}
.foregroundStyle(tabOption.tab == .tab2 ? .primary : .secondary)
.font(.title)
}
Spacer()
// Open tab 3
Button(action: {
withAnimation {
// Set the direction the tabs will slide when transitioning between the tabs
if tabOption.tab.rawValue == TabItem.tab4.rawValue {
tabOption.slideLeft = true
} else {
tabOption.slideLeft = false
}
// Change to the selected tab
tabOption.tab = TabItem.tab3
}
}) {
VStack {
Image(systemName: TabItem.tab3.icon).font(.title2)
Text(TabItem.tab3.description).font(.caption2)
}
.foregroundStyle(tabOption.tab == .tab3 ? .primary : .secondary)
.font(.title)
}
Spacer()
// Open tab 4
Button(action: {
withAnimation {
// Set the direction the tabs will slide when transitioning between the tabs
tabOption.slideLeft = false
// Change to the selected tab
tabOption.tab = TabItem.tab4
}
}) {
VStack {
Image(systemName: TabItem.tab4.icon).font(.title2)
Text(TabItem.tab4.description).font(.caption2)
}
.foregroundStyle(tabOption.tab == .tab4 ? .primary : .secondary)
.font(.title)
}
Spacer()
} // HStack closure
.foregroundStyle(.blue)
.padding(.top, 5)
}
}
}
}
struct SlideOneView: View {
var body: some View {
ZStack {
Group {
Color.blue
Text("Tab Content 1")
.font(.largeTitle)
.foregroundColor(.white)
}
}
}
}
struct SlideTwoView: View {
var body: some View {
ZStack {
Group {
Color.green
Text("Tab Content 2")
.font(.largeTitle)
.foregroundColor(.white)
}
}
}
}
struct Slide3View: View {
var body: some View {
ZStack {
Group {
Color.purple
Text("Tab Content 3")
.font(.largeTitle)
.foregroundColor(.white)
}
}
}
}
struct SlideFourView: View {
var body: some View {
ZStack {
Group {
Color.red
Text("Tab Content 4")
.font(.largeTitle)
.foregroundColor(.white)
}
}
}
}
And finally, here's the screenshot where the bottom (departing) view is moving incorrectly from left to right which briefly leaves white space on the left, while the incoming view is correctly moving from right to left.
HERE'S MY REVISED CODE PER COMMENTS BELOW:
class TabOption: ObservableObject {
#Published var tab: TabItem = .tab1
#Published var slideLeft: Bool = true
func changeTab(to newTab: TabItem) {
switch newTab.rawValue {
// case let allows you to make a comparison in the case statement
// This determines the direction is decreasing, so we want a right slide
case let t where t < tab.rawValue:
slideLeft = false
// This determines the direction is increasing, so we want a left slide
case let t where t > tab.rawValue:
slideLeft = true
// This determines that the user tapped this tab, so do nothing
default:
return
}
// We have determined the proper direction, so change tabs.
withAnimation(.easeInOut) {
tab = newTab
}
}
}
enum TabItem: Int, CaseIterable {
// MARK: These are the four main elements of the app that are navigated to via the custom tab or sidebar controls
case tab1 = 0
case tab2 = 1
case tab3 = 2
case tab4 = 3
var description: String {
switch self {
case .tab1: return "Tab 1"
case .tab2: return "Tab 2"
case .tab3: return "Tab 3"
case .tab4: return "Tab 4"
}
}
var icon: String {
switch self {
case .tab1: return "1.circle"
case .tab2: return "2.circle"
case .tab3: return "3.circle"
case .tab4: return "4.circle"
}
}
}
struct ContentView: View {
#EnvironmentObject var tabOption: TabOption
var body: some View {
NavigationStack {
VStack {
// Content
Group {
switch tabOption.tab {
case TabItem.tab1:
SlideOneView()
case TabItem.tab2:
SlideTwoView()
case TabItem.tab3:
Slide3View()
case TabItem.tab4:
SlideFourView()
}
}
// Use a slide transition when changing the tab views
.transition(
.asymmetric(
insertion: .move(edge: tabOption.slideLeft ? .trailing : .leading),
removal: .move(edge: tabOption.slideLeft ? .leading : .trailing)
)
)
Spacer()
// Custom tab bar
HStack {
Spacer()
// Open tab 1
Button(action: {
withAnimation {
tabOption.changeTab(to: .tab1)
}
}) {
VStack {
Image(systemName: TabItem.tab1.icon).font(.title2)
Text(TabItem.tab1.description).font(.caption2)
}
.foregroundStyle(tabOption.tab == .tab1 ? .primary : .secondary)
.font(.title)
}
Spacer()
// Open tab 2
Button(action: {
withAnimation {
tabOption.changeTab(to: .tab2)
}
}) {
VStack {
Image(systemName: TabItem.tab2.icon).font(.title2)
Text(TabItem.tab2.description).font(.caption2)
}
.foregroundStyle(tabOption.tab == .tab2 ? .primary : .secondary)
.font(.title)
}
Spacer()
// Open tab 3
Button(action: {
withAnimation {
tabOption.changeTab(to: .tab3)
}
}) {
VStack {
Image(systemName: TabItem.tab3.icon).font(.title2)
Text(TabItem.tab3.description).font(.caption2)
}
.foregroundStyle(tabOption.tab == .tab3 ? .primary : .secondary)
.font(.title)
}
Spacer()
// Open tab 4
Button(action: {
tabOption.changeTab(to: .tab4)
}) {
VStack {
Image(systemName: TabItem.tab4.icon).font(.title2)
Text(TabItem.tab4.description).font(.caption2)
}
.foregroundStyle(tabOption.tab == .tab4 ? .primary : .secondary)
.font(.title)
}
Spacer()
} // HStack closure
.foregroundStyle(.blue)
.padding(.top, 5)
}
}
}
}
Here's a GIF of the issue using the revised code (apologies for the gif compression "squashing" the screen image, but you get the idea):

So, a couple of things. First, you had way too much logic in your view code. Remember the DRY principal(Don't Repeat Yourself). Essentially, you are using TabOption, so your logic should go in there. I added a function to TabOption that contains all the logic to change tabs:
class TabOption: ObservableObject {
#Published var tab: TabItem = .tab1
#Published var slideLeft: Bool = true
func changeTab(to newTab: TabItem) {
switch newTab.rawValue {
// case let allows you to make a comparison in the case statement
// This determines the direction is decreasing, so we want a right slide
case let t where t < tab.rawValue:
slideLeft = false
// This determines the direction is increasing, so we want a left slide
case let t where t > tab.rawValue:
slideLeft = true
// This determines that the user tapped this tab, so do nothing
default:
return
}
// We have determined the proper direction, so change tabs.
withAnimation(.easeInOut) {
tab = newTab
}
}
}
With that in place, things are easier to reason. In the end, the views were not sliding in the directions you expected because you didn't realize you were dealing with two views that you wanted to do different things with. If you have a slide left, you want the original view to exit by moving its trailing edge, and the new view moving its leading edge. The right slide is reversed. Your transition was telling them to enter and exit from the same direction. What you want is an .asymmetric() transition like this:
.transition(
.asymmetric(
insertion: .move(edge: tabOption.slideLeft ? .trailing : .leading),
removal: .move(edge: tabOption.slideLeft ? .leading : .trailing)
)
)
Lastly, to complete this, each of your button actions are simply like this:
// Open tab 1
Button(action: {
tabOption.changeTab(to: .tab1)
}) {
...
}
Edit:
Using the code provided, this is the result following your comments:
As you can see, there are no issues. Please make sure you adopted all of my code, and not just the asymmetric transition. I am not sure that having the tabOption.slideLeft = true inside the animation block is not also causing problems.

Related

iOS button and navigationLink next to each other in List

I have a List, each element has its own HStack that contains Button and NavigationLink to the next view but both (checkbox button and navigation link) is activated wherever I click on single HStack element.
That means icon on the button changes when I click on the element but application also loads the next view. The same happens when I want to go to the next view by simply clicking on the NavigationLink. Can you help me separate this two functionalities (checkbox Button and NavigationLink)?
struct ContentView: View {
#ObservedObject var spendingList: SpendingList
var body: some View {
NavigationView {
List {
ForEach($spendingList.spendings) { $spending in
HStack{
Button(action: {
spending.Bought = !spending.Bought
}, label: {
if spending.Bought == false {
Image(systemName: "square")
.foregroundColor(.accentColor)
} else {
Image(systemName: "checkmark.square")
.foregroundColor(.accentColor)
}
})
NavigationLink(destination: DetailView(spending: $spending)
.navigationTitle(Text(spending.Name)),
label: {
Text(spending.Name).frame(maxWidth: .infinity, alignment: .leading)
if spending.Price != 0 {
Text(String(spending.Price)).frame(maxWidth: .infinity, alignment: .trailing)
} else {
Text("empty").foregroundColor(.gray)
}
})
}
}
.navigationTitle(Text("Spending Priority"))
}
}
}
}
The default Style of Button & NavigationLink makes the whole row click as one. However, using PlainButtonStyle() fixes the issue by making the button clickable & not the cell:
.buttonStyle(.plain)//to your button & NavigationLink
Unfortunately, the parent-view list item becomes the Navigation link. So the button press will never be recorded.
In iOS 16 you can solve this by replacing the NavigationLink with a Button and pushing an item onto the navigation stack with the Button.
Documentation: https://developer.apple.com/documentation/swiftui/migrating-to-new-navigation-types
Something like this, for example:
struct ContentView: View {
#State var path: [View] = []
var body: some View {
NavigationStack(path: $path) {
List {
ForEach($spendingList.spendings) { $spending in
HStack {
Button(action: {
spending.Bought = !spending.Bought
}, label: {
Image(...)
})
Button(action: {
path.append(DetailView(spending: $spending))
}, label: {
Text(...)
})
Text(spending.Name)
})
}
}
}
.navigationTitle(Text("Spending Priority"))
.navigationDestination(for: View.self) { view in
view
}
}
}
}

SwiftUI: On tap gesture switches views

I have the following simplified code in a project using XCode 13.2. The intended functionality is to have 2 tabs (Tab1 and Tab2), and the user is able to tap the screen on Tab2 to toggle the background from red to green.
Instead, however, when Tab2 is clicked, the screen displays tab 1, instead of Tab2 with the changed color. I'm wondering why the app goes to Tab1 when Tab2 is tapped.
There are no NavigationLinks or actions that make the user go to Tab1. My only thought is that maybe the app is rebuilding when Tab2 is clicked, which is why it goes back to the first tab? If so, are there any good workarounds for this?
Note: the color still changes on Tab2, but not before the app switches to Tab1.
struct ContentView: View {
var body: some View {
TabView {
Tab1()
.tabItem {
Text("Tab 1")
}
Tab2()
.tabItem {
Text("Tab 2")
}
}
}
}
struct Tab1: View {
var body: some View {
Text("Tab 1")
}
}
struct Tab2: View {
#State var isRed: Bool = false
var body: some View {
if !isRed {
Color.green
.onTapGesture {
isRed = true
}
} else {
Color.red
.onTapGesture {
isRed = false
}
}
}
}
The reason why tapping on Tab2 causes it to jump back to Tab1 is that the structural identity of Tab2 changes whenever you tap on it causing swiftUI to reload the body. See the explanation here.
A simple fix to the example you have above would be to wrap the entire body in a ZStack
var body: some View {
ZStack {
if !isRed {
Color.green
.onTapGesture {
isRed = true
}
} else {
Color.red
.onTapGesture {
isRed = false
}
}
}
}
I have noticed that using if/else in that way causes strange behavior. Refactor Tab2 as shown below and it should work as expected.
struct Tab2: View {
#State var isRed: Bool = false
var body: some View {
ZStack {
Color.red
Color.green
.opacity(isRed ? 0 : 1)
}
.onTapGesture {
isRed.toggle()
}
}
}

How do I consecutively close, then open a new panel (if one is active to begin with) for multiple panel views overlayed on the same view?

I'm working on a SwiftUI application where users can create groups for other members, and therefore become the admin of that group. As admin, they have access to other controls and abilities, in the form of a custom toolbar of icons at the top of the group's page view. Those being:
Settings / Admin tools
Manage members
Post / manage content
Pressing each of these control icons triggers its respective panel (or drawer I think its more commonly called), a half sheet overlay with view content.
The main issue I'm having is that if one of these panels is currently open, and I press on another panel control icon, the panel view content instantly changes. What I'd like to happen is for the panel to close, then re-open the new panel. The animation to open/close (.spring()) is there but I'm unsure how to consecutively do these view operations.
I thought of using a Timer to wait for view to close, but that feels sloppy. I feel there is a consecutive view modifier/operation to use more appropriately, but I'm unaware of it.
Also I feel some of my code is redundant and inefficient, so if anyone has suggestions there I'd be happy to hear them.
struct GroupPage: View {
// The enum and #State vars here I feel could be executed alot better and consolidated somehow
enum ActivePanel {
case add
case settings
case member
case none
}
#State var addMode: OUI.PanelMode = .none
#State var settingsMode: OUI.PanelMode = .none
#State var memberMode: OUI.PanelMode = .none
#State var activePanel: ActivePanel = .none
#ObservedObject var manager: GroupViewModel
init(_ manager: GroupViewModel) { self.manager = manager }
var body: some View {
ZStack {
// Group page content here
adminControls
// Panels are "present" but in closed state
addPanel
settingsPanel
memberPanel
}
}
var adminControls: some View {
VStack {
HStack(spacing: 15) {
Spacer()
Button(action: { showPanel(.member)}) {
Image(systemName: "person")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
}
Button(action: { showPanel(.settings)}) {
Image(systemName: "gear")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
}
Button(action: { showPanel(.add)}) {
Image(systemName: "plus")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
}
}
Spacer()
}
.padding(.top, 10)
}
var addPanel: some View {
OUI.Panel($addMode) {
// Add / manage posts view content
}
}
var settingsPanel: some View {
OUI.Panel($settingsMode) {
// Settings view content
}
}
var memberPanel: some View {
OUI.Panel($memberMode) {
// Member management view content
}
}
private func showPanel(_ mode: ActivePanel) {
// closePanel here I would like to visually close the panel, then reopen it back-to-back in switch statements below
closePanel()
// if statements close the panel altogether if already active
switch mode {
case .add:
if addMode == .half { addMode = .none }
else { addMode = .half }
case .settings:
if settingsMode == .half { settingsMode = .none }
else { settingsMode = .half }
case .member:
if memberMode == .half { memberMode = .none }
else { memberMode = .half }
case .none:
closePanel() // Redundant probably
}
}
private func closePanel() {
addMode = .none
settingsMode = .none
memberMode = .none
activePanel = .none
}
}
// OUI (a typealias) is my app's custom UI class and below is the Panel code
// No initializers or anything else that is needed to be known for OUI
// Execution for Panel code I believe is very rough here too and could be done much better
// Conceptually I mean, not visually (will fix visuals later but wanted to get the functionality first)
// Mostly replicated from here: https://www.youtube.com/watch?v=t2gFel5Bins&t=1s&ab_channel=azamsharp
// Tweaked for my use case which is where I think I overcomplicated the code but could be wrong
extension OUI {
enum PanelMode: CaseIterable {
case none
case quarter
case half
case threequarters
case full
case responsive // Tightens the view as much as possible
}
struct Panel<Content: View>: View {
var mode: Binding<PanelMode>
var content: () -> Content
let deviceHeight: CGFloat = UIScreen.main.bounds.height
init(_ mode: Binding<PanelMode>, #ViewBuilder content: #escaping () -> Content) {
self.mode = mode
self.content = content
}
var body: some View {
Group {
if mode.wrappedValue == .responsive {
responsiveBody
} else if mode.wrappedValue == .half || mode.wrappedValue == .quarter {
halfQuarterBody
} else {
fullBody
}
}
.animation(.spring())
}
var panelTemplate: some View {
Color.offBlack
.frame(maxWidth: .infinity, maxHeight: mode.wrappedValue == .responsive ? nil : .infinity)
.clipShape(RoundedRectangle(cornerRadius: 10))
.border(Color.blue)
}
var fullBody: some View {
panelTemplate
.overlay {
content()
.padding()
}
.offset(y: calculateOffset())
}
var halfQuarterBody: some View {
panelTemplate
.overlay {
VStack {
content()
.padding()
Spacer()
}
}
.offset(y: calculateOffset())
}
var responsiveBody: some View {
VStack {
Spacer()
content()
.padding()
.frame(maxWidth: .infinity)
.background { panelTemplate }
}
}
private func calculateOffset() -> CGFloat {
switch mode.wrappedValue {
case .none:
return deviceHeight
case .quarter:
return (3/4) * deviceHeight
case .half:
return (1/2) * deviceHeight
case .threequarters:
return (1/4) * deviceHeight
case .full:
return 0
case .dynamic:
return 0
}
}
}
}

Presenting a modal view sheet from a Sub view

I am trying to present a sheet from a sub view selected from the menu item on the navigation bar but the modal Sheet does does not display. I spent a few days trying to debug but could not pin point the problem.
I am sorry, this is a little confusing and will show a simplified version of the code to reproduce. But in a nutshell, the problem seems to be a sheet view that I have as part of the main view. Removing the sheet code from the main view displays the sheet from the sub view. Unfortunately, I don't have the freedom to change the Mainview.swift
Let me show some code to make it easy to understand....
First, before showing the code, the steps to repeat the problem:
click on the circle with 3 dots in the navigation bar
select the second item (Subview)
click on the "Edit Parameters" button and the EditParameters() view will not display
ContentView.swift (just calls the Mainview()). Included code to copy for reproducing issue :-)
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Mainview()
}
}
}
}
Mainview.swift. This is a simplified version of the actual App which is quite complex and I don't have leeway to change much here unfortunately!
fileprivate enum CurrentView {
case summary, sub
}
enum OptionSheet: Identifiable {
var id: Self {self}
case add
}
struct Mainview: View {
#State private var currentView: CurrentView? = .summary
#State private var showSheet: OptionSheet? = nil
var body: some View {
GeometryReader { g in
content.frame(width: g.size.width, height: g.size.height)
.navigationBarTitle("Main", displayMode: .inline)
}
//Removing the below sheet view will display the sheet from the subview but with this sheet here, it the sheet from subview does not work. This is required as these action items are accessed from the second menu item (circle and arrow) navigation baritem
.sheet(item: $showSheet, content: { mode in
sheetContent(for: mode)
})
.toolbar {
HStack {
trailingBarItems
actionItems
}
}
}
var actionItems: some View {
Menu {
Button(action: {
showSheet = .add
}) {
Label("Add Elements", systemImage: "plus")
}
} label: {
Image(systemName: "cursorarrow.click").resizable()
}
}
var trailingBarItems: some View {
Menu {
Button(action: {currentView = .summary}) {
Label("Summary", systemImage: "list.bullet.rectangle")
}
Button(action: {currentView = .sub}) {
Label("Subview", systemImage: "circle")
}
} label: {
Image(systemName: "ellipsis.circle").resizable()
}
}
#ViewBuilder
func sheetContent(for mode: OptionSheet) -> some View {
switch mode {
case .add:
AddElements()
}
}
#ViewBuilder
var content: some View {
if let currentView = currentView {
switch currentView {
case .summary:
SummaryView()
case .sub:
SubView()
}
}
}
}
Subview.swift. This is the file that contains the button "Edit Parameters" which does not display the sheet. I am trying to display the sheet from this view.
struct SubView: View {
#State private var editParameters: Bool = false
var body: some View {
VStack {
Button(action: {
editParameters.toggle()
}, label: {
HStack {
Image(systemName: "square.and.pencil")
.font(.headline)
Text("Edit Parameters")
.fontWeight(.semibold)
.font(.headline)
}
})
.padding(10)
.foregroundColor(Color.white)
.background(Color(.systemBlue))
.cornerRadius(20)
.sheet(isPresented: $editParameters, content: {
EditParameterView()
})
.padding()
Text("Subview....")
}
.padding()
}
}
EditParameters.swift. This is the view it should display when the Edit Parameters button is pressed
struct EditParameterView: View {
var body: some View {
Text("Edit Parameters...")
}
}
Summaryview.swift. Nothing special here. just including for completeness
struct SummaryView: View {
var body: some View {
Text("Summary View")
}
}
In SwiftUI, you can't have 2 .sheet() modifiers on the same hierarchy. Here, the first .sheet() modifier is on one of the parent views to the second .sheet(). The easy solution is to move one of the .sheets() so it's own hierarchy.
You could either use ZStacks:
var body: some View {
ZStack {
GeometryReader { g in
content.frame(width: g.size.width, height: g.size.height)
.navigationBarTitle("Main", displayMode: .inline)
}
ZStack{ }
.sheet(item: $showSheet, content: { mode in
sheetContent(for: mode)
})
}
.toolbar {
HStack {
trailingBarItems
actionItems
}
}
}
or more elegantly:
var body: some View {
GeometryReader { g in
content.frame(width: g.size.width, height: g.size.height)
.navigationBarTitle("Main", displayMode: .inline)
}
.background(
ZStack{ }
.sheet(item: $showSheet, content: { mode in
sheetContent(for: mode)
})
)
.toolbar {
HStack {
trailingBarItems
actionItems
}
}
}

Custom TabView navigation in SwiftUI

I am trying to create a a custom TabView for my app. I have followed a tutorial for this but i am unavble to get the view to change depending on what button is pressed. My code is below for a button displayed on my TabView. when this is pressed i want HomeView to show and when the Account button is pressed to show AccountView etyc etc.
I was wondering how i can get around this. I have tried using NavLinks but with no luck as i unable to use the animation.
I am new to SwiftUI and trying to learn as i go.
Thank you
Button{
withAnimation{
index = 0
}
}
label: {
HStack(spacing: 8){
Image(systemName: "house.fill")
.foregroundColor(index == 0 ? .white : Color.black.opacity(0.35))
.padding(10)
.background(index == 0 ? Color("BrightGreen") : Color.clear)
.cornerRadius(8)
Text(index == 0 ? "Home" : "")
.foregroundColor(.black)
}
}
You can use #State variable to decide which view should be shown to user. Then depending on what button is tapped set value of this variable. I've made really simple code to show the idea.
// Views to show
struct HomeView: View {
var body: some View {
Text("Home View")
}
}
struct AccountView: View {
var body: some View {
Text("Account View")
}
}
// Enum containing views available in tabbar
enum ViewToDisplay {
case home
case account
}
struct ContentView: View {
#State var currentView: ViewToDisplay = .home
var body: some View {
VStack {
switch currentView{
case .home:
HomeView()
case .account:
AccountView()
}
Spacer()
// Tabbar buttons
HStack {
Button(action: { currentView = .home }) { Text("Home") }
Button(action: { currentView = .account }) { Text("Account") }
}
}
}
}
It works like this:

Resources