How to use NavigationLink outside of NavigationStack? - ios

I have a safe area inset view that is placed on my NavigationStack so that acts as a bottom bar across all pushed views. However, in the safe area inset view, there are button which I'd like to push views onto the stack. The NavigationLink is greyed out though since it is outside of a NavigationStack.
This is what my code looks like:
NavigationStack {
VStack {
Image(systemName: "globe")
Text("Hello, world!")
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
NavigationLink("Button 1") {
Text("Screen 1")
}
}
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink("Button 2") {
Text("Screen 2")
}
}
}
}
.safeAreaInset(edge: .bottom, spacing: 0) {
HStack {
NavigationLink("Button 1") {
Text("Screen 1")
}
.frame(minWidth: 0, maxWidth: .infinity)
NavigationLink("Button 2") {
Text("Screen 2")
}
.frame(minWidth: 0, maxWidth: .infinity)
}
.padding()
.background(.regularMaterial)
}
It behaves in a way where the bottom view does indeed stick across all pushed views, but the buttons there do not do anything (the ones in the top navigation bar work properly though):
How can I get the buttons in the bottom safe area inset to navigation to different screens while still keeping it on the NavigationStack level since I do not want to place this bottom overlay code on each sub view?

You can use programmatic navigation with NavigationStack:
The state var navigationPath holds the active path and you can change it either with NavigationLink(value:) or by setting it directly (like in the safe area buttons).
struct ContentView: View {
// holds the active navigation path
#State private var navigationPath: [Int] = []
var body: some View {
NavigationStack(path: $navigationPath ) {
VStack {
Image(systemName: "globe")
Text("Hello, world!")
}
// defines destinations of path
.navigationDestination(for: Int.self, destination: { value in
switch value {
case 1: Text("Screen 1")
case 2: Text("Screen 2")
default: Text("unknown view")
}
})
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
NavigationLink("Button 1", value: 1)
}
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink("Button 2", value: 2)
}
}
}
.safeAreaInset(edge: .bottom, spacing: 0) {
HStack {
Button("Button 1") {
navigationPath = [1]
// or if you want to extend the path:
// navigationPath.append(1)
}
.frame(maxWidth: .infinity)
Button("Button 2") {
navigationPath = [2]
// or if you want to extend the path:
// navigationPath.append(2)
}
.frame(maxWidth: .infinity)
}
.padding()
.background(.regularMaterial)
}
}
}

Related

SwiftUI - ExpandableView inside VStack

I'm trying to have multiple expandable views with animation inside a VStack. I have the following code:
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
VStack {
ExpandableView(headerTitle: "First")
ExpandableView(headerTitle: "Second")
Spacer()
}
}
}
}
}
And the ExpandableView:
struct ExpandableView: View {
let headerTitle: String
#State private var collapsed: Bool = true
var body: some View {
Button(
action: {
self.collapsed.toggle()
},
label: {
VStack(spacing: 2) {
ZStack {
Rectangle()
.fill(.gray)
VStack {
Text("\(headerTitle) Header")
if !collapsed {
HStack {
Text("Text A")
Text("Text B")
}
}
}
}
.frame(height: collapsed ? 52 : 80)
ZStack(alignment: .top) {
Rectangle()
.fill(.gray)
.frame(height: 204)
VStack {
Text("Content A")
Text("Content B")
Text("Content C")
}
}
.frame(maxHeight: collapsed ? 0 : .none)
.clipped()
}
}
)
.buttonStyle(PlainButtonStyle())
.animation(.easeOut, value: collapsed)
}
}
The result is this:
As you can see if I open the last expandableView is opens correctly. However if I open the first one when the second is closed, it actually opens the second. It only opens correctly the first one if the second is already open. It seems the VStack is not rendering correctly itself. Any ideas why this happening?
Thanks for the help.
I migth be the way the buttons works. Here is a cleaner solution:
struct ExpandableView: View {
let headerTitle: String
#State private var collapsed: Bool = true
var body: some View {
VStack(spacing: 2) {
Button(
action: {
withAnimation(.easeOut){
self.collapsed.toggle()
}
},
label: {
VStack {
Text("\(headerTitle) Header")
if !collapsed {
HStack {
Text("Text A")
Text("Text B")
}
}
}.frame(maxWidth: .infinity)
})
.buttonStyle(.borderedProminent)
.tint(.gray)
if(!self.collapsed) {
VStack {
Divider().background(.black)
Text("Content A")
Text("Content B")
Text("Content C")
}
}
Spacer()
}
.frame(height: collapsed ? 52 : 204)
.frame(maxWidth: .infinity)
.background(.gray)
.padding()
}
}

How to create sticky bottom in SwiftUI?

