SwiftUI | Dynamic Navigation Bar Title - ios

Hello,
My app has a TabView inside the only navigation view in the whole app. To change the navigationTitle I use an extension that has a variable navigationBarTitle, that returnes different titles for every tab bar selection. Unfortunately, the whole "changing the title thing" stopped working a while back and I can't get it to update.
The switch inside is always called and returns correctly, but the navigation title doesn't change either way.
What am I doing wrong?
Here's the code
ContentView.swift
NavigationView {
TabView(selection: self.$viewModel.tabBarSelection) {
ListView(viewModel: self.toProcessViewModel)
.tabItem {
R.image.tabBar.toProcess
Text("tabbar.title.approval".localized)
}
.tag(0)
ListView(viewModel: self.myRequestsViewModel)
.tabItem {
R.image.tabBar.myRequests
Text("tabbar.title.myRequests".localized)
}
.tag(1)
PasswordView()
.tabItem {
R.image.tabBar.password
Text("tabbar.title.password".localized)
}
.tag(2)
.commonRedimBackground()
SettingsView(viewModel: self.settingsViewModel)
.tabItem {
R.image.tabBar.settings
Text("tabbar.title.settings".localized)
}
.tag(3)
}
.navigationBarTitle(Text(navigationBarTitle), displayMode: .inline)
.navigationBarHidden(navigationBarHidden)
.navigationBarTitleDisplayMode(.inline)
.accentColor(R.color.blue)
}
ContentViewModel.swift
final class ContentViewModel: ObservableObject {
#Published var tabBarSelection: Int = 0
}
ContentView-Extended
private extension ContentView { // For navigation
var navigationBarTitle: String {
switch viewModel.tabBarSelection {
case 0:
return "tabbar.title.approval".localized
case 1:
return "tabbar.title.myRequests".localized
case 2:
return "" //"tabbar.title.password".localized - UIKit View - remove after switching to SwiftUI
case 3:
return "tabbar.title.settings".localized
default:
return "SELECTION_ERROR"
}
}
var navigationBarHidden: Bool {
switch self.viewModel.tabBarSelection {
case 2:
return true
default:
return false
}
}
}
Edit after Yrb's comment - Unfortunately still not working
struct ContentView: View {
#ObservedObject var viewModel: ContentViewModel
#ObservedObject var toProcessViewModel: ListViewModel = ListViewModel(listType: .toProcess)
#ObservedObject var myRequestsViewModel: ListViewModel = ListViewModel(listType: .myRequests)
#ObservedObject var settingsViewModel: SettingsViewModel = SettingsViewModel()
#State var navTitle: String = ""
var body: some View {
NavigationView {
TabView(selection: self.$viewModel.tabBarSelection) {
ListView(viewModel: self.toProcessViewModel)
.tabItem {
R.image.tabBar.toProcess
Text("tabbar.title.approval".localized)
}
.tag(0)
ListView(viewModel: self.myRequestsViewModel)
.tabItem {
R.image.tabBar.myRequests
Text("tabbar.title.myRequests".localized)
}
.tag(1)
PasswordView()
.tabItem {
R.image.tabBar.password
Text("tabbar.title.password".localized)
}
.tag(2)
.commonRedimBackground()
SettingsView(viewModel: self.settingsViewModel)
.tabItem {
R.image.tabBar.settings
Text("tabbar.title.settings".localized)
}
.tag(3)
}
.navigationBarTitle(Text(navTitle), displayMode: .inline)
.navigationBarHidden(navigationBarHidden)
.navigationBarTitleDisplayMode(.inline)
.accentColor(R.color.redimBlue)
}
.toast(isPresented: self.$viewModel.showingToast, text: self.viewModel.toastTitle, image: self.viewModel.toastImage, backgroundColor: self.viewModel.toastBackgroundColor)
.bottomSheet(isPresented: self.$viewModel.showingBottomSheet, view: self.viewModel.bottomSheetView)
.alert(isPresented: self.$viewModel.showingAlert) {
Alert(title: Text(self.viewModel.alertTitle), message: Text(self.viewModel.alertMessage), dismissButton: .default(Text("general.understand".localized)))
}
.onReceive(self.viewModel.$tabBarSelection) { tag in
switch tag {
case 0:
navTitle = "tabbar.title.approval".localized
case 1:
navTitle = "tabbar.title.myRequests".localized
case 2:
navTitle = "" //"tabbar.title.password".localized - UIKit View - remove after switching to SwiftUI
case 3:
navTitle = "tabbar.title.settings".localized
default:
navTitle = "SELECTION_ERROR"
}
}
}
}
Edit: The only thing that worked so far, but I'm not really happy with that solution
struct ContentView: View {
#ObservedObject var viewModel: ContentViewModel
#ObservedObject var toProcessViewModel: ListViewModel = ListViewModel(listType: .toProcess)
#ObservedObject var myRequestsViewModel: ListViewModel = ListViewModel(listType: .myRequests)
#ObservedObject var settingsViewModel: SettingsViewModel = SettingsViewModel()
#State var uuid: UUID = UUID()
var body: some View {
NavigationView {
TabView(selection: self.$viewModel.tabBarSelection) {
ListView(viewModel: self.toProcessViewModel)
.tabItem {
R.image.tabBar.toProcess
Text("tabbar.title.approval".localized)
}
.tag(0)
ListView(viewModel: self.myRequestsViewModel)
.tabItem {
R.image.tabBar.myRequests
Text("tabbar.title.myRequests".localized)
}
.tag(1)
PasswordView()
.tabItem {
R.image.tabBar.password
Text("tabbar.title.password".localized)
}
.tag(2)
.commonRedimBackground()
SettingsView(viewModel: self.settingsViewModel)
.tabItem {
R.image.tabBar.settings
Text("tabbar.title.settings".localized)
}
.tag(3)
}
.navigationBarTitle(Text(navigationBarTitle), displayMode: .inline)
.navigationBarHidden(navigationBarHidden)
.navigationBarTitleDisplayMode(.inline)
.accentColor(R.color.redimBlue)
}
.id(uuid)
.toast(isPresented: self.$viewModel.showingToast, text: self.viewModel.toastTitle, image: self.viewModel.toastImage, backgroundColor: self.viewModel.toastBackgroundColor)
.bottomSheet(isPresented: self.$viewModel.showingBottomSheet, view: self.viewModel.bottomSheetView)
.alert(isPresented: self.$viewModel.showingAlert) {
Alert(title: Text(self.viewModel.alertTitle), message: Text(self.viewModel.alertMessage), dismissButton: .default(Text("general.understand".localized)))
}
.onReceive(self.viewModel.$tabBarSelection) { _ in
self.uuid = UUID()
}
}
}
private extension ContentView { // For navigation
var navigationBarTitle: String {
switch viewModel.tabBarSelection {
case 0:
return "tabbar.title.approval".localized
case 1:
return "tabbar.title.myRequests".localized
case 2:
return "" //"tabbar.title.password".localized - UIKit View - remove after switching to SwiftUI
case 3:
return "tabbar.title.settings".localized
default:
return "SELECTION_ERROR"
}
}
var navigationBarHidden: Bool {
switch self.viewModel.tabBarSelection {
case 2:
return true
default:
return false
}
}
}
Minimal, Reproducible Example that strangely works
ContentView
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel: ContentViewModel
var body: some View {
NavigationView {
TabView(selection: self.$viewModel.tabBarSelection) {
Text("Tag0")
.tabItem {
Image(systemName: "xmark")
Text("tabbar.title.settings")
}
.tag(0)
Text("Tag1")
.tabItem {
Image(systemName: "xmark")
Text("tabbar.title.settings")
}
.tag(1)
Text("Tag2")
.tabItem {
Image(systemName: "xmark")
Text("tabbar.title.settings")
}
.tag(2)
Text("Tag3")
.tabItem {
Image(systemName: "xmark")
Text("tabbar.title.settings")
}
.tag(3)
}
.navigationBarTitle(Text(navigationBarTitle))
.navigationBarTitleDisplayMode(.inline)
}
}
}
ContentViewModel
final class ContentViewModel: ObservableObject {
#Published var tabBarSelection: Int = 0
}
ContentView-Extension
private extension ContentView { // For navigation
var navigationBarTitle: String {
switch viewModel.tabBarSelection {
case 0:
return "title.approval"
case 1:
return "title.myRequests"
case 2:
return "title.password"
case 3:
return "title.settings"
default:
return "SELECTION_ERROR"
}
}
}

