Navigation View -Notes Like Sidebar - Toggle button - ios

I am very new to iOS development and Swift UI. I am making an app for our company. I believe Apple Notes like approach is best. I got most working tanks to some Udemmy courses and a couple of weeks of intense Googling. But I can't figure out how to implement the toggle sidebar button. I am probably searching for something obvious but using the wrong terminology.
I am talking about this:
When I remove most of the code, I have a structure like this:
NavigationView {
List {
Section(header: RoomHeader()) {
ForEach(sections) { section in
NavigationLink(destination: ViewRoom(section: section)) {
RoomListItem(section: section)
}
}
}
}
.navigationTitle("Rooms")
.listStyle(InsetGroupedListStyle())
}
The ViewRoom class
import SwiftUI
struct ViewRoom: View {
var room: RoomModel
var body: some View {
ZStack {
ScrollView {
VStack {
controls
title
// ....
}
.padding()
}
bottomBar
}
.navigationTitle(room.name)
.navigationBarItems(
trailing: HStack {
// ...
}
)
}
var controls: some View {
HStack {
Spacer()
// Couldn't find the icon on SF Symbols but this is the toggle button
Button(action: {}, label: {
Image(systemName: "rectangle.portrait.arrowtriangle.2.outward")
})
}
.font(.system(size: 24))
.padding(.top, 15)
}
// ...
}
I'd appreciate it if you could let me know if how to implement this toggle feature.

You can use Zstack & animation & transition features of SwiftUI. I made a sample for you to dig more into it and explore more about above mentioned concepts.
struct ContentView: View {
#State private var showDetails = false
var body: some View {
ZStack {
if showDetails {
LeftView()
.transition(.move(edge: .leading))
.zIndex(1)
} else {
Button("Press to show details") {
withAnimation(.spring()) {
showDetails.toggle()
}
}
}
}
.onTapGesture {
withAnimation(.spring()) {
showDetails.toggle()
}
}
}
}
Below is leftView which will animated from left
struct LeftView: View {
var body: some View {
GeometryReader { geometry in
VStack {
Text("Hello, World!")
}
.frame(width: 200, height: geometry.size.height)
.background(Color.blue)
}
}
}

Related

SwiftUI LazyVStack with pinned views

Context: I need to pin a view in a ScrollView to the top of the screen when scrolling, so I use a LazyVStack with pinnedViews, set the view I need as Section. All good.
Issue: The ScrollView other views might change the content while the view is scrolled to the bottom, when that happens the screen removes all views and doesn't display them back unless I scroll to the top.
Question: Is there another way to pin a view to the top? (I tried to use List, but not exactly what I need) Or is possible to make a custom Stack with pinned views?
This is my answer for another post, but it seems like you both are having a similar problem. Download and import TrackableScrollView, and try out the below code. While scrolling, there is a pinned View() which is displayed at the top of the screen.
Link package: https://github.com/maxnatchanon/trackable-scroll-view
Code:
import SwiftUI
import SwiftUITrackableScrollView //Added
import Combine
struct GameTabView: View {
#State private var scrollViewContentOffset = CGFloat(0) //Added
#State var selectedTab: Int = 0
init() {
UITableView.appearance().sectionHeaderTopPadding = 0
}
var body: some View {
listView
.ignoresSafeArea()
}
var listView: some View {
ZStack { //Added
TrackableScrollView(.vertical, showIndicators: true, contentOffset: $scrollViewContentOffset) {
VStack {
Color.gray.frame(height: 400)
sectionView
}
}
if(scrollViewContentOffset > 400) {
VStack {
headerView
Spacer()
}
}
}
}
var sectionView: some View {
Section {
tabContentView
.transition(.scale) // FIXED
.background(Color.blue)
} header: {
headerView
}
}
private var headerView: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
Button {
withAnimation {
selectedTab = 0
}
} label: {
Text("AAAA")
.padding()
}
Button {
withAnimation {
selectedTab = 1
}
} label: {
Text("BBBB")
.padding()
}
Button {
withAnimation {
selectedTab = 2
}
} label: {
Text("BBBB")
.padding()
}
}
}
}
.background(Color.green)
}
#ViewBuilder private var tabContentView: some View {
switch selectedTab {
case 0:
DummyScreen(title: "FIRST", color: .red)
case 1:
DummyScreen(title: "SECOND", color: .green)
case 2:
DummyScreen(title: "THIRD", color: .blue)
default:
EmptyView()
}
}
}
struct DummyScreen: View {
let title: String
let color: Color
var body: some View {
VStack {
ForEach(0..<15, id: \.self) { index in
HStack {
Text("#\(index): title \(title)")
.foregroundColor(Color.black)
.font(.system(size: 30))
.padding(.vertical, 20)
Spacer()
}
.background(Color.yellow)
}
}
.background(color)
}
}
From personal experience, I have done this using a VStack.
VStack{
PinnedItems()
LazyVStack {
OtherItems()
}
}
Then Pinned items will not scroll and are permanently attached to the top. And the lazyVStack will still be able to scroll underneath.

