Use same `navigationBarTitle` and `navigationBarItems` on two views which are shown based on condition - ios

I have a View which shows Text view to show help text for user to tap on plus icon to add group. Once group is added, it shows List view. To show navigation bar, I need to call navigationBarTitle and navigationBarItems on both Text and List view. Below is my code snippet.
import SwiftUI
struct Home:View {
#EnvironmentObject var dataStore:DataStore
var body: some View {
NavigationView {
if dataStore.groups.isEmpty {
Text("Tap on + icon to add group.")
.font(.caption)
.multilineTextAlignment(.center)
.padding()
.foregroundColor(.gray)
.navigationBarTitle(Text("My App Name"), displayMode: .automatic)
.navigationBarItems(
trailing:
NavigationLink(
destination:
CreateGroup(),
label: {
Image(systemName: "plus")
.foregroundColor(Color.blue)
})
)
} else {
List(dataStore.groups) { groupElement in
GroupRow(group: groupElement)
}
.navigationBarTitle(Text("My App Name"), displayMode: .automatic)
.navigationBarItems(
trailing:
NavigationLink(
destination:
CreateGroup(),
label: {
Image(systemName: "plus")
.foregroundColor(Color.blue)
})
)
}
}
}
}
Is there a way to call navigationBarTitle and navigationBarItems only once rather than calling on both Text and List views?

Is there a way to call navigationBarTitle and navigationBarItems only
once rather than calling on both Text and List views?
Yes, you can wrap condition into any container, like Group or xStack:
struct Home:View {
#EnvironmentObject var dataStore:DataStore
var body: some View {
NavigationView {
Group {
if dataStore.groups.isEmpty {
Text("Tap on + icon to add group.")
.font(.caption)
.multilineTextAlignment(.center)
.padding()
.foregroundColor(.gray)
} else {
List(dataStore.groups) { groupElement in
GroupRow(group: groupElement)
}
}
}
.navigationBarTitle(Text("My App Name"), displayMode: .automatic)
.navigationBarItems(
trailing:
NavigationLink(
destination:
CreateGroup(),
label: {
Image(systemName: "plus")
.foregroundColor(Color.blue)
})
)
}
}
}

Related

How to resize header inside NavigationLink view in SwiftUI

I am currently trying to resize the top of the view when a NavigationLink is opened. As you can see from my screenshot, it's somehow double and there is a line separating the header. Do you know how I could get rid of the part above the back arrow?
Screenshot View
This is my code
NavigationView{
List(presenter.sections, id: \.self) { section in
Section(header: Text(section.title)) {
ForEach(section.items, id: \.self) { item in
NavigationLink("\(item.selectModel.title) \(item.selectModel.selectedValue?.name ?? "")") {
List{
ForEach(selectModel.listSelection, id: \.self) { value in
Button(action: {selectModel.action(value)}) {
VStack(alignment: .leading) {
Text(value.name)
.foregroundColor(.black)
}
.contentShape(Rectangle())
.cornerRadius(12)
}
}
}
.offset(y: 50)
}
.navigationBarTitle(selectModel.title, displayMode: .inline)
.navigationBarHidden(true)
}
}
}
}
.navigationBarTitle("TEST", displayMode: .inline)
.navigationBarHidden(true)

Dismiss Tab View from a Child Navigation View and go back to RootView SwiftUI

