SwiftUI UINavigationBar does not ignore the top safe area. How to get rid of empty space on the top? - swiftui

Code I used to create an example of this navigation bar:
struct ContentView: View {
init() {
let defaultAppearance = UINavigationBarAppearance()
defaultAppearance.configureWithOpaqueBackground()
defaultAppearance.backgroundColor = .red
UINavigationBar.appearance().standardAppearance = defaultAppearance
UINavigationBar.appearance().setBackgroundImage(UIImage(), for: .default)
}
var body: some View {
ParentView()
}
}
struct ParentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("Tap here")
}
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarHidden(true)
.edgesIgnoringSafeArea(.all)
}
}
}
struct DetailView: View {
var body: some View {
VStack {
Text("Detail View")
}
.edgesIgnoringSafeArea(.all)
}
}
It doesn't matter where I'm putting .edgesIgnoringSafeArea(.all) it doesn't work. Is there another way to tell Navigation Bar to ignore the safe area?
If it's not the safe area problem, then I need to make the whole navigation bar to have the same height of UINavigationBarContentView displayed in UI Hierarchy:

Related

SwiftUI Navigation Controller stuttering with two Navigationlinks per List row

I am trying to create two NavigationLinks in a repeating List. Each has a separate destination. The code all works fine until I imbed the call to the root view in a List/ForEach loop. At which point the navigation becomes very strange.
Try to click on either link and then click the back indicator at the top. It will go to one NavigationLink, and then the other. Sometimes in a different order, and sometimes it will auto-return from one of the links, and othertimes it won't open the second detail view until you return from the first detail view. It does this both in Preview, as well as if you build and run the application.
I have distilled down the code to the most basic below. If you comment the 2 lines as indicated in ContentView, you will then see correct behavior.
I am running Catalina 10.15.5, xCode 11.6, with the application target of IOS 13.6.
How can I modify the code, so that it will work with the List/ForEach loop?
import SwiftUI
struct DetailView1: View {
var body: some View {
HStack {
Text("Here is Detail View 1." )}
.foregroundColor(.green)
}
}
struct DetailView2: View {
var body: some View {
HStack {
Text( "Here is Detail View 2.") }
.foregroundColor(.red)
}
}
struct RootView: View {
var body: some View {
HStack {
VStack {
NavigationLink(destination: DetailView1())
{ VStack { Image(systemName: "ant.circle").resizable()
.frame(width:75, height:75)
.scaledToFit()
}.buttonStyle(PlainButtonStyle())
Text("Tap for Detail 1.")
.foregroundColor(.green)
}
}
NavigationLink(destination: DetailView2())
{ Text("Tap for Detail 2.")
.foregroundColor(.red)
}
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
// Comment the following line for correct behavior
List { ForEach(0..<3) {_ in
RootView()
// Comment the following line for correct behavior
} }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
ContentView()
.navigationBarTitle("Strange Behavior")
}
}
}
In your case both navigation links are activated at once user tap a row, to avoid this below is possible approach
Tested with Xcode 12 / iOS 14
The idea is to have one link which is activated programmatically and destination is selected dynamically depending on which button is clicked
struct RootView: View {
#State private var isFirst = false
#State private var isActive = false
var body: some View {
HStack {
VStack {
Button(action: {
self.isFirst = true
self.isActive = true
})
{ VStack { Image(systemName: "ant.circle").resizable()
.frame(width:75, height:75)
.scaledToFit()
}
Text("Tap for Detail 1.")
.foregroundColor(.green)
}
}
Button(action: {
self.isFirst = false
self.isActive = true
})
{ Text("Tap for Detail 2.")
.foregroundColor(.red)
}
.background(
NavigationLink(destination: self.destination(), isActive: $isActive) { EmptyView() }
)
}
.buttonStyle(PlainButtonStyle())
}
#ViewBuilder
private func destination() -> some View {
if isFirst {
DetailView1()
} else {
DetailView2()
}
}
}

SwiftUI How To Hide The Navigation Bar While Keeping The Back Button