SwiftUI how to show large navigation title without using a navigation view?

I'm using the below PullToRefreshHack from this SO Answer which works well, but the problem is when I wrap it inside a NavigationView (which I need to be able to show this view's large navigation title) I loose the functionality of the pull to refresh. I'm not sure why though? 🤔 How can I fix this, or remove the NavigationView but still show a large title at the top?
//Usage
var body: some View {
NavigationView {
ScrollView {
PullToRefreshHack(coordinateSpaceName: "pullToRefreshInTrendsView") {
print("user pulled to refresh")
generator.impactOccurred()
self.loadDataForTrendsView()
}
struct PullToRefreshHack: View {
var coordinateSpaceName: String
var onRefresh: ()->Void
#State var needRefresh: Bool = false
var body: some View {
GeometryReader { geo in
if (geo.frame(in: .named(coordinateSpaceName)).midY > 50) {
Spacer()
.onAppear {
needRefresh = true
}
} else if (geo.frame(in: .named(coordinateSpaceName)).maxY < 10) {
Spacer()
.onAppear {
if needRefresh {
needRefresh = false
onRefresh()
}
}
}
HStack {
Spacer()
if needRefresh {
ProgressView()
} else {
Text("")
}
Spacer()
}
.onAppear {
//print("PullToRefreshHack VIEW .onAppear is called")
}
}.padding(.top, -50)
}
}

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
}
}
}

Show different views from NavigationBarItem menu in SwiftUI

