Handle Tab Selection SwiftUI - ios

Following tutorials, I have the following code to show a tab view with 3 tab items all with an icon on them, When pressed they navigate between the three different views. This all works fine, however, I want to be able to handle the selection and only show views 2 or 3 if certain criteria are met.
Is there a way to get the selected value and check it then check my own criteria and then show the view is criteria is met, or show an alert if it is not saying they can't use that view at the moment.
Essentially I want to be able to intercept the selection value before it switches out the view, maybe I need to rewrite all of this but this is the functionality I'm looking for as this is how I had my previous app working using the old framework.
#State private var selection = 1
var body: some View
{
TabbedView(selection: $selection)
{
View1().tabItemLabel(
VStack
{
Image("icon")
Text("")
})
.tag(1)
View2().tabItemLabel(
VStack
{
Image("icon")
Text("")
}).tag(2)
View3().tabItemLabel(
VStack
{
Image("icon")
Text("")
}).tag(3)
}
}

You can do it by changing the value of selection on tap. You can use .onAppear() method for a particular tab to check your condition:
#State private var selection = 1
var conditionSatisfied = false
var body: some View
{
TabbedView(selection: $selection)
{
View1().tabItemLabel(
VStack
{
Image("icon")
Text("")
})
.tag(1)
View2().tabItemLabel(
VStack
{
Image("icon")
Text("")
}).tag(2)
.onAppear() {
if !conditionSatisfied {
self.selection = 1
}
}
View3().tabItemLabel(
VStack
{
Image("icon")
Text("")
}).tag(3)
.onAppear() {
if !conditionSatisfied {
self.selection = 1
}
}
}
}

Related

How to deselect a list button within a NavigationLink

