I have a SwiftUI app that uses a sidebar on iPad.
Using a List with the modifier.listStyle(.sidebar) the inset spacing no longer applies when rotating, however when I force close the app and reopen it appears normal.
Cases where edge spacing no longer applies:
When user first signs in
rotates device
collapses sidebar and reopens)
The problem goes away when force quitting the app and reloading when user is signed in.
ContentView
struct ContentView: View {
#State var signedIn = false
var body: some View {
if signedIn = false {
Text("Sign In").onTapGesture(){signedIn = true}
}
if signedIn = true {
AppSidebarNavigation()
}
}
}
Sidebar
struct AppSidebarNavigation: View {
enum NavigationItem {
case home
case expenses
}
#State private var selection: NavigationItem? = .home
var body: some View {
NavigationView {
sidebar
.navigationTitle("")
.navigationBarTitleDisplayMode(.inline)
.navigationBarHidden(true)
// Main View
HomeView()
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
extension AppSidebarNavigation {
var sidebar: some View {
List(selection: $selection) {
Group {
NavigationLink(destination: HomeView()
.environmentObject(store), tag: NavigationItem.home, selection: $selection) {
Label("Homes", systemImage: "house")
}
.tag(NavigationItem.home)
NavigationLink(destination: Expenses(),
tag: NavigationItem.expenses, selection: $selection) {
Label("Expenses", systemImage: "arrow.right.arrow.left")
}
.tag(NavigationItem.expenses)
.navigationBarTitleDisplayMode(.large)
}
}
.listStyle(.sidebar)
}
}
So I would consider trying
.listRowInsets(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8))
or
.listRowInsets(EdgeInsets())
Even add
.listRowInsets(.init())
However your problem may be due to your subviews overriding TableView rows. Check if you have something like (I am not sure the exact code)
init() {
UITableViewCell.removeEdgeInsets
}
Related
The .navigationTitle on some views seem to be having some problems. On some views (and only some of the time), the .navigationTitle will not change from .large to .inline as would be expected. Instead, the title stays in place when scrolling up, and the navigation bar is completely invisible (as outlined in the video below). This is all reproducible every time.
Video of reproducible .navigationTitle bugs
I haven't found any people on stack overflow or the Apple Developer forums who have run into this exact issue. There have some people who have produced similar results as this, but those were all fixed by removing some stylizing code to the .navigationbar, of which I am not making any modifications to it anywhere in my code.
Below are some snippets of my code:
import SwiftUI
struct WelcomeUI: View {
var body: some View {
NavigationView {
VStack {
//NavigationLink(destination: SignupUI(), label: {
//Text("Sign Up")
//}
NavigationLink(destination: LoginUI(), label: {
Text("Log In")
})
}
}
}
}
struct LoginUI: View {
var body: some View {
VStack {
NavigationLink(destination: MainUI(), label: { Text("Log In") })
//Button(action: { ... }
}
.navigationBarHidden(false)
}
}
struct MainUI: View {
#State var selectedTab: Views = .add
var body: some View {
TabView(selection: $selectedTab) {
SpendingView()
.tabItem {
Image(systemName: "bag.circle")
Text("Spending")
}.tag(Views.spending)
Text("Adding View")
.tabItem {
Image(systemName: "plus")
Text("Add")
}.tag(Views.add)
Text("Edit View")
.tabItem {
Image(systemName: "pencil")
Text("Edit")
}.tag(Views.edit)
SettingsView()
.tabItem {
Image(systemName: "gear")
Text("Settings")
}.tag(Views.settings)
}
.navigationBarTitle(Text(selectedTab.rawValue))
.navigationBarBackButtonHidden(true)
}
}
enum Views: String {
case spending = "Spending"
case add = "Add"
case edit = "Edit"
case settings = "Settings"
}
struct SettingsView: View {
var body: some View {
VStack{
ZStack {
Form {
Section(header: Text("Section Header")) {
NavigationLink(destination: WelcomeUI()) {
Text("Setting Option")
}
}
Section {
//Button("Log Out") {
//self.logout()
//}
Text("Log Out")
}
}
Button("say-high", action: {print("Hi")})
}
}
}
}
struct SpendingView: View {
var body: some View {
ScrollView{
Text("SpendingView")
NavigationLink("subSpending", destination: SubSpendingView())
}.padding()
}
}
struct SubSpendingView: View {
var body: some View {
ScrollView{
Text("SubSpendingView")
}.navigationBarTitle("SubSpending")
}
}
It almost seems like a bug in SwiftUI itself just because the fact that bringing down the control centre makes it kind of work, but with no animation (as seen in the video). Also, changing which view is selected first in #State var selectedTab: Views seems to let the view selected to work as expected, but lets the rest of the tabs mess up.
When I build and run the app on my iPad, it behaves as expected with no bugs, it's only when run on my iPhone and the iOS simulator on Mac that it does this, any way to fix this?
For this to work flawlessly the ScrollView needs to be the direct child of the NavigationView. I ran into a similar issue with wanting to dismiss the TabView when I navigating but SwiftUI won't let that happen. Each tab needs to be a NavigationView and you need to dismiss the TabView creatively if that is what you want.
TabView {
NavigationView {
ScrollView {
// your view here
}
}.tabItem {
// tab label
}
// etc
}
Essentially the navigation view needs to be a child (in the brackets) of the tab view and the scrollview needs to be the direct child of the navigation view.
Use navigationBarTitle("Title") and navigationBarBackButtonHidden(true) on the TabView's sub-view, not on itself.
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
}
.navigationBarTitle("Title")
.navigationBarBackButtonHidden(true)
}
}
}
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
}
)
}
}
}
}
I am calling API when tab item is appeared if there is any changes. Why onAppear called after called onDisappear?
Here is the simple example :
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
Text("Home")
.navigationTitle("Home")
.onAppear {
print("Home appeared")
}
.onDisappear {
print("Home disappeared")
}
}
.tabItem {
Image(systemName: "house")
Text("Home")
}.tag(0)
NavigationView {
Text("Account")
.navigationTitle("Account")
.onAppear {
print("Account appeared")
}
.onDisappear {
print("Account disappeared")
}
}
.tabItem {
Image(systemName: "gear")
Text("Account")
}.tag(1)
}
}
}
Just run above code and we will see onAppear after onDisappear.
Home appeared
---After switch tab to Account---
Home disappeared
Account appeared
Home appeared
Is there any solution to avoid this?
It's very annoying bug, imagine this scenario:
Home view onAppear method contains a timer which is fetching data repeatedly.
Timer is triggered invisibly by switching to the Account view.
Workaround:
Create a standalone view for every embedded NavigationView content
Pass the current selection value on to standalone view as #Binding parameter
E.g.:
struct ContentView: View {
#State var selected: MenuItem = .HOME
var body: some View {
return TabView(selection: $selected) {
HomeView(selectedMenuItem: $selected)
.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
VStack {
Image(systemName: "house")
Text("Home")
}
}
.tag(MenuItem.HOME)
AccountView(selectedMenuItem: $selected)
.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
VStack {
Image(systemName: "gear")
Text("Account")
}
}
.tag(MenuItem.ACCOUNT)
}
}
}
enum MenuItem: Int, Codable {
case HOME
case ACCOUNT
}
HomeView:
struct HomeView: View {
#Binding var selectedMenuItem: MenuItem
var body: some View {
return Text("Home")
.onAppear(perform: {
if MenuItem.HOME == selectedMenuItem {
print("-> HomeView")
}
})
}
}
AccountView:
struct AccountView: View {
#Binding var selectedMenuItem: MenuItem
var body: some View {
return Text("Account")
.onAppear(perform: {
if MenuItem.ACCOUNT == selectedMenuItem {
print("-> AccountView")
}
})
}
}
To whom it may help.
Because this behaviour I only could reproduce on iOS 14+, I end up using https://github.com/NicholasBellucci/StatefulTabView (which properly only get called when showed; but don't know if it's a bug or not, but it works with version 0.1.8) and TabView on iOS 13+.
I'm not sure why you are seeing that behaviour in your App. But I can explain why I was seeing it in my App.
I had a very similar setup to you and was seeing the same behaviour running an iOS13 App on iOS14 beta. In my Home screen I had a custom Tab Bar that would animate in and out when a detail screen was displayed. The code for triggering the hiding of the Tab Bar was done in the .onAppear of the Detail screen. This was triggering the Home screen to be redrawn and the .onAppear to be called. I removed the animation and found a much better set up due to this bug and the Home screen .onAppear stopped being called.
So if you have something in your Account Screen .onAppear that has a visual effect on the Home Screen then try commenting it out and seeing if it fixes the issue.
Good Luck.
I have been trying to understand this behavior for a number of days now. If you are working with a TabView, all of your onAppears() / onDisapear() will fire immediately on app init and never again. Which actually makes since I guess?
This was my solution to fix this:
import SwiftUI
enum TabItems {
case one, two
}
struct ContentView: View {
#State private var selection: TabItems = .one
var body: some View {
TabView(selection: $selection) {
ViewOne(isSelected: $selection)
.tabBarItem(tab: .one, selection: $selection)
ViewTwo(isSelected: $selection)
.tabBarItem(tab: .two, selection: $selection)
}
}
}
struct ViewOne: View {
#Binding var isSelected: TabItems
var body: some View {
Text("View One")
.onChange(of: isSelected) { _ in
if isSelected == .one {
// Do something
}
}
}
}
struct ViewTwo: View {
#Binding var isSelected: TabItems
var body: some View {
Text("View Two")
.onChange(of: isSelected) { _ in
if isSelected == .two {
// Do something
}
}
}
}
View Modifier for custom TabView
struct TabBarItemsPreferenceKey: PreferenceKey {
static var defaultValue: [TabBarItem] = []
static func reduce(value: inout [TabBarItem], nextValue: () -> [TabBarItem]) {
value += nextValue()
}
}
struct TabBarItemViewModifer: ViewModifier {
let tab: TabBarItem
#Binding var selection: TabBarItem
func body(content: Content) -> some View {
content
.opacity(selection == tab ? 1.0 : 0.0)
.preference(key: TabBarItemsPreferenceKey.self, value: [tab])
}
}
extension View {
func tabBarItem(tab: TabBarItem, selection: Binding<TabBarItem>) -> some View {
modifier(TabBarItemViewModifer(tab: tab, selection: selection))
}
}
Here is a bug in SwiftUI when you show modal from button inside navigation bar items.
In code below Button 1 works as expected, but Button 2 works only once:
struct DetailView: View {
#Binding var isPresented: Bool
#Environment (\.presentationMode) var presentationMode
var body: some View {
NavigationView {
Text("OK")
.navigationBarTitle("Details")
.navigationBarItems(trailing: Button(action: {
self.isPresented = false
// or:
// self.presentationMode.wrappedValue.dismiss()
}) {
Text("Done").bold()
})
}
}
}
struct ContentView: View {
#State var showSheetView = false
var body: some View {
NavigationView {
Group {
Text("Master")
Button(action: { self.showSheetView.toggle() }) {
Text("Button 1")
}
}
.navigationBarTitle("Main")
.navigationBarItems(trailing: Button(action: {
self.showSheetView.toggle()
}) {
Text("Button 2").bold()
})
}.sheet(isPresented: $showSheetView) {
DetailView(isPresented: self.$showSheetView)
}
}
}
This bug is from the middle of the last year, and it still in Xcode 11.3.1 + iOS 13.3 Simulator and iOS 13.3.1 iPhone XS.
Is here any workaround to make button work?
EDIT:
Seems to be tap area goes somewhere down and it's possible to tap below button to show modal.
Temporary solution to this is to use inline navigation bar mode:
.navigationBarTitle("Main", displayMode: .inline)
Well, the issue is in bad layout (seems broken constrains) of navigation bar button after sheet has closed
It is clearly visible in view hierarchy debug:
Here is a fix (workaround of course, but safe, because even after issue be fixed it will continue working). The idea is not to fight with broken layout but just create another button, so layout engine itself remove old-bad button and add new one refreshing layout. The instrument for this is pretty known - use .id()
So modified code:
struct ContentView: View {
#State var showSheetView = false
#State private var navigationButtonID = UUID()
var body: some View {
NavigationView {
Group {
Text("Master")
Button(action: { self.showSheetView.toggle() }) {
Text("Button 1")
}
}
.navigationBarTitle("Main")
.navigationBarItems(trailing: Button(action: {
self.showSheetView.toggle()
}) {
Text("Button 2").bold() // recommend .padding(.vertical) here
}
.id(self.navigationButtonID)) // force new instance creation
}
.sheet(isPresented: $showSheetView) {
DetailView(isPresented: self.$showSheetView)
.onDisappear {
// update button id after sheet got closed
self.navigationButtonID = UUID()
}
}
}
}
when I have a TabView{} and the first Tab has a NavigationView, when I click on a Row, I want that TabView{} to disappear. How do I do that?
Same Issue here: How to hide the TabBar when navigate with NavigationLink in SwiftUI?
But unfortunately no solution.
There is no way to do that currently. For example, NavigationView responds to the .navigationBarHidden(_:) method on its descendants, but there is not an equivalent for TabView.
If this is something you'd like to see, let Apple know.
there's no way to hide TabView so I had to add TabView inside ZStack as this:
var body: some View {
ZStack {
TabView {
TabBar1().environmentObject(self.userData)
.tabItem {
Image(systemName: "1.square.fill")
Text("First")
}
TabBar2()
.tabItem {
Image(systemName: "2.square.fill")
Text("Second")
}
}
if self.userData.showFullScreen {
FullScreen().environmentObject(self.userData)
}
}
}
UserData:
final class UserData: ObservableObject {
#Published var showFullScreen = false}
TabBar1:
struct TabBar1: View {
#EnvironmentObject var userData: UserData
var body: some View {
Text("TabBar 1")
.edgesIgnoringSafeArea(.all)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.background(Color.green)
.onTapGesture {
self.userData.showFullScreen.toggle()
}
}
}
FullScreen:
struct FullScreen: View {
#EnvironmentObject var userData: UserData
var body: some View {
Text("FullScreen")
.edgesIgnoringSafeArea(.all)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.background(Color.red)
.onTapGesture {
self.userData.showFullScreen.toggle()
}
}
}
check full code on Github
there's also some other ways but it depends on the structure of the views
To solve this limitation, I came out with this approach:
Created an enum to identify the tabs
enum Tabs: Int {
case tab1
case tab2
var title: String {
switch self {
case .tab1: return "Tab 1 Title"
case .tab2: return "Tab 2 Title"
}
}
var imageName: String {
switch self {
case .tab1: return "star" // Example using SF Symbol
case .tab2: return "ellipsis.circle"
}
}
}
Inside the view, such as ContentView.swift, added a property like this:
#State private var selectedTab = Tabs.tab1
Inside the body:
NavigationView {
TabView(selection: $selectedTab) {
ViewExample1()
.tabItem {
Image(systemName: Tabs.tab1.imageName)
Text(Tabs.tab1.title)
}.tag(Tabs.tab1)
ViewExample2()
.tabItem {
Image(systemName: Tabs.tab2.imageName)
Text(Tabs.tab2.title)
}.tag(Tabs.tab2)
}
.navigationBarTitle(selectedTab.title)
}
That's all. I hope this might be helpful.
Note: Just be aware this workaround hides the TabView in any and all child views, if you want to hide in just a particular view, this won't give you the result that you looking for.
Hopefully, Apple implements an (official and proper) option to hide the TabView soon.