So I'm trying to hide the navigationBar in a Details view in SwiftUI. I've technically gotten it to work by using an init() in a different view, but the issue is that it's making the navigationBar transparent for the whole app, which I only want it in one view. The reason I haven't used an init() in the DetailsView is because I have a variable that needs an input, so I wasn't sure how to do that! Here is the code for the initializer:
init() {
let navBarAppearance = UINavigationBar.appearance()
navBarAppearance.backgroundColor = .clear
navBarAppearance.barTintColor = .clear
navBarAppearance.tintColor = .black
navBarAppearance.setBackgroundImage(UIImage(), for: .default)
navBarAppearance.shadowImage = UIImage()
}
Here's What The Content View and Details View code is like with the init() inside the detailsView:
// ContentView //
struct ContentView: View {
var body: some View {
NavigationView {
List {
ForEach(0..<5) { i in
NavigationLink(destination: DetailsView(test: 1)) {
Text("DetailsView \(i)")
}
}
}
.listStyle(InsetGroupedListStyle())
.navigationBarTitle("Test App")
}
}
}
// DetailsView //
struct DetailsView: View {
var test: Int
var body: some View {
ScrollView {
Text("More Cool \(test)")
Text("Cool \(test)")
Text("Less Cool \(test)")
}
}
init(test: Int) {
self.test = 8
let navBarAppearance = UINavigationBar.appearance()
navBarAppearance.backgroundColor = .clear
navBarAppearance.barTintColor = .clear
navBarAppearance.tintColor = .black
navBarAppearance.setBackgroundImage(UIImage(), for: .default)
navBarAppearance.shadowImage = UIImage()
}
}
struct DetailsView_Previews: PreviewProvider {
static var previews: some View {
DetailsView(test: 8)
}
}
It's a heavily edited version of my code, but it shows the problem I have. With no variables needing to be passed in, the init() worked to remove the bar in only that view. However, with that variable input, not only does it change all the views to the "8" for the number, but it also doesn't even hide the navigationBar. I'm not sure if I'm just doing something wrong nor if this is even the right way to do it, but any help would be appreciated!
Also, on a side note, does anyone know how to hide the statusBar in iOS 14 with the NavigationView?
I think you try to use UIKit logic instead of the SwiftUI one. This is what I would do to hide the navigation bar with a back button on the top leading side of your view.
As for hiding the status bar, I would use .statusBar(hidden: true).
But it seems not to work on iOS14. It may be a bug... You can refer yourself to the Apple documentation on this topic.
struct DetailsView: View {
#Environment(\.presentationMode) var presentation
var test: Int
var body: some View {
ZStack(alignment: .topLeading) {
ScrollView {
Text("More Cool \(test)")
Text("Cool \(test)")
Text("Less Cool \(test)")
}
Button(action: { presentation.wrappedValue.dismiss() }) {
HStack {
Image(systemName: "chevron.left")
.foregroundColor(.blue)
.imageScale(.large)
Text("Back")
.font(.title3)
.foregroundColor(.blue)
}
}
.padding(.leading)
.padding(.top)
}
.navigationTitle(Text(""))
.navigationBarHidden(true)
.statusBar(hidden: true)
}
}

SwiftUI, setting title to child views of TabView inside of NavigationView does not work

