SwiftUI: do NavigationView attributes needs to be copy-pasted in each View? - ios

I have a NavigationStack that I use to move between Views in my iOS app that has some modifications done on the toolbar (Color, colorScheme, toolbar items).
To keep the same appearance on all the different views I have to define again all the different attributes in each View I reach using NavigationLinks.
Is there a way to keep what I defined in the top NavigationView across all the different views?
Code snippet below:
First View where I define the NavigationStack
NavigationStack
{
VStack{
...
}
.navigationTitle("Lavorazioni")
.navigationBarTitleDisplayMode(NavigationBarItem.TitleDisplayMode.large)
.toolbarBackground(.blue, for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
.toolbarColorScheme(.dark, for: .navigationBar)
.toolbar {
Button {
mostraProfilo.toggle()
} label: {
Label("User Profile", systemImage: "person.crop.circle")
}
}
Second View reached by NavigationLink in VStack
var body: some View {
VStack{
...
}
.navigationTitle("Selezione tipologia")
.toolbarBackground(.blue, for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
.toolbarColorScheme(.dark, for: .navigationBar)
.toolbar {
Button {
mostraProfilo.toggle()
} label: {
Label("User Profile", systemImage: "person.crop.circle")
}
}
}
Is there a way to avoid copypasting every time the following?
.toolbarBackground(.blue, for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
.toolbarColorScheme(.dark, for: .navigationBar)
.toolbar {
Button {
mostraProfilo.toggle()
} label: {
Label("User Profile", systemImage: "person.crop.circle")
}
}

You can create a custom ViewModifier that holds all your common toolbar modifiers.
struct MyToolbar: ViewModifier{
//Use shared router
#EnvironmentObject var viewRouter: MyViewRouter
//Add unique title for each instance
let title: String
func body(content: Content) -> some View {
content
.navigationTitle(title)
.navigationBarTitleDisplayMode(.large)
.toolbarBackground(.blue, for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)
.toolbarColorScheme(.dark, for: .navigationBar)
.toolbar {
Button {
viewRouter.mostraProfilo.toggle()
} label: {
Label("User Profile", systemImage: "person.crop.circle")
}
}
}
}
You can share common navigation actions with the use of a ViewRouter that is accessible to all.
The portfolio Button is a good example.
///Router to share navigation
class MyViewRouter: ObservableObject{
//Shared portfolio variable
#Published var mostraProfilo: Bool = false
//You can also add things like a `NavigationPath` to make navigation easier.
enum ViewOptions: String, Hashable{
//Create a case for each view
case something
case somethingDifferent
//Set the title for a view
var title: String{
switch self{
case .something:
return "Something Title"
case .somethingDifferent:
return "different"
}
}
//List the views for each option
#ViewBuilder var view: some View {
switch self{
case .somethingDifferent:
VStack{
Text("something different")
}
//Add individually
.modifier(MyToolbar(title: "different"))
//View specific modifiers over the common toolbar for overridding
.navigationBarTitleDisplayMode(.inline)
.toolbarBackground(.red, for: .navigationBar)
case .something:
VStack{
Text("something")
}
//Add individually
.modifier(MyToolbar(title: "something"))
}
}
}
}
Your Views can then use these shared Options and modifiers.
struct SharedNavigationModifiers: View {
#StateObject var viewRouter: MyViewRouter = .init()
var body: some View {
NavigationStack{
VStack{
NavigationLink("go somewhere", value: MyViewRouter.ViewOptions.something)
NavigationLink("different", value: MyViewRouter.ViewOptions.somethingDifferent)
}
//Everone can display this portfolio using the Router
.navigationDestination(isPresented: $viewRouter.mostraProfilo) {
Text("Portfolio View")
//Add to each View
.modifier(MyToolbar(title: "portfolio"))
}
.navigationDestination(for: MyViewRouter.ViewOptions.self) { option in
option.view
//Add to all options for a more standard look or individually (See Options)
//.modifier(MyToolbar(title: option.title))
}
}
//Inject router so all `View`s in Stack have access
.environmentObject(viewRouter)
}
}

Related

SwiftUI Toolbar, Tabview & TabBarItem is not behaving as I would expect

When adopting NavigationStack with a TabView embedded, then using a Toolbar and ToolbarItems, I get unexpected behaviour when switching between tabs.
Expectation: I should be able to have different ToolbarItems depending on which tab is selected and when switching between tabs, the ToolbarItem in the position I specify per tab should change.
Behaviour: The ToolbarItems append to the specified position as you move across each tab.
#State var selectedIndex: Int
#State var path = NavigationPath()
var body: some View {
ZStack {
NavigationStack(path: $path) {
TabView(selection: $selectedIndex) {
Group {
TextView(tabName: .constant("Home"))
.tabItem {
Label("Home", systemImage: "house")
}
.tag(1)
TextView(tabName: .constant("Contacts"))
.tabItem {
Label("Contacts", systemImage: "person.3")
}
.tag(1)
TextView(tabName: .constant("Settings"))
.tabItem {
Label("Settings", systemImage: "gearshape")
}
.tag(1)
}
.toolbarBackground(Color.red)
.toolbarBackground(.visible, for: .navigationBar)
.toolbarColorScheme(.dark, for: .navigationBar)
}
}
}
}
}
And for the view for each tab (this is a simplified example, I have tried using seperate views but get the same behaviour);
#Binding var tabName: String
var body: some View {
Text("Tab \(tabName)")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
print("Profile ")
} label: {
Image(systemName: "person.circle")
}
}
}
}
}
Screenshot of Tab 2
I can't find any information in the developer docs on how to resolve this issue.