My Navigation Flow:
Here, my View A to View G is under one Navigation View.
NavigationView {
ViewA()
}
And from View D & View G I am moving to my TabView H by modal, like this:
Button(action: {
isPresented.toggle()
}, label: {
Text("GO!")
})
.fullScreenCover(isPresented: $isPresented) {
TabbarView()
}
In my Tab View all the views have their own Navigation View, like this:
TabView(selection: $tabbarViewModel.tabSelection) {
NavigationView {
HomeView()
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
Text("Home")
}
}
}.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
Image(systemName: "house")
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
Text("Home")
}
.tag(0)
NavigationView {
CartView()
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
Text("Cart")
}
}
}.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
Image(systemName: "cart")
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
Text("Cart")
}
.tag(1)
NavigationView {
ProductView()
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
Text("Product")
}
}
}.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
Image(systemName: "seal")
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
Text("Product")
}
.tag(2)
NavigationView {
ProfileView()
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
Text("Profile")
}
}
}.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
Image(systemName: "person")
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
Text("Profile")
}
.tag(3)
}
.accentColor(Color("AppsDefaultColor"))
Now I want to go back to viewA, say from Home View by pressing the Sign Out button. I tried this, just to see if it takes me back to previous view, but it doesn't work.
struct HomeView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Dismiss")
})
}
}
So how can I dismiss the tabview and go back to my Root view A?
Finally I have managed to achieve this. To roll back to the Root view I used this:
NavigationLink(destination: <#T##_#>, tag: <#T##Hashable#>, selection: <#T##Binding<Hashable?>#>, label: <#T##() -> _#>)
And to dismiss the presented view, I used this:
UIApplication.shared.windows.first?.rootViewController?.dismiss(animated: true, completion: nil)
To be honest it's quite simple. All we need to make NavigationLink selection which is selectedItem in this case to nil & dismiss the modal throughout the project. All of these have done inside of a tab bar view model class with the help of #EnvironmentObject
First create the TabbarViewModel: ObservableObject:
import Foundation
import SwiftUI
class TabbarViewModel: ObservableObject {
#Published var tabSelection: Int = 0
#Published var selectedItem: String? = nil
func gotoRootView() {
withAnimation {
UIApplication.shared.windows.first?.rootViewController?.dismiss(animated: true, completion: nil)
selectedItem = nil
}
}
}
Now, let's create the ViewA which is AuthListView:
import SwiftUI
struct CellItem: Identifiable {
var id = UUID()
let title: String
let image: String
let destination: AnyView
}
struct AuthListView: View {
var body: some View {
AuthListContentView()
.navigationBarHidden(true)
}
}
struct AuthListContentView: View {
#State private var cellList: [CellItem] = [
CellItem(title: "Icon", image: "", destination: AnyView(EmptyView())),
CellItem(title: "Phone", image: "Phone", destination: AnyView(PhoneView())),
CellItem(title: "Email", image: "Email", destination: AnyView(SignInView())),
CellItem(title: "Google", image: "Google", destination: AnyView(GoogleView())),
CellItem(title: "Facebook", image: "Facebook", destination: AnyView(FacebookView())),
CellItem(title: "Twitter", image: "Twitter", destination: AnyView(TwitterView()))]
var body: some View {
List(cellList, id: \.id) { item in
if item.title == "Icon" {
IconImageView()
} else {
AuthListCell(cellItem: item)
}
}
}
}
struct IconImageView: View {
var body: some View {
VStack {
Image("firebase")
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 120, height: 120, alignment: .center)
}
.frame(maxWidth: .infinity, minHeight: 120, alignment: .center)
.padding(.top, 50)
.padding(.bottom, 50)
}
}
This is each cell of auth View:
import SwiftUI
struct AuthListCell: View {
var cellItem: CellItem
#EnvironmentObject var tabbarViewModel: TabbarViewModel
var body: some View {
NavigationLink(
destination: cellItem.destination,
tag: cellItem.title,
selection: $tabbarViewModel.selectedItem) {
cell(cellItem: cellItem)
}
}
}
struct cell: View {
var cellItem: CellItem
var body: some View {
HStack(spacing: 15) {
Image(cellItem.image)
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 28, maxHeight: .infinity, alignment: .center)
Text(cellItem.title)
.foregroundColor(Color("DefaultText"))
.font(.system(size: 17))
Spacer()
}
.padding(.top, 5)
.padding(.bottom, 5)
}
}
Load this view inside of your ContentView under a Navigation View:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
var body: some View {
NavigationView {
AuthListView()
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
Till now, we can push to ViewB from ViewA. Here, I am only showing the navigation flow for ViewA push> ViewB push> ViewC present> TabView > and then Dismiss TabView from HomeView and go back to root ViewA, cause rest of the other views will follow the same. It also works from a child navigation of any Tab bar views as well. So let's create ViewB(PhoneView) and push toViewC(PINView):
ViewB:
struct PhoneView: View {
var body: some View {
PhoneContentView()
.navigationBarTitle("Phone Number", displayMode: .inline)
}
}
struct PhoneContentView: View {
var body: some View {
NavigationLink(destination: PINView()) {
Text("Go")
}
}
}
ViewC:
struct PINView: View {
var body: some View {
PINContentView()
.navigationBarTitle("PIN", displayMode: .inline)
}
}
struct PINContentView: View {
#State private var isPresented = false
var body: some View {
Button(action: {
isPresented.toggle()
}, label: {
Text("Sign In")
})
.fullScreenCover(isPresented: $isPresented) {
TabbarView()
}
}
}
Till now we have presented the tab view from previous ViewC. This is our tab view:
import SwiftUI
struct TabbarView: View {
#EnvironmentObject var tabbarViewModel: TabbarViewModel
var body: some View {
TabView(selection: $tabbarViewModel.tabSelection) {
NavigationView {
HomeView().navigationBarTitle("Home", displayMode: .inline)
}
.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
Image(systemName: "house")
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
Text("Home")
}
.tag(0)
NavigationView {
CartView().navigationBarTitle("Cart", displayMode: .inline)
}
.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
Image(systemName: "cart")
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
Text("Cart")
}
.tag(1)
NavigationView {
ProductView().navigationBarTitle("Product", displayMode: .inline)
}
.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
Image("product")
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
Text("Product")
}
.tag(2)
NavigationView {
ProfileView().navigationBarTitle("Profile", displayMode: .inline)
}
.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
Image(systemName: "person")
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
Text("Profile")
}
.tag(3)
}
.accentColor(Color("AppsDefaultColor"))
}
}
Now, If I want to dismiss the presented tab view & go back to root view by pressing sign out button from my HomeView, all I have to do is call tabbarViewModel.gotoRootView() like this:
struct HomeView: View {
#EnvironmentObject var tabbarViewModel: TabbarViewModel
var body: some View {
Button(action: {
tabbarViewModel.gotoRootView()
}, label: {
Text("Sign Out")
})
}
}
I can dismiss the tab view and go to the root view from a child view of my HomeView as well. Let's go to a child view from the HomeView:
struct HomeView: View {
var body: some View {
NavigationLink(destination: HomeDetailsView()) {
Text("Go")
}
}
}
This is the HomedetailsView` and by following the same call I can accomplish the same result like this:
struct HomeDetailsView: View {
var body: some View {
HomeDetailsContentView()
.navigationBarTitle("Home Details", displayMode: .inline)
}
}
struct HomeDetailsContentView: View {
#EnvironmentObject var tabbarViewModel: TabbarViewModel
var body: some View {
Button(action: {
tabbarViewModel.gotoRootView()
}, label: {
Text("Dismiss")
})
}
}
By this you can dismiss tab view and go to the root view from any view of your project. :)

SwiftUI change view from first screen to tabview screen

I want to change views once the user taps 'get started' but due to having navigation view in my first view, it is showing back button on my next screen which I don't want. Please see the images attached below.
Code for the first view is below:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Spacer()
Text("LifePath")
.font(.system(size: 48, weight: .semibold))
.padding(.bottom)
Spacer()
NavigationLink(destination: ViewChanger()) {
Text("Get Started")
.font(.headline)
.navigationBarBackButtonHidden(true)
}
}
.padding()
}
}
}
back button showing on screen 2
First view
Change the location of your navigationBackButtonHidden modifier so that it actually modifies the view that you're going to (and not the NavigationLink label):
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Spacer()
Text("LifePath")
.font(.system(size: 48, weight: .semibold))
.padding(.bottom)
Spacer()
NavigationLink(destination: ViewChanger()
.navigationBarBackButtonHidden(true) // <-- Here
) {
Text("Get Started")
.font(.headline)
}
}
.padding()
}
}
}
If you want not only the back button to be gone, but the entire header bar, you can use the .navigationBarHidden(true) modifier.
Also, if you run this on iPad at all, you probably want .navigationViewStyle(StackNavigationViewStyle()) added to the outside of your NavigationView
If you use a NavigationLink (in a NavigationView), the view will be pushed. If you want to replace the view, you can do this with an if statement.
For example, this could be implemented like this:
struct ContentView: View {
#State var showSecondView: Bool = false
var body: some View {
if !showSecondView {
NavigationView {
VStack {
Spacer()
Text("LifePath")
.font(.system(size: 48, weight: .semibold))
.padding(.bottom)
Spacer()
Button(action: { showSecondView = true }) {
Text("Get Started")
.font(.headline)
}
}
.padding()
} else {
TabView {
// ...
}
}
}
}

SwiftUI: How do you dismiss a sheet and launch a different sheet from ContentView?

For my app, I have a welcome screen that intro's what the app does and allows the user to create their first item. When the user clicks the button I'd like to dismiss the 'welcomeScreen' sheet and and then launch the 'newRemindr' sheet.
I tried to achieve this by creating an observable object with an 'addNewTrigger' boolean set to false. When I click the Add New Reminder button on the welcomeScreen, the button's action causes the welcomeScreen to dismiss and toggles the 'addNewTrigger' boolean to True. (I've verified this is working with Print Statements). However content view is listening to that same observed object to launch the 'newRemindr' sheet but that action doesn't seem to be working.
Can somebody please take a look at the code and see where I am going wrong? Or suggest an alternative that can provide the type of functionality.
I really appreciate all the help. Thanks!
Code Below...
welcomeScreen:
import SwiftUI
import Combine
struct welcomeScreen: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
#ObservedObject var addNewReminder = showAddScreen()
var body: some View {
NavigationView {
ZStack (alignment: .center) {
LinearGradient(gradient: Gradient(colors: [Color.white, Color.white, Color.gray]), startPoint: .top, endPoint: .bottom)
.edgesIgnoringSafeArea(.all)
Image("Ellipse2")
.offset(y: -475)
VStack {
Spacer()
Text("Welcome to")
.foregroundColor(.white)
.fontWeight(.bold)
Image("RemindrLogoWhite")
Spacer()
Text("What is remindr?")
.font(.title)
.fontWeight(.bold)
.padding(.bottom, 25)
Text("Remindr is a simple app designed to help you schedule random reminders with the goal of clearing your mind.\n\nRemind yourself to check in with your body, set up positive affirmations, set your intentions; Whatever it is, the power is up to you.")
.padding(.horizontal, 25)
.padding(.bottom, 25)
Text("Click below to get started:")
.fontWeight(.bold)
// Add New Reminder Button
Button(action: {
self.mode.wrappedValue.dismiss()
print("Add Reminder Button from Welcome Screen is Tapped")
self.addNewReminder.addNewTrigger.toggle()
print("var addNewTrigger has been changed to \(self.addNewReminder.addNewTrigger)")
}) {
Image("addButton")
.renderingMode(.original)
}.padding(.bottom, 25)
Spacer()
} .frame(maxWidth: UIScreen.main.bounds.width,
maxHeight: UIScreen.main.bounds.height)
}
.navigationBarTitle(Text(""), displayMode: .automatic)
.navigationBarItems(trailing: Button(action: {
self.mode.wrappedValue.dismiss()
}, label: {
Image(systemName: "xmark")
.foregroundColor(.white)
}))
}
}
}
ContentView:
import SwiftUI
import CoreData
class showAddScreen: ObservableObject {
#Published var addNewTrigger = false
}
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: ReminderEntity.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \ReminderEntity.dateCreated, ascending: false)])
var reminder: FetchedResults<ReminderEntity>
// Sheet Control
#ObservedObject var addNewReminder = showAddScreen()
//#State private var showingAddScreen = false
#State var showWelcomeScreen = false
let emojiList = EmojiList()
//Toggle Control
#State var notifyOn = true
// Save Items Function
func saveItems() {
do {
try moc.save()
} catch {
print(error)
}
}
// Delete Item Function
func deleteItem(indexSet: IndexSet) {
let source = indexSet.first!
let listItem = reminder[source]
moc.delete(listItem)
}
// View Controller
var body: some View {
VStack {
NavigationView {
ZStack (alignment: .top) {
// List View
List {
ForEach(reminder, id: \.self) { notification in
NavigationLink(destination: editRemindr(reminder: notification,
notifyOn: notification.notifyOn,
emojiChoice: Int(notification.emojiChoice),
notification: notification.notification ?? "unknown",
notes: notification.notes ?? "unknown")) {
// Text within List View
HStack {
// MARK: TODO
//Toggle("NotifyOn", isOn: self.$notifyOn)
// .labelsHidden() // Hides the label/title
Text("\(self.emojiList.emojis[Int(notification.emojiChoice)]) \(notification.notification!)")
}
}
}
.onDelete(perform: deleteItem)
}.lineLimit(1)
// Navigation Items
.navigationBarTitle("", displayMode: .inline)
.navigationBarItems(
leading:
HStack {
Button(action: {
self.showWelcomeScreen.toggle()
}) {
Image(systemName: "info.circle.fill")
.font(.system(size: 24, weight: .regular))
}.foregroundColor(.gray)
// Positioning Remindr Logo on Navigation
Image("remindrLogoSmall")
.resizable()
.aspectRatio(contentMode: .fit)
//.frame(width: 60, height: 60, alignment: .center)
.padding(.leading, 83)
.padding(.top, -10)
},
// Global Settings Navigation Item
trailing: NavigationLink(destination: globalSettings()){
Image("settings")
.font(Font.title.weight(.ultraLight))
}.foregroundColor(.gray)
)
// Add New Reminder Button
VStack {
Spacer()
Button(action: { self.addNewReminder.addNewTrigger.toggle()
}) {
Image("addButton")
.renderingMode(.original)
}
.sheet(isPresented: $addNewReminder.addNewTrigger) {
newRemindr().environment(\.managedObjectContext, self.moc)
}
}
}
} .sheet(isPresented: $showWelcomeScreen) {
welcomeScreen()
}
}
}
}
First what I see is you use different observable objects in both views, but should use same, so changes made in one view be available for second view as well.
Se here is a way to solve this
struct welcomeScreen: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
#ObservedObject var addNewReminder: showAddScreen // << declare to be injected
// ... other code
and in ContentView
} .sheet(isPresented: $showWelcomeScreen) {
welcomeScreen(addNewReminder: self.addNewReminder) // << inject !!
}
Alternate: you can remove addNewReminder from welcomeScreen and work with it only in ContentView by activating on welcome sheet dismiss, like
} .sheet(isPresented: $showWelcomeScreen, onDismiss: {
// it is better to show second sheet with delay to give chance
// for first one to animate closing to the end
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.addNewReminder.addNewTrigger.toggle()
}
}
) {
welcomeScreen()
}

Navigation stuff in SwiftUI

I'm trying to figure out how to use the navigation bar in SwiftUI
I want to put BarButtonItem and images inside the NavigationBar
I have been able to display the navigation bar and put titles
var body: some View {
NavigationView{
List(0...5) { note in
VStack(alignment: .leading) {
Text("title")
Text("Date")
.font(.subheadline)
.foregroundColor(.secondary)
}
}
.navigationBarTitle(Text("Notes"))
}
}
iOS 14
You should use the toolbar modifier:
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") { /* action */ }
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { /* Actions */ }, label: {
HStack {
Image(systemName: "trash")
Text("Delete")
}
})
.foregroundColor(.red) // You can apply colors and other modifiers too
}
}
Note 1: You can have ANY View there. (not only a Button) and also any modifiers
Note 2: Both codes above and below will generate the same look items but with different approachs
iOS 13 and above (deprecated but still works)
You should use .navigationBarItems() modifier. For example you can add Button or Image like this:
.navigationBarItems(
leading: Button("Cancel") {
// Actions
},
trailing: Button(action: {
// Actions
}, label: { Label("Delete", systemImage: "trash") }
).foregroundColor(.red) // You can apply colors and other modifiers too
)
💡 Pro TIP
Always try to encapsulate each item in a separated struct, so your code will be simplified and very easy to replace with newer technologies. for example, take a look at this sample:
.navigationBarItems(
leading: MyCustomButtonItem(),
trailing: MyCustomButtonItem(text: "foo", image: "Bard")
)
.navigationBarItems() is the function you are looking for. You can specify a leading view, trailing view, or both. Within the view, you can specify horizontal and vertical stacks to add additional buttons.
var body: some View {
NavigationView{
List(0...5) { note in
VStack(alignment: .leading) {
Text("title")
Text("Date")
.font(.subheadline)
.foregroundColor(.secondary)
}
}
.navigationBarItems(leading: HStack {
Button(action: {}, label: {Image(systemName: "star.fill")})
Button(action: {}, label: {Text("Edit")})
}, trailing: VStack {
Button(action: {}, label: {Image(systemName: "star.fill")})
Button(action: {}, label: {Text("Edit")})
})
.navigationBarTitle(Text("Notes"))
}
}
SwiftUI 2
In SwiftUI 2 / iOS 14 the navigationBarItems modifier is deprecated.
Instead we should use a toolbar with ToolbarItems.
NavigationView {
List {
// ...
}
.navigationTitle("Notes")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Tap me") {
// action
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Image(systemName: "plus")
}
}
}
You can see the documentation for more ToolbarItemPlacements.
Check the image here
The toolbar() modifier lets us add single or multiple bar button items to the leading and trailing edge of a navigation view, as well as other parts of our view if needed. These might be tappable buttons, but there are no restrictions – you can add any sort of view.
var body: some View {
NavigationView {
ScrollView {
VStack{
}//: VStack
}//: Scroll
.navigationTitle("Settings")
.toolbar(content: {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "xmark")
}
}
})
.padding()
}//: Navigation
}
put this from parentViewController
NavigationLink(destination: NotesListView())

Resources