I have a list with one of the list items in a NavigationLink because it needs to move to a detail view once tapped. When I come back from that detail view the list button is still in a selected state. Prior to SwiftUI, I'd just tell the .isSelected to equal false but I can't figure out how to do that in SwiftUI?
List {
NavigationLink(destination: SettingsStartdayView()){
HStack {
Text("Start Day Notification")
Spacer()
Text(startDayNotificationSetting)
.font(.subheadline)
.foregroundColor(Color.gray)
.multilineTextAlignment(.trailing)
}
}
}
Starting List View
After Detail View, Back
This View is being loaded into the main app's view through:
NavigationView{
TabView(selection: $isSelectedTab) {
SettingsView()
.tabItem{
}.tag(1)
Here's a public project that has a complete example of what I'm dealing with: https://gitlab.com/jammyman34/test-sounds-project
Go to the Settings tab and then click the top list item to go to the detail page. Notice how the list you clicked stays selected when you go back. It doesn't clear unless you change to the other tab.
The issue here is the Text which is above the List in SettingsView - a bug reported here.
Instead, you can use a native navigation title and attach it to the TabView.
struct SettingsHomeView: View {
#State var startDayNotificationSetting: String = "8:30AM"
#State var appVersion: String = "0.01"
var body: some View {
// no `Text` above `List`
List {
NavigationLink(destination: SettingsStartdayView()){
HStack {
Text("Start Day Notification")
Spacer()
Text(startDayNotificationSetting)
.font(.subheadline)
.foregroundColor(Color.gray)
.multilineTextAlignment(.trailing)
//Image(systemName: "chevron.right")
}
}
}
}
}
struct ContentView: View {
#State private var isSelectedTab = 1 // select the first tab
var body: some View {
NavigationView{
TabView(selection: $isSelectedTab) {
// ...
}
// control displaying the title depending on the `isSelectedTab`
.navigationTitle("Settings")
.navigationBarHidden(isSelectedTab == 1)
}
}
}

SwiftUI. Present a detail view pushed from the root view as the initial application view

I have two SwiftUI views, the first view has a navigation link to the second view and I want to show the second view that is "pushed" out of the first view, as the initial application view.
This is the behavior of the iOS Notes app, where users see a list of notes as the initial view controller and can return to the folder list with the back navigation button.
Can I implement this with SwiftUI and how?
Here is a simple demo. Prepared & tested with Xcode 11.7 / iOS 13.7
struct ContentView: View {
#State private var isActive = false
var body: some View {
NavigationView {
NavigationLink(destination: Text("Second View"), isActive: $isActive) {
Text("First View")
}
}
.onAppear { self.isActive = true }
}
}
you can add another state variable to hide the first view until the second view appears on the screen.
struct ContentView1: View {
#State private var isActive = false
#State private var showView = false
var body: some View {
NavigationView {
NavigationLink(destination: Text("Second View")
.onAppear {
self.showView = true
},
isActive: $isActive) {
if self.showView {
Text("First View")
} else {
EmptyView()
}
}
}
.onAppear {
self.isActive = true
}
}
}
As mentioned in my comments to another answer, by setting an initial state for a variable that controls the presentation of the second view to true, your ContentView presents this second view as the initial view.
I've tested this using the simulator and on device. This appears to solve your problem and does not present the transition from the first view to the second view to the user - app opens to the second view.
struct ContentView: View {
#State private var isActive = true
var body: some View {
NavigationView {
NavigationLink(destination: Text("Second View"), isActive: $isActive) {
Text("First View")
}
}
}
}
I made my own implementation based on #Asperi and #Mohammad Rahchamani answers.
This implementation allows you to navigate even from a list with multiple navigation links. Tested on Xcode 12 with SwiftUI 2.0.
struct IOSFolderListView: View {
#State var isActive = false
#State var wasViewShown = false
var body: some View {
let list = List {
NavigationLink(destination: Text("SecondView").onAppear {
self.wasViewShown = true
}, isActive: $isActive) {
Text("SecondView")
}
NavigationLink(destination: Text("ThirdView")) {
Text("ThirdView")
}
.onAppear {
self.isActive = false
}
}
if wasViewShown {
list.listStyle(GroupedListStyle())
.navigationBarTitle("FirstView")
.navigationBarItems(leading: Image(systemName: "folder.badge.plus"), trailing: Image(systemName: "square.and.pencil"))
} else {
list.opacity(0)
.onAppear {
self.isActive = true
}
}
}
}

SwitUI - Two navigationLink in a list

I have two NavigationLink in a cell of my List
I want to go to destination1 when I tap once,and go to destination2 when I tap twice.
So I added two tap gesture to control the navigation.
But when I tap,there are two questions:
1 The tap gesture block won't be called.
2 The two navigation link will be both activated automatically even if they are behind a TextView.
The real effect is: Tap the cell -> go to Destination1-> back to home -> go to Destination2 -> back to home
Here is my code :
struct MultiNavLink: View {
#State var mb_isActive1 = false;
#State var mb_isActive2 = false;
var body: some View {
return
NavigationView {
List {
ZStack {
NavigationLink("", destination: Text("Destination1"), isActive: $mb_isActive1)
NavigationLink("", destination: Text("Destination2"), isActive: $mb_isActive2)
Text("Single tap::go to destination1\nDouble tap,go to destination2")
}
.onTapGesture(count: 2, perform: {()->Void in
NSLog("Double tap::to destination2")
self.mb_isActive2 = true
}).onTapGesture(count: 1, perform: {()->Void in
NSLog("Single tap::to destination1")
self.mb_isActive1 = true
})
}.navigationBarTitle("MultiNavLink",displayMode: .inline)
}
}
}
I have tried remove the List element,then everything goes as I expected.
It seems to be the List element that makes everything strange.
I found this question:SwiftUI - Two buttons in a List,but the situation is different from me.
I am expecting for your answer,thank you very much...
Try the following approach - the idea is to hide links in background of visible content and make them inactive for UI, but activated programmatically.
Tested with Xcode 12 / iOS 14.
struct MultiNavLink: View {
var body: some View {
return
NavigationView {
List {
OneRowView()
}.navigationBarTitle("MultiNavLink", displayMode: .inline)
}
}
}
struct OneRowView: View {
#State var mb_isActive1 = false
#State var mb_isActive2 = false
var body: some View {
ZStack {
Text("Single tap::go to destination1\nDouble tap,go to destination2")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.background(Group {
NavigationLink(destination: Text("Destination1"), isActive: $mb_isActive1) {
EmptyView() }
.buttonStyle(PlainButtonStyle())
NavigationLink(destination: Text("Destination2"), isActive: $mb_isActive2) {
EmptyView() }
.buttonStyle(PlainButtonStyle())
}.disabled(true))
.highPriorityGesture(TapGesture(count: 2).onEnded {
self.mb_isActive2 = true
})
.onTapGesture(count: 1) {
self.mb_isActive1 = true
}
}
}
Navigation link has a initializer that takes a binding selection and whenever that selection is set to the value of the NavigationLink tag, the navigation link will trigger.
As a tip, if the app can't differentiate and identify your taps, and even with two taps, still the action for one-tap will be triggered, then you can use a simultaneous gesture(.simultaneousGesture()) modifier instead of a normal gesture(.gesture()) modifier.
struct someViewName: View {
#State var navigationLinkTriggererForTheFirstOne: Bool? = nil
#State var navigationLinkTriggererForTheSecondOne: Bool? = nil
var body: some View {
VStack {
NavigationLink(destination: SomeDestinationView(),
tag: true,
selection: $navigationLinkTriggererForTheFirstOne) {
EmptyView()
}
NavigationLink(destination: AnotherDestinationView(),
tag: true,
selection: $navigationLinkTriggererForTheSecondOne) {
EmptyView()
}
NavigationView {
Button("tap once to trigger the first navigation link.\ntap twice to trigger the second navigation link.") {
// tap once
self.navigationLinkTriggererForTheFirstOne = true
}
.simultaneousGesture(
TapGesture(count: 2)
.onEnded { _ in
self.navigationLinkTriggererForTheSecondOne = true
}
)
}
}
}
}

How to replace the current view in SwiftUI?

I am developing an app with SwiftUI.
I have a NavigationView and I have buttons on the navigation bar. I want to replace the current view (which is a result of a TabView selection) with another one.
Basically, when the user clicks "Edit" button, I want to replace the view with another view to make the edition and when the user is done, the previous view is restored by clicking on a "Done" button.
I could just use a variable to dynamically choose which view is displayed on the current tab view, but I feel like this isn't the "right way to do" in SwiftUI. And this way I could not apply any transition visual effect.
Some code samples to explain what I am looking for.
private extension ContentView {
#ViewBuilder
var navigationBarLeadingItems: some View {
if tabSelection == 3 {
Button(action: {
print("Edit pressed")
// Here I want to replace the tabSelection 3 view by another view temporarly and update the navigation bar items
}) {
Text("Edit")
}
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
TabView(selection: $tabSelection) {
ContactPage()
.tabItem {
Text("1")
}
.tag(1)
Text("Chats")
.tabItem() {
Text("2")
}
.tag(2)
SettingsView()
.tabItem {
Text("3")
}
.tag(3)
}.navigationBarItems(leading: navigationBarLeadingItems)
}
}
}
Thank you
EDIT
I have a working version where I simply update a toggle variable in my button action that makes my view display one or another thing, it is working but I cannot apply any animation effect on it, and it doesn't look "right" in SwiftUI, I guess there is something better that I do not know.
If you just want to add animations you can try:
struct ContentView: View {
...
#State var showEditView = false
var body: some View {
NavigationView {
TabView(selection: $tabSelection) {
...
view3
.tabItem {
Text("3")
}
.tag(3)
}
.navigationBarItems(leading: navigationBarLeadingItems)
}
}
}
private extension ContentView {
var view3: some View {
VStack {
if showEditView {
FormView()
.background(Color.red)
.transition(.slide)
} else {
Text("View 3")
.background(Color.blue)
.transition(.slide)
}
}
}
}
struct FormView: View {
var body: some View {
Form {
Text("test")
}
}
}
A possible alternative is to use a ViewRouter: How To Navigate Between Views In SwiftUI By Using An #EnvironmentObject.

SwiftUI Disable specific tabItem selection in a TabView?

I have a TabView that presents a sheet after tapping on the [+] (2nd) tabItem. At the same time, the ContentView is also switching the TabView's tab selection, so when I dismiss the sheet that is presented, the selected tab is a blank one without any content. Not an ideal user experience.
My question:
I am wondering how I can simply disable that specific tabItem so it doesn't "behave like a tab" and simply just present's the sheet while maintaining the previous tab selection prior to tapping the [+] item. Is this possible with SwiftUI or should I got about this another way to achieve this effect?
Image of my tab bar:
Here's the code for my ContentView where my TabView is:
struct SheetPresenter<Content>: View where Content: View {
#EnvironmentObject var appState: AppState
#Binding var isPresenting: Bool
var content: Content
var body: some View {
Text("")
.sheet(isPresented: self.$isPresenting, onDismiss: {
// change back to previous tab selection
print("New listing sheet was dismissed")
}, content: { self.content})
.onAppear {
DispatchQueue.main.async {
self.isPresenting = true
print("New listing sheet appeared with previous tab as tab \(self.appState.selectedTab).")
}
}
}
}
struct ContentView: View {
#EnvironmentObject var appState: AppState
#State private var selection = 0
#State var newListingPresented = false
var body: some View {
$appState.selectedTab back to just '$selection'
TabView(selection: $appState.selectedTab){
// Browse
BrowseView()
.tabItem {
Image(systemName: (selection == 0 ? "square.grid.2x2.fill" : "square.grid.2x2")).font(.system(size: 22))
}
.tag(0)
// New Listing
SheetPresenter(isPresenting: $newListingPresented, content: NewListingView(isPresented: self.$newListingPresented))
.tabItem {
Image(systemName: "plus.square").font(.system(size: 22))
}
.tag(1)
// Bag
BagView()
.tabItem {
Image(systemName: (selection == 2 ? "bag.fill" : "bag")).font(.system(size: 22))
}
.tag(2)
// Profile
ProfileView()
.tabItem {
Image(systemName: (selection == 3 ? "person.crop.square.fill" : "person.crop.square")).font(.system(size: 22))
}
.tag(3)
}.edgesIgnoringSafeArea(.top)
}
}
And here's AppState:
final class AppState: ObservableObject {
#Published var selectedTab: Int = 0
}
You are pretty close to what you want to achieve. You will just need to preserve the previous selected tab index and reset the current selected tab index with that preserved value at the time of the dismissal of the sheet. That means:
.sheet(isPresented: self.$isPresenting, onDismiss: {
// change back to previous tab selection
self.appState.selectedTab = self.appState.previousSelectedTab
}, content: { self.content })
So how do you keep track of the last selected tab index that stays in sync with the selectedTab property of the AppState? There may be more ways to do that with the APIs from Combine framework itself, but the simplest solution that comes to my mind is:
final class AppState: ObservableObject {
// private setter because no other object should be able to modify this
private (set) var previousSelectedTab = -1
#Published var selectedTab: Int = 0 {
didSet {
previousSelectedTab = oldValue
}
}
}
Caveats:
The above solution of may not be the exact thing as disable specific tab item selection but after you dismiss the sheet it will revert back with a soothing animation to the selected tab prior to presenting the sheet. Here is the result.
You may add something in the dismiss of sheet to switch the tabView to other tabs. Maybe you can insert some animation during the process.
struct SheetPresenter<Content>: View where Content: View {
#EnvironmentObject var appState: AppState
#Binding var isPresenting: Bool
#Binding var showOtherTab: Int
var content: Content
var body: some View {
Text("")
.sheet(isPresented: self.$isPresenting,
onDismiss: {
// change back to previous tab selection
self.showOtherTab = 0
} ,
content: { self.content})
.onAppear {
DispatchQueue.main.async {
self.isPresenting = true
print("New listing sheet appeared with previous tab as tab \(self.appState.selectedTab).")
}
}
}
}
struct ContentView: View {
#EnvironmentObject var appState: AppState
#State private var selection = 0
#State var newListingPresented = false
var body: some View {
// $appState.selectedTab back to just '$selection'
TabView(selection: $appState.selectedTab){
// Browse
Text("BrowseView") //BrowseView()
.tabItem {
Image(systemName: (selection == 0 ? "square.grid.2x2.fill" : "square.grid.2x2"))
.font(.system(size: 22))
} .tag(0)
// New Listing
SheetPresenter(isPresenting: $newListingPresented,
showOtherTab: $appState.selectedTab,
content: Text("1232"))//NewListingView(isPresented: self.$newListingPresented))
.tabItem {
Image(systemName: "plus.square")
.font(.system(size: 22))
} .tag(1)
// Bag
// BagView()
Text("BAGVIEW")
.tabItem {
Image(systemName: (selection == 2 ? "bag.fill" : "bag"))
.font(.system(size: 22))
} .tag(2)
// Profile
Text("ProfileView") // ProfileView()
.tabItem {
Image(systemName: (selection == 3 ? "person.crop.square.fill" : "person.crop.square"))
.font(.system(size: 22))
} .tag(3)
} .edgesIgnoringSafeArea(.top)
}
}
I was able to replicate the following behaviors of the tabview of Instagram using SwiftUI and MVVM:
when the middle tab is selected, a modal view will open
when the middle tab is closed, the previously selected tab is again selected, not the middle tab
A. ViewModels (one for the whole tabview and another for a specific tab)
import Foundation
class TabContainerViewModel: ObservableObject {
//tab with sheet that will not be selected
let customActionTab: TabItemViewModel.TabItemType = .addPost
//selected tab: this is the most important code; here, when the selected tab is the custom action tab, set the flag that is was selected, then whatever is the old selected tab, make it the selected tab
#Published var selectedTab: TabItemViewModel.TabItemType = .feed {
didSet{
if selectedTab == customActionTab {
customActionTabSelected = true
selectedTab = oldValue
}
}
}
//flags whether the middle tab is selected or not
var customActionTabSelected: Bool = false
//create the individual tabItemViewModels that will get displayed
let tabItemViewModels:[TabItemViewModel] = [
TabItemViewModel(imageName:"house.fill", title:"Feed", type: .feed),
TabItemViewModel(imageName:"magnifyingglass.circle.fill", title:"Search", type: .search),
TabItemViewModel(imageName:"plus.circle.fill", title:"Add Post", type: .addPost),
TabItemViewModel(imageName:"heart.fill", title:"Notifications", type: .notifications),
TabItemViewModel(imageName:"person.fill", title:"Profile", type: .profile),
]
}
//this is the individual tabitem ViewModel
import SwiftUI
struct TabItemViewModel: Hashable {
let imageName:String
let title:String
let type: TabItemType
enum TabItemType {
case feed
case search
case addPost
case notifications
case profile
}
}
B. View (makes use of the ViewModels)
import SwiftUI
struct TabContainerView: View {
#StateObject private var tabContainerViewModel = TabContainerViewModel()
#ViewBuilder
func tabView(for tabItemType: TabItemViewModel.TabItemType) -> some View {
switch tabItemType {
case .feed:
FeedView()
case .search:
SearchView()
case .addPost:
AddPostView(tabContainerViewModel: self.tabContainerViewModel)
case .notifications:
NotificationsView()
case .profile:
ProfileView()
}
}
var body: some View {
TabView(selection: $tabContainerViewModel.selectedTab){
ForEach(tabContainerViewModel.tabItemViewModels, id: \.self){ viewModel in
tabView(for: viewModel.type)
.tabItem {
Image(systemName: viewModel.imageName)
Text(viewModel.title)
}
.tag(viewModel.type)
}
}
.accentColor(.primary)
.sheet(isPresented: $tabContainerViewModel.customActionTabSelected) {
PicsPicker()
}
}
}
struct TabContainerView_Previews: PreviewProvider {
static var previews: some View {
TabContainerView()
}
}
Note: In the course of my investigation, I tried adding code to onAppear in the middle tab. However, I found out that there is a current bug in SwiftUI that fires the onAppear even if a different tab was tapped. So the above seems to be the best way.
Happy coding!
References:
https://www.youtube.com/watch?v=SZj3CjMfT-8

Resources