SwiftUI - Navigation View title is overlapping list

I have a simple list with a navigation view, where the navigation view is overlapping the list while scrolling.
Here is the look I want.
Here is what I am getting with the overlap
Here is the code
struct MedicalDashboard: View {
let menuItemData = MenuItemList()
var body: some View {
NavigationView {
List(menuItemData.items, id: \.id) { item in
MenuItemRow(menuItem: item)
}
.listStyle(.insetGrouped)
.navigationTitle("Dashboard")
.navigationBarItems(trailing:
Button(action: {
// TODO: - Pop up a sheet for the settings page.
print("User icon pressed...")
}) {
Image(systemName: "person.crop.circle").imageScale(.large)
}
)
.padding(.top)
}
}
}
when I add padding(.top) the overlap stops but I get a different color background on the navigation
On Xcode 13.4, except a missing }, without the .padding(.top) and with a custom List everything works like a charm for me.
The problem might come from MenuItemList().
I have still updated your code by replacing .navigationBarItems and by adding the sheet for you:
struct MedicalDashboard: View {
#State private var showSheet = false
var body: some View {
NavigationView {
List { // Custom list
Text("Hello")
Text("Salut")
}
.listStyle(.insetGrouped)
.navigationTitle("Dashboard")
.toolbar() { // .navigationBarItems will be deprecated
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showSheet.toggle()
print("User icon pressed.")
}, label: {
Image(systemName: "person.crop.circle")
})
.sheet(isPresented: $showSheet) { /* SettingsView() */ }
}
}
}
} // New
}
Edit your post and show us MenuItemList().
Try this:
Swift
struct MedicalDashboard: View {
init() {
if #available(iOS 15, *) {
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
UINavigationBar.appearance().standardAppearance = appearance
UINavigationBar.appearance().scrollEdgeAppearance = appearance
}
}
...
}

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

replace Tabbar with toolbar in SwiiftUI 2.0