Why I am putting TabView into a NavigationView is because I need to hide the bottom tab bar when user goes into 2nd level 'detail' views which have their own bottom action bar.
But doing this leads to another issue: all the 1st level 'list' views hosted by TabView no longer display their titles. Below is a sample code:
import SwiftUI
enum Gender: String {
case female, male
}
let members: [Gender: [String]] = [
Gender.female: ["Emma", "Olivia", "Ava"], Gender.male: ["Liam", "Noah", "William"]
]
struct TabItem: View {
let image: String
let label: String
var body: some View {
VStack {
Image(systemName: image).imageScale(.large)
Text(label)
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
TabView {
ListView(gender: .female).tag(0).tabItem {
TabItem(image: "person.crop.circle", label: Gender.female.rawValue)
}
ListView(gender: .male).tag(1).tabItem {
TabItem(image: "person.crop.circle.fill", label: Gender.male.rawValue)
}
}
}
}
}
struct ListView: View {
let gender: Gender
var body: some View {
let names = members[gender]!
return List {
ForEach(0..<names.count, id: \.self) { index in
NavigationLink(destination: DetailView(name: names[index])) {
Text(names[index])
}
}
}.navigationBarTitle(Text(gender.rawValue), displayMode: .inline)
}
}
struct DetailView: View {
let name: String
var body: some View {
ZStack {
VStack {
Text("profile views")
}
VStack {
Spacer()
HStack {
Spacer()
TabItem(image: "pencil.circle", label: "Edit")
Spacer()
TabItem(image: "minus.circle", label: "Delete")
Spacer()
}
}
}
.navigationBarTitle(Text(name), displayMode: .inline)
}
}
What I could do is to have a #State var title in the root view and pass the binding to all the list views, then have those list views to set their title back to root view on appear. But I just don't feel so right about it, is there any better way of doing this? Thanks for any help.
The idea is to join TabView selection with NavigationView content dynamically.
Demo:
Here is simplified code depicting approach (with using your views). The NavigationView and TabView just position independently in ZStack, but content of NavigationView depends on the selection of TabView (which content is just stub), thus they don't bother each other. Also in such case it becomes possible to hide/unhide TabView depending on some condition - in this case, for simplicity, presence of root list view.
struct TestTabsOverNavigation: View {
#State private var tabVisible = true
#State private var selectedTab: Int = 0
var body: some View {
ZStack(alignment: .bottom) {
contentView
tabBar
}
}
var contentView: some View {
NavigationView {
ListView(gender: selectedTab == 0 ? .female : .male)
.onAppear {
withAnimation {
self.tabVisible = true
}
}
.onDisappear {
withAnimation {
self.tabVisible = false
}
}
}
}
var tabBar: some View {
TabView(selection: $selectedTab) {
Rectangle().fill(Color.clear).tag(0).tabItem {
TabItem(image: "person.crop.circle", label: Gender.female.rawValue)
}
Rectangle().fill(Color.clear).tag(1).tabItem {
TabItem(image: "person.crop.circle.fill", label: Gender.male.rawValue)
}
}
.frame(height: 50) // << !! might be platform dependent
.opacity(tabVisible ? 1.0 : 0.0)
}
}
This maybe a late answer, but the TabView items need to be assigned tag number else binding selection parameter won't happen. Here is how I do the same thing on my project:
#State private var selectedTab:Int = 0
private var pageTitles = ["Home", "Customers","Sales", "More"]
var body: some View {
NavigationView{
TabView(selection: $selectedTab, content:{
HomeView()
.tabItem {
Image(systemName: "house.fill")
Text(pageTitles[0])
}.tag(0)
CustomerListView()
.tabItem {
Image(systemName: "rectangle.stack.person.crop.fill")
Text(pageTitles[1])
}.tag(1)
SaleView()
.tabItem {
Image(systemName: "tag.fill")
Text(pageTitles[2])
}.tag(2)
MoreView()
.tabItem {
Image(systemName: "ellipsis.circle.fill")
Text(pageTitles[3])
}.tag(3)
})
.navigationBarTitle(Text(pageTitles[selectedTab]),displayMode:.inline)
.font(.headline)
}
}

How can you switch views without having a navigationView or an popover?

I am trying to change a view without having something over it like when you used segue in swift. But the only solution I came up with is to have a navigation bar navigationBar or a popover.
struct view1: View {
var body: some View{
Button(action: {
// go to view2``
}) {
Text("press")
}
}
}
struct view2: View {
var body: some View{
Text("yeay")
}
}
If you just want to hide the navigation bar it's easy:
import SwiftUI
struct View2: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("POP")
}
}
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
}
}
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: View2()) {
Text("PUSH")
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you, instead, want to get rid of the NavigationView and NavigationLink views you have to implement your own custom navigation. It's a little more complicated. The following is just a simple example of a push/pop transition between two views.
import SwiftUI
struct View2: View {
#Binding var push: Bool
var body: some View {
ZStack {
Color.yellow
Button(action: {
withAnimation(.easeOut(duration: 0.3)) {
self.push.toggle()
}
}) {
Text("POP")
}
}
.edgesIgnoringSafeArea(.all)
}
}
struct View1: View {
#Binding var push: Bool
var body: some View {
ZStack {
Color.green
Button(action: {
withAnimation(.easeOut(duration: 0.3)) {
self.push.toggle()
}
}) {
Text("PUSH")
}
}
.edgesIgnoringSafeArea(.all)
}
}
struct ContentView: View {
#State private var push = false
var body: some View {
ZStack {
if !push {
View1(push: $push)
.transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .leading)))
}
if push {
View2(push: $push)
.transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .trailing)))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Anyone coming to this later might find this to be of interest; in short, shove a hunk of data into #environment, tickle that with a button push in whatever view you want, and since it's at the very top of the overall application stack, it forces a redraw, which acts like a full view navigation, without the potential lost memory and orphaned or lost objects of the whole push/pop navigation view silliness.
It's still a little more "single page app"-ey than I'd like, but since SwiftUI is so crippled in its navigation thoroughness, it'll do nicely.
Not my site, not my link, not my tutorial, and it's buried way down in the list of hits when searching, which is a shame; this is the closest to what many are looking for. IMO, this should be baked into SwiftUI as a first class operation, and made less workaround-ey.
https://blckbirds.com/post/how-to-navigate-between-views-in-swiftui-by-using-an-environmentobject/
You can also do this completely without NavigationView. Take a look at the following example:
struct MainView: View
{
#State private var showView = "LoginView"
var body: some View
{
switch showView
{
case "LoginView":
Text("Please login.")
Button("Login")
{
showView = "NormalView"
}
case "NormalView":
Text("This is youre NormalView!")
Button("Next view")
{
showView = "NextView"
}
case "NextView":
Text("This is the NextView")
Button("Back")
{
showView = "NormalView"
}
default:
Text("Default") // you should never reach this
}
}
}
Perhaps not the best code practice, but it solves your problem.
I think this not best way but it's easy
struct ContentView: View {
#State var gotoDetail3:Bool = false
var body: some View {
NavigationView{
ZStack{
VStack {
// Normal NavigationLink
NavigationLink {
Text("Detail......")
} label: {
Text("goto..")
}
//use for change state
Button {
gotoDetail3.toggle()
} label: {
Text("goto33333")
}
}// End VStack
// Hide Navigation Link
NavigationLink(
LocalizedStringKey("123"), destination: Text("Subsequent View"),
isActive: $gotoDetail3)
.hidden()
}
}
}
}