Related

EmptyBody().onAppear not works when sheet present with item

When I present sheet with .sheet(isPresented... onAppear of EmptyView() triggered
but when I use .sheet(item... then onAppear doesn't trigger. I don't understand what mistake I am doing?
item:
enum ActiveSheet: Identifiable {
var id: String { UUID().uuidString }
case customA
case customB
}
Main View:
struct ContentView: View {
#State private var activeSheet: ActiveSheet?
var body: some View {
VStack {
Button(action: { activeSheet = .customA }) {
Text("View A")
}
Button(action: { activeSheet = .customB }) {
Text("View B")
}
}
.buttonStyle(.borderedProminent)
//If I use this .sheet(isPresented... then onAppear triggers, but not with item
.sheet(item: $activeSheet) { item in
switch item {
case .customA:
CustomViewA()
case .customB:
CustomViewB()
}
}
}
}
Empty Views:
struct CustomViewA: View {
var body: some View {
EmptyView()
.onAppear {
print("OnAppear")
}
}
}
struct CustomViewB: View {
var body: some View {
EmptyView()
.onAppear {
print("OnAppear")
}
}
}

Creating two sidebars in iPadOS application using SwiftUI

I have created one sidebar with NavigationView, which by default appends to the left of the landscape view of application. However I wanted to have another on the right side.
NavigationView {
List {
Label("Pencil", systemImage: "pencil")
Label("Paint", systemImage: "paintbrush.fill")
Label("Erase", systemImage: "quote.opening")
Label("Cutter", systemImage: "scissors")
Label("Eyedropper", systemImage: "eyedropper.halffull")
Label("Draw Line", systemImage: "line.diagonal")
}
.listStyle(SidebarListStyle())
}
Here's a simple working example made with SwiftUI:
struct ContentView: View {
var body: some View {
NavigationView{
TagView()
Text("Default second View")
Text("Default Third View")
}
}
}
struct TagView: View {
let tags = ["Apple", "Google"]
var body: some View {
List{
ForEach(tags, id: \.self) { name in
NavigationLink {
ProductView(tag: name)
} label: {
Text(name)
}
}
}
}
}
struct ProductView: View {
var tag: String
var products: [String] {
if tag == "Apple" {
return ["iPhone", "iPad", "MacBook"]
} else {
return ["resuable stuff"]
}
}
var body: some View {
List{
ForEach(products, id: \.self) { name in
NavigationLink {
DetailsView()
} label: {
Text(name)
}
}
}
}
}
struct DetailsView: View {
var body: some View {
Text("Detailed explanation about product")
}
}

Swiftui how to add a view above a tabview?

The requirement is to display a banner above the tabbar. So that the banner does not disappear when the tabs are changed? How can I achieve it?
I can think of starting points, to help you see ways to approach this, but I don't think either idea really meets your requirements. It will depend on the details of whether the banner is permanent, where its content comes from, etc.
The first idea:
struct BannerView : View {
var text : String
var body: some View {
VStack {
Spacer()
HStack {
Spacer()
Text(text)
Spacer()
}.background(Color.orange)
}
}
}
Then you can include this in a ZStack along with your tabs:
TabView {
ZStack {
Text("Tab 1")
BannerView("BANNER")
}.tabItem { Text("Home") }
ZStack {
Text("Tab 2")
BannerView("BANNER")
}.tabItem { Text("History") }
}
The second idea uses the BannerView from the first idea, but in a slightly cleaner way, still not great:
struct TabWrapperWithOptionalBanner<Content> : View where Content : View {
var showBanner : Bool
var content : Content
init(showBanner : Bool, #ViewBuilder content: () -> Content) {
self.showBanner = showBanner
self.content = content()
}
var body: some View {
ZStack {
content
if showBanner {
BannerView(text: "BANNER")
}
}
}
}
then your ContentView looks like this:
TabView {
TabWrapperWithOptionalBanner(showBanner: showBanner) {
Text("Tab 1")
}.tabItem { Text("Home") }
TabWrapperWithOptionalBanner(showBanner: showBanner) {
Text("Tab 2")
}.tabItem { Text("History") }
}
Update Try a pair of TabViews bound to the same State:
struct BanneredTabView: View {
#State private var selected = panels.one
var body: some View {
VStack {
TabView(selection: $selected) {
panels.one.label.tag(panels.one)
panels.two.label.tag(panels.two)
panels.three.label.tag(panels.three)
}
.tabViewStyle(PageTabViewStyle())
Text("Banner")
.frame(height: 40, alignment: .top)
TabView(selection: $selected) {
ForEach(panels.allCases) { panel in
Text("").tabItem {
panel.label
}
.tag(panel)
}
}
.frame(height: 30)
}
}
enum panels : Int, CaseIterable, Identifiable {
case one = 1
case two = 2
case three = 3
var label : some View {
switch self {
case .one:
return Label("Tab One", systemImage: "1.circle")
case .two:
return Label("Tab Two", systemImage: "2.square")
case .three:
return Label("Tab Three", systemImage: "asterisk.circle")
}
}
// so the enum can be identified when enumerated
var id : Int { self.rawValue }
}
}

Swift - Update List from different View

I have 2 Views in my Swift Project and when I click on the Button on the secondView, I want to update the List in the First View. I don't know how to do it! If I use a static variable in my MainView and then edit this variable from the secondView, it works, but it won't update. And if I don't use static and instead use #State, it would update, but I can't access it from my secondView.
Here is the Code below:
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
MainView()
.tabItem() {
VStack {
Image(systemName: "circle.fill")
Text("MainView")
}
}.tag(0)
UpdateOtherViewFromHere()
.tabItem() {
VStack {
Image(systemName: "circle.fill")
Text("SecondView")
}
}.tag(1)
}
}
}
struct MainView: View {
var arrayList: [CreateListItems] = []
init() {
let a = CreateListItems(name: "First Name!")
let b = CreateListItems(name: "Second Name!")
let c = CreateListItems(name: "Third Name!")
arrayList.append(a)
arrayList.append(b)
arrayList.append(c)
}
var body: some View {
return VStack {
ZStack {
NavigationView {
List {
ForEach(arrayList) { x in
Text("\(x.name)")
}
}.navigationBarTitle("Main View")
}
}
}
}
}
struct UpdateOtherViewFromHere: View {
func updateList() {
//Code that should remove "FirstName" from the List in MainView
}
var body: some View {
return VStack {
Button(action: {
updateList()
}) {
Image(systemName: "heart.slash")
.font(.largeTitle)
Text("Click Me!")
}
}
}
}
struct CreateListItems: Identifiable {
var id: UUID = UUID()
var name: String
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can share it using #State and #Binding if you put
struct ContentView: View {
#State var arrayList: [CreateListItems] = []
struct MainView: View {
#Binding var arrayList: [CreateListItems]
struct UpdateOtherViewFromHere: View {
#Binding var arrayList: [CreateListItems]
or you use the MVVM pattern and store the list in an ObservableObject and use #StateObject/#ObservedObject (source) and use #EnvironmentObject(connection) to share it between your Views.
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
class ParentViewModel: ObservableObject{
#Published var arrayList: [CreateListItems] = []
init(){
addSamples()
}
func addSamples() {
let a = CreateListItems(name: "First Name!")
let b = CreateListItems(name: "Second Name!")
let c = CreateListItems(name: "Third Name!")
arrayList.append(a)
arrayList.append(b)
arrayList.append(c)
}
func updateList() {
let a = CreateListItems(name: "\(arrayList.count + 1) Name!")
arrayList.append(a)
}
}
struct ParentView: View {
#StateObject var vm: ParentViewModel = ParentViewModel()
var body: some View {
TabView {
MainView().environmentObject(vm)
.tabItem() {
VStack {
Image(systemName: "circle.fill")
Text("MainView")
}
}.tag(0)
UpdateOtherViewFromHere().environmentObject(vm)
.tabItem() {
VStack {
Image(systemName: "circle.fill")
Text("SecondView")
}
}.tag(1)
}
}
}
struct MainView: View {
#EnvironmentObject var vm: ParentViewModel
var body: some View {
return VStack {
ZStack {
NavigationView {
List {
ForEach(vm.arrayList) { x in
Text(x.name)
}
}.navigationBarTitle("Main View")
}
}
}
}
}
struct UpdateOtherViewFromHere: View {
#EnvironmentObject var vm: ParentViewModel
var body: some View {
return VStack {
Button(action: {
vm.updateList()
}) {
Image(systemName: "heart.slash")
.font(.largeTitle)
Text("Click Me!")
}
}
}
}

SwiftUI - Present 3 different Views with different parameter

I need to present 3 different Views.
AddListView
ChangeColor
EditListView
They take different paramater. AddListView does not have parameter while ChangeColor and EditListView takes Color and NSManagedObject respectively. However for the sake of simplicity, EditListView's paramter is integer in this example.
I am using .fullScreenCover(item: <#T##Binding<Identifiable?>#>, content: <#T##(Identifiable) -> View#>) for presenting them.
.fullScreenCover(item: $presentedViewType) { type in
if type == .AddListView {
AddListView()
}
else if type == .EditListView {
if let index = selectedIndex {
EditListView(index: index)
}
}
else if type == .ChangeColor {
if let color = selectedColor {
ColorView(color: color)
}
}
}
selectedIndex and selectedColor is nil even though I initialize them before initializing presentedViewType. And hence, an EmptyView is presented.
This is the project.
enum PresentedViewType: Identifiable {
case AddListView
case ChangeColor
case EditListView
var id: Int {
return hashValue
}
}
struct ContentView: View {
#State var presentedViewType: PresentedViewType?
#State var selectedColor: Color?
#State var selectedIndex: Int?
var body: some View {
NavigationView {
List {
Section {
NavigationLink(destination: Text("All")) {
Text("All")
}
.background(Color.blue)
.contextMenu {
Button(action: {
selectedColor = .blue
presentedViewType = .ChangeColor
}) {
Label("Change Color", systemImage: "paintbrush.pointed.fill")
}
}
}
ForEach(0..<10) { index in
NavigationLink(destination: Text("Row Details \(index)")) {
Text("Row \(index)")
}
.contextMenu {
Button(action: {
selectedIndex = index
presentedViewType = .EditListView
}) {
Label("Edit", systemImage: "square.and.pencil")
}
}
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
presentedViewType = .AddListView
}) {
Label("Add", systemImage: "plus")
}
}
}
.fullScreenCover(item: $presentedViewType) { type in
if type == .AddListView {
AddListView()
}
else if type == .EditListView {
if let index = selectedIndex {
EditListView(index: index)
}
}
else if type == .ChangeColor {
if let color = selectedColor {
ColorView(color: color)
}
}
}
}
}
}
struct ColorView: View {
#Environment(\.presentationMode) var presentationMode
#State var color: Color
var body: some View {
NavigationView {
Text("Color View")
.background(color)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image(systemName: "xmark")
}
}
}
}
}
}
}
struct AddListView: View {
#Environment(\.presentationMode) var presentationMode
#State var text: String = ""
var body: some View {
NavigationView {
TextField("", text: $text)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image(systemName: "xmark")
}
}
}
}
}
}
}
struct EditListView: View {
#Environment(\.presentationMode) var presentationMode
#State var index: Int
var body: some View {
NavigationView {
Text("Row \(index)")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image(systemName: "xmark")
}
}
}
}
}
}
}
I have to mention that they do not have fixed value. They have different value depending on which row you need to edit.
How to pass selectedIndex and selectedColor to EditListView and ColorView respectively?
Update
EditListView takes only selectedIndex while ColorView takes only selectedColor
You need to have #Binding properties inside EditListView and ColorView
struct EditListView: View {
#Binding var selectedIndex: Int?
// rest of view implementation
}
struct ColorView: View {
#Binding var selectedIndex: Int?
// rest of view implementation
}
and then pass the binding in the initialisers
.fullScreenCover(item: $presentedViewType) { type in
if type == .AddListView {
AddListView()
} else if type == .EditListView {
EditListView(index: $selectedIndex)
} else if type == .ChangeColor {
ColorView(color: $selectedColor)
}
}

Resources