I'm trying replicate the behavior of iOS Photos app.
Till now the thing I can't figure how could be done is the select mode, where when I press the button select how I can change the bottom bar?
Graphically, what I intend is, in this view:
When I pressed the button, the bottom bar changes to:
In the real project the views are embed inside a NavigationView
The code of the main view is similar to
struct ContentView: View {
var body: some View {
NavigationView{
TabView{
data()
.tabItem {
Text("Data")
}
data2()
.tabItem {
Text("Data")
}
}
}
}
I'm using Xcode 12 and swiftUI 2.0
First we need Conditional modifier like that https://stackoverflow.com/a/61253769/2715636
struct conditionalModifier: ViewModifier {
var isShowing: Bool
func body(content: Content) -> some View {
Group {
if self.isShowing {
content
.toolbar {
ToolbarItem(placement: .bottomBar, content: {
Button(action: {
}){
Image(systemName: "square.and.arrow.up")
}
})
}
.toolbar {
ToolbarItem(placement: .status, content: {
Text("Toolbar")
.fontWeight(.bold)
})
}
}
}
}}
I don't need else statement cause I only want to see Toolbar
else { content }
And here is my Tabbar inside ZStack. We're gonna overlay it with Text using Conditional modifier applied to Text
struct ContentView: View {
#State private var showToolbar: Bool = false
var body: some View {
Button(action: {
showToolbar.toggle()
}, label: {
Text(showToolbar ? "Show Tabbar" : "Show Toolbar")
}).padding()
ZStack {
TabView {
someView()
.tabItem {
Image(systemName: "placeholdertext.fill")
Text("Tab 1")
}
someView()
.tabItem {
Image(systemName: "placeholdertext.fill")
Text("Tab ")
}
someView()
.tabItem {
Image(systemName: "placeholdertext.fill")
Text("Tab 3")
}
}
Text("")
.modifier(conditionalModifier(isShowing: showToolbar))
}
}}
Final result
tabbar to toolbar
There's a new view modifier in iOS 16 that let you switch the tab bar and the bottom bar.
https://developer.apple.com/documentation/swiftui/view/toolbar(_:for:)
For example,
ContentView()
.toolbar(isSelecting ? .visible : .hidden, for: .bottomBar)
.toolbar(isSelecting ? .hidden : .visible, for: .tabBar)

SwiftUI list multiple custom row edit option

I need a multiple rows edit option for SwiftUI list
My login screen in NavigationView
After login Home screen is tab
bar
one of the tab is list view (I need edit option for this
list)
Here is my total code
first screen I have only simple navigation view with text fields and button.
struct HomeView: View {
#ObservedObject var viewModel = HomeViewModel()
//#State var title = "Home"
var body: some View {
TabView(selection: $viewModel.selectedView) {
TasksView()
.tabItem {
Image(“one”)
Text(“one”)
}.tag(0)
DashboardView()
.tabItem {
Image(“two”)
Text(“two”)
}.tag(1)
NotifcationsView()
.tabItem {
Image(“some”)
Text(“some”)
}.tag(2)
SettingsView()
.tabItem {
Image(“set”)
Text("Se")
}.tag(3)
}
.navigationBarBackButtonHidden(true)
.navigationBarItems(trailing: EditButton())
.navigationBarTitle(Text(viewModel.title) , displayMode: .inline)
}
}
struct TasksView: View {
#ObservedObject var viewModel = TViewModel()
#State var segmentSelection = 0
#State var selection = Set<String>()
// #State var editMode = EditMode.active
var body: some View {
VStack {
Picker(selection: $viewModel.segmentSelection, label: Text("")) {
ForEach(0..<self.viewModel.segments.count) {index in
Text(self.viewModel.segments[index]).tag(index)
}
}.pickerStyle(SegmentedPickerStyle())
.padding(5)
//MyTaskListView()
List (selection: $selection) {
ForEach(viewModel.mt){ t in
//TaskCell(t : task)
Text("Title")
}
.onDelete(perform: viewModel.delete)
}
}.onAppear{
self.viewModel.requestMYTasks()
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
}
init() {
UISegmentedControl.appearance().selectedSegmentTintColor = Colors.navBarColor
UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected)
UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: Colors.navBarColor], for: .normal)
}
}
after keeping simple row also , edit is not working
EditButton tracks edit mode automatically, so just remove your explicit state and all works (on replicated code, Xcode 11.4)
//#State var editMode = EditMode.active // remove this
List (selection: $selection) {
ForEach(viewModel.myTasks){ task in
TaskCell(task : task)
}
.onDelete(perform: viewModel.delete)
// .environment(\.editMode, self.$editMode) // ... and this
}
.navigationBarItems(trailing: EditButton())

Resources