I am trying to show a different view based on the option chosen in the NavigationBar menu. I am getting stuck on the best way to do this.
First, based on my current approach (I think it is not right!), I get a message in Xcode debugger when I press the menu item:
SideMenu[16587:1131441] [UILog] Called -[UIContextMenuInteraction
updateVisibleMenuWithBlock:] while no context menu is visible. This
won't do anything.
How do I fix this?
Second, When I select an option from the menu, how do I reset the bool so that it does not get executed unless it is chosen from the menu again. Trying to reset as self.showNewView = false within the if condition gives a compiler error
Here is a full executable sample code I am trying to work with. Appreciate any help in resolving this. Thank you!
struct ContentView: View {
#State var showNewView = false
#State var showAddView = false
#State var showEditView = false
#State var showDeleteView = false
var body: some View {
NavigationView {
GeometryReader { g in
VStack {
if self.showAddView {
AddView()
}
if self.showNewView {
NewView()
}
if self.showEditView {
EditView()
}
if self.showDeleteView {
DeleteView()
}
}.frame(width: g.size.width, height: g.size.height)
}
.navigationTitle("Title")
.navigationBarItems(leading: {
Menu {
Button(action: {showNewView.toggle()}) {
Label("New", systemImage: "pencil")
}
Button(action: {showEditView.toggle()}) {
Label("Edit", systemImage: "square.and.pencil")
}
} label: {
Image(systemName: "ellipsis.circle")
}
}(), trailing: {
Menu {
Button(action: {showAddView.toggle()}) {
Label("Add", systemImage: "plus")
}
Button(action: {showDeleteView.toggle()}) {
Label("Delete", systemImage: "trash")
}
} label: {
Image(systemName: "plus")
}
}())
}
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct NewView: View {
var body: some View {
GeometryReader { g in
Text("This is New View")
}
.background(Color.red)
}
}
struct EditView: View {
var body: some View {
GeometryReader { g in
Text("This is Edit View")
}
.background(Color.green)
}
}
struct AddView: View {
var body: some View {
GeometryReader { g in
Text("This is Add View")
}
.background(Color.orange)
}
}
struct DeleteView: View {
var body: some View {
GeometryReader { g in
Text("This is Delete View")
}
.background(Color.purple)
}
}
Here is what I get when I select each of the menu items. I would like to be able to show only one menu item. Essentially dismiss the other one when a new menu item is selected
A possible solution is to use a dedicated enum for your current view (instead of four #State properties):
enum CurrentView {
case new, add, edit, delete
}
#State var currentView: CurrentView?
Note that you can also extract parts of code to computed properties.
Here is a full code:
enum CurrentView {
case new, add, edit, delete
}
struct ContentView: View {
#State var currentView: CurrentView?
var body: some View {
NavigationView {
GeometryReader { g in
content
.frame(width: g.size.width, height: g.size.height)
}
.navigationTitle("Title")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: leadingBarItems, trailing: trailingBarItems)
}
.navigationViewStyle(StackNavigationViewStyle())
}
#ViewBuilder
var content: some View {
if let currentView = currentView {
switch currentView {
case .add:
AddView()
case .new:
NewView()
case .edit:
EditView()
case .delete:
DeleteView()
}
}
}
var leadingBarItems: some View {
Menu {
Button(action: { currentView = .new }) {
Label("New", systemImage: "pencil")
}
Button(action: { currentView = .edit }) {
Label("Edit", systemImage: "square.and.pencil")
}
} label: {
Image(systemName: "ellipsis.circle")
}
}
var trailingBarItems: some View {
Menu {
Button(action: { currentView = .add }) {
Label("Add", systemImage: "plus")
}
Button(action: { currentView = .delete }) {
Label("Delete", systemImage: "trash")
}
} label: {
Image(systemName: "plus")
}
}
}

SwiftUI Hide TabView bar inside NavigationLink views

I have a TabView and separate NavigationView stacks for every Tab item. It works well but when I open any NavigationLink the TabView bar is still displayed. I'd like it to disappear whenever I click on any NavigationLink.
struct MainView: View {
#State private var tabSelection = 0
var body: some View {
TabView(selection: $tabSelection) {
FirstView()
.tabItem {
Text("1")
}
.tag(0)
SecondView()
.tabItem {
Text("2")
}
.tag(1)
}
}
}
struct FirstView: View {
var body: some View {
NavigationView {
NavigationLink(destination: FirstChildView()) { // How can I open FirstViewChild with the TabView bar hidden?
Text("Go to...")
}
.navigationBarTitle("FirstTitle", displayMode: .inline)
}
}
}
I found a solution to put a TabView inside a NavigationView, so then after I click on a NavigationLink the TabView bar is hidden. But this messes up NavigationBarTitles for Tab items.
struct MainView: View {
#State private var tabSelection = 0
var body: some View {
NavigationView {
TabView(selection: $tabSelection) {
...
}
}
}
}
struct FirstView: View {
var body: some View {
NavigationView {
NavigationLink(destination: FirstChildView()) {
Text("Go to...")
}
.navigationBarTitle("FirstTitle", displayMode: .inline) // This will not work now
}
}
}
With this solution the only way to have different NavigationTabBars per TabView item, is to use nested NavigationViews. Maybe there is a way to implement nested NavigationViews correctly? (As far as I know there should be only one NavigationView in Navigation hierarchy).
How can I hide TabView bar inside NavigationLink views correctly in SwiftUI?
I really enjoyed the solutions posted above, but I don't like the fact that the TabBar is not hiding according to the view transition.
In practice, when you swipe left to navigate back when using tabBar.isHidden, the result is not acceptable.
I decided to give up the native SwiftUI TabView and code my own.
The result is more beautiful in the UI:
Here is the code used to reach this result:
First, define some views:
struct FirstView: View {
var body: some View {
NavigationView {
VStack {
Text("First View")
.font(.headline)
}
.navigationTitle("First title")
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.background(Color.yellow)
}
}
}
struct SecondView: View {
var body: some View {
VStack {
NavigationLink(destination: ThirdView()) {
Text("Second View, tap to navigate")
.font(.headline)
}
}
.navigationTitle("Second title")
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.background(Color.orange)
}
}
struct ThirdView: View {
var body: some View {
VStack {
Text("Third View with tabBar hidden")
.font(.headline)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.background(Color.red.edgesIgnoringSafeArea(.bottom))
}
}
Then, create the TabBarView (which will be the root view used in your app):
struct TabBarView: View {
enum Tab: Int {
case first, second
}
#State private var selectedTab = Tab.first
var body: some View {
VStack(spacing: 0) {
ZStack {
if selectedTab == .first {
FirstView()
}
else if selectedTab == .second {
NavigationView {
VStack(spacing: 0) {
SecondView()
tabBarView
}
}
}
}
.animation(nil)
if selectedTab != .second {
tabBarView
}
}
}
var tabBarView: some View {
VStack(spacing: 0) {
Divider()
HStack(spacing: 20) {
tabBarItem(.first, title: "First", icon: "hare", selectedIcon: "hare.fill")
tabBarItem(.second, title: "Second", icon: "tortoise", selectedIcon: "tortoise.fill")
}
.padding(.top, 8)
}
.frame(height: 50)
.background(Color.white.edgesIgnoringSafeArea(.all))
}
func tabBarItem(_ tab: Tab, title: String, icon: String, selectedIcon: String) -> some View {
ZStack(alignment: .topTrailing) {
VStack(spacing: 3) {
VStack {
Image(systemName: (selectedTab == tab ? selectedIcon : icon))
.font(.system(size: 24))
.foregroundColor(selectedTab == tab ? .primary : .black)
}
.frame(width: 55, height: 28)
Text(title)
.font(.system(size: 11))
.foregroundColor(selectedTab == tab ? .primary : .black)
}
}
.frame(width: 65, height: 42)
.onTapGesture {
selectedTab = tab
}
}
}
This solution also allows a lot of customization in the TabBar.
You can add some notifications badges, for example.
If we talk about standard TabView, the possible workaround solution can be based on TabBarAccessor from my answer on Programmatically detect Tab Bar or TabView height in SwiftUI
Here is a required modification in tab item holding NavigationView. Tested with Xcode 11.4 / iOS 13.4
struct FirstTabView: View {
#State private var tabBar: UITabBar! = nil
var body: some View {
NavigationView {
NavigationLink(destination:
FirstChildView()
.onAppear { self.tabBar.isHidden = true } // !!
.onDisappear { self.tabBar.isHidden = false } // !!
) {
Text("Go to...")
}
.navigationBarTitle("FirstTitle", displayMode: .inline)
}
.background(TabBarAccessor { tabbar in // << here !!
self.tabBar = tabbar
})
}
}
Note: or course if FirstTabView should be reusable and can be instantiated standalone, then tabBar property inside should be made optional and handle ansbsent tabBar explicitly.
Thanks to another Asperi's answer I was able to find a solution which does not break animations and looks natural.
struct ContentView: View {
#State private var tabSelection = 1
var body: some View {
NavigationView {
TabView(selection: $tabSelection) {
FirstView()
.tabItem {
Text("1")
}
.tag(1)
SecondView()
.tabItem {
Text("2")
}
.tag(2)
}
// global, for all child views
.navigationBarTitle(Text(navigationBarTitle), displayMode: .inline)
.navigationBarHidden(navigationBarHidden)
.navigationBarItems(leading: navigationBarLeadingItems, trailing: navigationBarTrailingItems)
}
}
}
struct FirstView: View {
var body: some View {
NavigationLink(destination: Text("Some detail link")) {
Text("Go to...")
}
}
}
struct SecondView: View {
var body: some View {
Text("We are in the SecondView")
}
}
Compute navigationBarTitle and navigationBarItems dynamically:
private extension ContentView {
var navigationBarTitle: String {
tabSelection == 1 ? "FirstView" : "SecondView"
}
var navigationBarHidden: Bool {
tabSelection == 3
}
#ViewBuilder
var navigationBarLeadingItems: some View {
if tabSelection == 1 {
Text("+")
}
}
#ViewBuilder
var navigationBarTrailingItems: some View {
if tabSelection == 1 {
Text("-")
}
}
}
How about,
struct TabSelectionView: View {
#State private var currentTab: Tab = .Scan
private enum Tab: String {
case Scan, Validate, Settings
}
var body: some View {
TabView(selection: $currentTab){
ScanView()
.tabItem {
Label(Tab.Scan.rawValue, systemImage: "square.and.pencil")
}
.tag(Tab.Scan)
ValidateView()
.tabItem {
Label(Tab.Validate.rawValue, systemImage: "list.dash")
}
.tag(Tab.Validate)
SettingsView()
.tabItem {
Label(Tab.Settings.rawValue, systemImage: "list.dash")
}
.tag(Tab.Settings)
}
.navigationBarTitle(Text(currentTab.rawValue), displayMode: .inline)
}
}
I also faced this problem. I don't want to rewrite, but the solution is in my github. I wrote everything in detail there
https://github.com/BrotskyS/AdvancedNavigationWithTabView
P.S: I have no reputation to write comments. Hikeland's solution is not bad. But you do not save the State of the page. If you have a ScrollView, it will reset to zero every time when you change tab
Also you can create very similar custom navBar for views in TabView
struct CustomNavBarView<Content>: View where Content: View {
var title: String = ""
let content: Content
init(title: String, #ViewBuilder content: () -> Content) {
self.title = title
self.content = content()
}
var body: some View {
content
.safeAreaInset(edge: .top, content: {
HStack{
Spacer()
Text(title)
.fontWeight(.semibold)
Spacer()
}
.padding(.bottom, 10)
.frame(height: 40)
.frame(maxWidth: .infinity)
.background(.ultraThinMaterial)
.overlay {
Divider()
.frame(maxHeight: .infinity, alignment: .bottom)
}
})
}
}
CustomNavBarView(title: "Create ad"){
ZStack{
NavigationLink(destination: SetPinMapView(currentRegion: $vm.region, region: vm.region), isActive: $vm.showFullMap) {
Color.clear
}
Color("Background").ignoresSafeArea()
content
}
}

Resources