SwiftUI ContextMenu navigation to another view

I am trying to get a context menu to navigate to another view using the following code
var body: some View
{
VStack
{
Text(self.event.name).font(.body)
...
Spacer()
NavigationLink(destination: EditView(event: self.event))
{
Image(systemName: "pencil")
}
}
.navigationBarTitle(Text(appName))
.contextMenu
{
NavigationLink(destination: EditView(event: self.event))
{
Image(systemName: "pencil")
}
}
}
The NavigationLink within the VStack works as expected and navigates to the edit view but I want to use a contextMenu. Although the context menu displays the image, when I tap on it it doesn't navigate to the edit view, instead it just cancels the context menu.
I am doing this within a watch app but don't think that should make a difference, is there anything special I have to do with context menu navigation?
I would use the isActive variant of NavigationLink that you can trigger by setting a state variable. Apple documents this here
This variant of NavigationLink is well fit for dynamic/programatic navigation.
Your .contextMenu sets the state variable to true and that activates the NavigationLink. Because you don't want the link to be visible, set the label view to EmptyView
Here's an example, not identical to your post but hopefully makes it clear.
struct ContentView: View {
#State private var showEditView = false
var body: some View {
NavigationView {
VStack {
Text("Long Press Me")
.contextMenu {
Button(action: {
self.showEditView = true
}, label: {
HStack {
Text("Edit")
Image(systemName: "pencil")
}
})
}
NavigationLink(destination: Text("Edit Mode View Here"), isActive: $showEditView) {
EmptyView()
}
}
.navigationBarTitle("Context Menu")
}
}
}
In Xcode 11.4 it's now possible to do this with sensible NavigationLink buttons. Yay! 🎉
.contextMenu {
NavigationLink(destination: VisitEditView(visit: visit)) {
Text("Edit visit")
Image(systemName: "square.and.pencil")
}
NavigationLink(destination: SegmentsEditView(timelineItem: visit)) {
Text("Edit individual segments")
Image(systemName: "ellipsis")
}
}
This works on Xcode 11.6
struct ContentView: View {
#State var isActiveFromContextMenu = false
var body: some View {
NavigationView{
VStack{
NavigationLink(destination : detailTwo(), isActive: $isActiveFromContextMenu ){
EmptyView()
}
List{
NavigationLink(destination: detail() ){
row(isActiveFromContextMenu: $isActiveFromContextMenu)
}
NavigationLink(destination: detail() ){
row(isActiveFromContextMenu: $isActiveFromContextMenu)
}
NavigationLink(destination: detail() ){
row(isActiveFromContextMenu: $isActiveFromContextMenu)
}
}
}
}
}
}
struct detail: View {
var body: some View{
Text("Detail view")
}
}
struct detailTwo: View {
var body: some View{
Text("DetailTwo view")
}
}
struct row: View {
#Binding var isActiveFromContextMenu : Bool
var body: some View {
HStack{
Text("item")
}.contextMenu{
Button(action: {
self.isActiveFromContextMenu = true
})
{
Text("navigate to")
}
}
}
}
I found success in masking the NavigationLink in the background and switching the context with a Button as the shortest yet simplest alternative.
struct ContentView: View {
#State private var isShowing = false
var body: some View {
NavigationView {
Text("Hello")
.background(NavigationLink("", destination: Text("World!"), isActive: $isShowing))
.contextMenu {
Button {
isShowing = true
} label: {
Label("Switch to New View", systemImage: "chevron.forward")
}
}
}
}
}