I am trying to create sticky footer in swiftUI where other part of screen is scrollable but in footer there is one view with buttons and other element which should be fixed.
Thank You for help.
If I understand correctly, what you want to do is stack vertically (VStack)
a Scrollview
another VStack (with the Toggle and the Button), aligned at the bottom :
VStack {
ScrollView {...} // 1
VStack { // 2
Toggle(...)
Button(...)
}
.frame(alignment: .bottom)
}
To take your example :
struct SwiftUIView: View {
#State private var checked: Bool = false
let text = String(repeating: "blabla ", count: 20)
var body: some View {
VStack {
ScrollView {
ForEach((1...100), id: \.self) {_ in
Text(text)
}
}
VStack {
Toggle(isOn: $checked, label: {
Text("I have read...")
})
Button("Enter") {
// action
}
.frame(maxWidth: .infinity)
.padding(.vertical)
.background(Color.red)
}
.padding()
.border(Color.black)
.frame(alignment: .bottom)
}
}
}

Implementing custom sidebar across all views - SwiftUI

OUTLINE
I have made a custom slimline sidebar that I am now implementing across the whole app. The sidebar consists of a main button that is always showing and when pressed it shows or hides the rest of the sidebar that consists of buttons navigating to other views.
I am currently implementing the sidebar across the app on each view by creating a ZStack like this:
struct MainView: View {
var body: some View {
ZStack(alignment: .topLeading) {
SideBarCustom()
Text("Hello, World!")
}
}
}
PROBLEM
I am planning on adding a GeometryReader so if the side bar is shown the rest of the content moves over. With this in mind, the way I am implementing the sidebar on every view feels clunky and a long winded way to add it. Is there a more simple/better method to add this to each view?
Sidebar Code:
struct SideBarCustom: View {
#State var isToggle = false
var names = ["Home", "Products", "Compare", "AR", "Search"]
var icons = ["house.fill", "printer.fill.and.paper.fill", "list.bullet.rectangle", "arkit", "magnifyingglass"]
var imgSize = 20
var body: some View {
GeometryReader { geo in
VStack {
Button(action: {
self.isToggle.toggle()
}, label: {
Image("hexagons")
.resizable()
.frame(width: 40, height: 40)
.padding(.bottom, 20)
})
if isToggle {
ZStack{
RoundedRectangle(cornerRadius: 5)
.foregroundColor(Color.red)
.frame(width: 70, height: geo.size.height)
VStack(alignment: .center, spacing: 60) {
ForEach(Array(zip(names, icons)), id: \.0) { item in
Button(action: {
// NAVIIGATE TO VIEW
}, label: {
VStack {
Image(systemName: item.1)
.resizable()
.frame(width: CGFloat(imgSize), height: CGFloat(imgSize))
Text(item.0)
}
})
}
}
}
}
}
}
}
}
I don't think there's necessarily a reason to use GeometryReader here. The following is an example that has a dynamic width sidebar (although you could set it to a fixed value) that slides in and out. The main content view resizes itself automatically, since it's in an HStack:
struct ContentView : View {
#State private var sidebarShown = false
var body: some View {
HStack {
if sidebarShown {
CustomSidebar(sidebarShown: $sidebarShown)
.frame(maxHeight: .infinity)
.border(Color.red)
.transition(sidebarShown ? .move(edge: .leading) : .move(edge: .trailing) )
}
ZStack(alignment: .topLeading) {
MainContentView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
if !sidebarShown {
Button(action: {
withAnimation {
sidebarShown.toggle()
}
}) {
Image(systemName: "info.circle")
}
}
}
}
}
}
struct CustomSidebar : View {
#Binding var sidebarShown : Bool
var body: some View {
VStack {
Button(action: {
withAnimation {
sidebarShown.toggle()
}
}) {
Image(systemName: "info.circle")
}
Spacer()
Text("Hi")
Text("There")
Text("World")
Spacer()
}
}
}
struct MainContentView: View {
var body: some View {
VStack {
Text("Main content")
}
}
}

SwiftUI - NavigationLink' content is not readable when contextMenu is presented

I have a coloured NavigationLink that has context-menu. Its content is not readable when the context-menu is presened. I have epxreminted using the context-menu on the immediate sub-view of the NavigationLink, but it is stil the same issue.
NavigationLink(destination: Text("View")) {
VStack(alignment: .leading) {
Text("Context Menu")
.font(.system(size: 24, weight: .bold))
}
.frame(minWidth: 0, maxWidth: .infinity, idealHeight: 70)
.foregroundColor(.white)
.padding()
.cornerRadius(3.0)
}
.background(Color.red)
.contextMenu {
Section {
Button(action: {
}) {
Label("Edit", systemImage: "square.and.pencil")
}
}
Section(header: Text("Secondary actions")) {
Button(action: {}) {
Label("Delete", systemImage: "trash")
}
}
}
NavigatoinLinks look like in its original state.
When the context-menu is presented. The problem is even worse If I use small sized text.
I have tested on ios 14.2 both on simulator and physical device.
Info
Hierarchy of views.
ScrollView {
LazyVStack {
ForEach(data) { item in
// NavigationLink
}
}
}
Update
This is a similar project that has the same issue.
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
LazyVStack {
ForEach(0..<10) { item in
NavigationLink(destination: Text("View")) {
VStack(alignment: .leading) {
Text("Context Menu")
.font(.system(size: 24, weight: .bold))
}
.frame(minWidth: 0, maxWidth: .infinity, idealHeight: 70)
.foregroundColor(.white)
.padding()
.cornerRadius(3.0)
}
.background(Color.red)
.contextMenu {
Section {
Button(action: {
}) {
Label("Edit", systemImage: "square.and.pencil")
}
}
Section(header: Text("Secondary actions")) {
Button(action: {}) {
Label("Delete", systemImage: "trash")
}
}
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I know it's been two years, but this still remains an issue with iOS 16 when you use LazyVStack. In contrast with List, SwiftUI generates a clear automatic preview. What's new with iOS 16 is that you can now define a custom preview, and SwiftUI will present that preview without the blur. If you choose to use LazyVStack for performance or another reason, this gives you an alternative, albeit with duplicate code.
From your example above, you would add:
NavigationLink(destination: Text("View")) {
// View such as your VStack containing Text
}
.contextMenu {
// Menu items
} preview: {
// View, again, but you might want to simplify or modify
}

SwiftUI List/Destination Navigation Bar Title Ghosting

I'm experiencing a strange visual bug with navigation bar titles. This swiftUI app has
a basic master/detail type view with a List view and a Detail view that is presented
with a NavigationLink. That Detail view is then a list of Texts displaying information
or a list of TextFields when the page is in the edit mode.
When in the Detail view with edit mode and I scroll to the bottom item in the list and
edit it then the navigationBarTitle on the first view has a ghost of the "Detail"
navigationBarTitle of the second view. If I don't scroll the ghosting does not appear.
By scrolling, I include the scroll so the keyboard does not hide the textfield.
If I change the Detail view navigationBarTitle to .inline the issues is a nearly
imperceptible flash with no permanent residual. Obviously, I will leave it in this mode
but I'd certainly prefer a standard iOS look and feel.
I've experimentally disabled every combination that I can think of - no joy.
The root List:
NavigationView {
List {
ForEach(myPhysicians, id: \.self) { mp in
NavigationLink(destination: PhysicianDetailView(physician: mp)) {
Text(mp.lastName ?? "no last name")
}
}
.onDelete { indexSet in
//standard core data delete process
}
}.navigationBarTitle("Physicians")
.navigationBarItems(
leading:
NavigationLink(destination: AddPhysicianView()) {
Image(systemName: "plus.circle.fill")
.font(.title)
},
trailing:
EditButton()
)//bar button items
}//nav
The Detail View:
var body: some View {
DispatchQueue.main.async {
self.localFirstName = self.physician.wrappedFirstName
self.localLastName = self.physician.wrappedLastName
//bunch more...
}
return ZStack(alignment: .topTrailing) {
List {
VStack { //group 1
VStack (alignment: .leading) {
Text("First Name:")
.font(.system(size: 14))
.font(.subheadline)
if !showEditView {
Text("\(physician.wrappedFirstName)")
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 5)
.textFieldStyle(RoundedBorderTextFieldStyle())
.font(.headline)
} else {
TextField("tf firstname", text: $localFirstName)
.modifier(TextFieldSetup())
}
}
VStack (alignment: .leading) {
Text("Last Name:")
.font(.system(size: 14))
.font(.subheadline)
if !showEditView {
Text("\(physician.wrappedLastName)")
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 5)
.textFieldStyle(RoundedBorderTextFieldStyle())
.font(.headline)
} else {
TextField("tf lastname", text: $localLastName)
.modifier(TextFieldSetup())
}
}
//bunch more...
}//outer vstack
}//list
.frame(maxWidth: .infinity - 20)
//.navigationBarTitle("Detail",displayMode: .inline)
.navigationBarTitle("Detail")
.navigationBarBackButtonHidden(true)
.navigationBarItems(
leading:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
if self.showEditView {
Text("Cancel")
} else {
Image(systemName: "arrowshape.turn.up.left.circle.fill")
.font(.title)
}
},
trailing:
Button(action: {
if self.showEditView {
self.saveEditedPhysicianToCoreData(physician: self.physician)
}
self.showEditView.toggle()
}) {
Image(systemName: self.showEditView ? "gear" : "square.and.pencil")
.font(.title)
.frame(width: 60, height: 60)
}
)//nav bar items
}//zstack
.modifier(AdaptsToSoftwareKeyboard())
}//body
Any guidance would be appreciated. Xcode 11.6 iOS 13.6
Added: 29 July 2020
struct AdaptsToSoftwareKeyboard: ViewModifier {
#State var currentHeight: CGFloat = 0
func body(content: Content) -> some View {
content
.padding(.bottom, self.currentHeight)
.edgesIgnoringSafeArea(self.currentHeight == 0 ? Edge.Set() : .bottom)
.onAppear(perform: subscribeToKeyboardEvents)
}
private let keyboardWillOpen = NotificationCenter.default
.publisher(for: UIResponder.keyboardWillShowNotification)
.map { $0.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! CGRect }
.map { $0.height }
private let keyboardWillHide = NotificationCenter.default
.publisher(for: UIResponder.keyboardWillHideNotification)
.map { _ in CGFloat.zero }
private func subscribeToKeyboardEvents() {
_ = Publishers.Merge(keyboardWillOpen, keyboardWillHide)
.subscribe(on: RunLoop.main)
.assign(to: \.self.currentHeight, on: self)
}
}

Resources