SwiftUI: Changing Light/Dark Mode in Sheet Does Not Refresh Sheet UI - swiftui

I want to have a system/light/dark mode picker in a sheet, along with other settings.
Selecting a different scheme does correctly change the UI in the ContentView (behind the sheet), but the sheet itself retains the old scheme and must be dismissed and opened again.
What am I doing wrong?
Example:
import SwiftUI
struct ContentView: View {
#EnvironmentObject var viewManager: ViewManager
#State var showSettingsSheet = false
var body: some View {
NavigationView {
List {
Text("Some content 1")
Text("Some content 2")
}
.toolbar {
ToolbarItem {
Button(action: {
showSettingsSheet.toggle()
}){
Image(systemName: "gearshape.fill")
}
.sheet(isPresented: $showSettingsSheet){SettingsSheet()}
}
}
}
.preferredColorScheme(viewManager.colorScheme == "Light" ? .light : viewManager.colorScheme == "Dark" ? .dark : nil)
}
}
struct SettingsSheet: View {
#Environment (\.presentationMode) var presentationMode
#EnvironmentObject var viewManager: ViewManager
var body: some View {
NavigationView{
GroupBox {
Picker("Display Mode", selection: $viewManager.colorScheme) {
Text("System").tag("System")
Text("Light").tag("Light")
Text("Dark").tag("Dark")
}
.pickerStyle(.segmented)
}
.toolbar {
ToolbarItem {
Button(action: {
presentationMode.wrappedValue.dismiss()
}){
Text("Done")
}
}
}
}
}
}
class ViewManager: ObservableObject {
#AppStorage("colorScheme") var colorScheme: String = "System"
}

You have to provide colorScheme to sheet manually as it is outside your view hierarchy, i.e.:
struct ContentView: View {
#EnvironmentObject var viewManager: ViewManager
#State var showSettingsSheet = false
var body: some View {
NavigationView {
List {
Text("Some content 1")
Text("Some content 2")
}
.toolbar {
ToolbarItem {
Button(action: {
showSettingsSheet.toggle()
}){
Image(systemName: "gearshape.fill")
}
.sheet(isPresented: $showSettingsSheet){
SettingsSheet()
.preferredColorScheme(viewManager.colorScheme == "Light" ? .light : viewManager.colorScheme == "Dark" ? .dark : nil) // HERE
}
}
}
}
.preferredColorScheme(viewManager.colorScheme == "Light" ? .light : viewManager.colorScheme == "Dark" ? .dark : nil)
}
}

Related

How to hide TabView navigating from tabItem in childview in SwiftUI?

I am using TabView in swiftui. I want navigate child view from tabview tabItem. When I navigate from taview to childview, it shows tabview in bottom. Here is the image.. that click login goes to Myview page. But in Myview, Tabview is not hiding
Here is my code..
Tabview
struct WelcomeView: View {
var body: some View {
TabView {
HomeView()
.tabItem {
Image("home_icon")
Text("Home")
}
.tag(0)
NotificationView()
.tabItem {
Image("notification_icon")
Text("Notification")
}.tag(1)
AccountView()
.tabItem {
Image("account_icon")
Text("Account")
}.tag(2)
SettingView()
.tabItem {
Image("settings_icon")
Text("Setting")
}.tag(3)
}
}
}
In SettingView tabItem:
struct SettingView: View {
#State private var isActive = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
VStack {
Button(action: {
isActive = true
}) {
Text("Login")
}
NavigationLink("", destination: MyView(), isActive: $isActive)
}
}
}
}
}
MyView:
struct MyView: View {
var body: some View {
ZStack{
Text("Hello My View")
}.navigationBarHidden(true)
.navigationBarTitleDisplayMode(.inline)
}
}
When I click login Button in SettingView tabItem, It goes to MyView page. But in MyView page tabview is not hide.
How to hide tabview from MyView page?
To hide the tab we can add a Bool that will take care of showing the view or not. Then by using #Binding we can pass it to the other child views, whatever changes you make down the chain will affect all the views.
struct WelcomeView: View {
#State var isTabViewShown = true
var body: some View {
TabView {
if isTabViewShown {
HomeView()
.tabItem {
Image(systemName: "house.fill")
Text("Home")
}
.tag(0)
NotificationView()
.tabItem {
Image(systemName: "envelope.open.fill")
Text("Notification")
}.tag(1)
AccountView()
.tabItem {
Image(systemName: "person.crop.circle")
Text("Account")
}.tag(2)
SettingView(isTabViewShown: $isTabViewShown)
.tabItem {
Image(systemName: "gearshape")
Text("Setting")
}.tag(3)
}
}
}
}
struct MyView: View {
#Binding var isTabViewShown: Bool
var body: some View {
ZStack{
Text("Hello My View")
}
.navigationBarHidden(false)
.navigationBarTitleDisplayMode(.inline)
.onAppear {
isTabViewShown = false
}
}
}
struct SettingView: View {
#State private var isActive = false
#Binding var isTabViewShown: Bool
var body: some View {
NavigationView {
VStack(alignment: .leading) {
VStack {
Button(action: {
isActive = true
}) {
Text("Login")
}
NavigationLink("", destination: MyView(isTabViewShown: $isTabViewShown), isActive: $isActive)
//Here is a button if you want to show it again
Button(action: {
isTabViewShown.toggle()
//You can also use this is you don't want to use a toggle
// isTabViewShown = true
}) {
Text("Show again")
}
}
}
}
}
}
struct HomeView: View {
var body: some View {
Text("Home View")
}
}
struct NotificationView: View {
var body: some View {
Text("Notification View")
}
}
struct AccountView: View {
var body: some View {
Text("Account View")
}
}

Swiftui NavigationLink not executing

I'm trying to but a NavigationLink in a LazyVGrid and I'm having a bit of trouble with the navigation. I would like that when the color is clicked that I could segue to the next page. Here is some code:
Here is how the start page looks here
Here is how the image card is set up:
ZStack{
Color(colorData.image)
.cornerRadius(15)
.aspectRatio(contentMode: .fit)
.padding(8)
.matchedGeometryEffect(id: colorData.image, in: animation)
}
Here is how the LazyVgrid is set up:
ScrollView(.vertical, showsIndicators: false, content: {
VStack{
LazyVGrid(columns: Array(repeating: GridItem(.flexible(),spacing: 15), count: 2),spacing: 15){
ForEach(bags){color in
ColorView(colorData: color,animation: animation)
.onTapGesture {
withAnimation(.easeIn){
print("pressed")
selectedColor = color
}
}
}
}
.padding()
.padding(.top,10)
}
})
}
Here is how I navigate:
if selectedColor != nil {
NavigationLink(destination: DetailView()) {}
}
You can do as mentioned in #workingdog's answer, or slightly modify the code like so:
struct ContentView: View {
#State var isActive: Bool = false
#State var selectedColor: Color?
var body: some View {
NavigationView {
NavigationLink(destination: DetailsVieww(selectedColor: selectedColor), isActive: $isActive) {
EmptyView()
}
VStack {
ColorView(colorData: color, animation: animation)
.onTapGesture {
selectedColor = color
isActive = true
}
}
}
}
}
typically with navigation you would do something like this:
struct ContentView: View {
#State var selection: Int?
#State var selectedColor: Color?
var body: some View {
NavigationView { // <--- required somewhere in the hierarchy of views
// ....
NavigationLink(destination: DetailsVieww(selectedColor: selectedColor),
tag: 1,
selection: $selection) {
ColorView(colorData: color, animation: animation)
.onTapGesture {
withAnimation(.easeIn) {
selectedColor = color
selection = 1 // <--- will trigger just the tag=1 NavigationLink
}
}
}
// ....
}
}
}
struct DetailsVieww: View {
#State var selectedColor: Color?
var body: some View {
Text("selected color view \(selectedColor?.description ?? "")")
}
}

Disable item in TabView SwiftUI

How Can I set an item to disabled (not clickable) but visible in my tabView ?
TabView(selection: $selectedTab) {
Settings()
.tabItem {
Image(systemName: "gearshape.fill")
Text("Settings")
}.tag(1)
.disabled(true) // Not Working
I just create a way to do what you want fully supported and customisable!
test with Xcode Version 12.1, iOS 14.1, Here goes:
import SwiftUI
struct ContentView: View {
#State private var selection = 0
#State private var exSelection = 0
private var disableThis = 2
var body: some View
{
TabView(selection: $selection)
{
viewFinder(selectedIndex: selection == disableThis ? $exSelection : $selection)
.tabItem { Image(systemName: "1.circle") }
.tag(0)
viewFinder(selectedIndex: selection == disableThis ? $exSelection : $selection)
.tabItem { Image(systemName: "2.circle") }
.tag(1)
viewFinder(selectedIndex: selection == disableThis ? $exSelection : $selection)
.tabItem { Image(systemName: "3.circle") }
.tag(2)
viewFinder(selectedIndex: selection == disableThis ? $exSelection : $selection)
.tabItem { Image(systemName: "4.circle") }
.tag(3)
}
.onAppear()
{
UITabBar.appearance().barTintColor = .white
}
.accentColor(selection == disableThis ? Color.gray : Color.red)
.onChange(of: selection) { _ in
if selection != disableThis { exSelection = selection } else { selection = exSelection }
}
}
}
struct viewFinder: View
{
#Binding var selectedIndex: Int
var body: some View {
return Group
{
if selectedIndex == 0
{
FirstView()
}
else if selectedIndex == 1
{
SecondView()
}
else if selectedIndex == 2
{
ThirdView()
}
else if selectedIndex == 3
{
FourthView()
}
else
{
EmptyView()
}
}
}
}
struct FirstView: View { var body: some View {Text("FirstView")}}
struct SecondView: View { var body: some View {Text("SecondView")}}
struct ThirdView: View { var body: some View {Text("ThirdView")}}
struct FourthView: View { var body: some View {Text("FourthView")}}
There is not direct SwiftUI instrument for this now (SwiftUI 2.0), so find below possible approach based on TabBarAccessor from my another answer https://stackoverflow.com/a/59972635/12299030.
Tested with Xcode 12.1 / iOS 14.1 (note - tint color changed just for demo because disabled item is grey and invisible on grey tabbar)
struct TestTabBar: View {
init() {
UITabBar.appearance().unselectedItemTintColor = UIColor.green
}
#State private var selection = 0
var body: some View {
TabView(selection: $selection) {
Text("First View")
.background(TabBarAccessor { tabBar in
tabBar.items?.last?.isEnabled = false // << here !!
})
.tabItem { Image(systemName: "1.circle") }
.tag(0)
Text("Second View")
.tabItem { Image(systemName: "2.circle") }
.tag(1)
}
}
}

SwiftUI, setting title to child views of TabView inside of NavigationView does not work

Why I am putting TabView into a NavigationView is because I need to hide the bottom tab bar when user goes into 2nd level 'detail' views which have their own bottom action bar.
But doing this leads to another issue: all the 1st level 'list' views hosted by TabView no longer display their titles. Below is a sample code:
import SwiftUI
enum Gender: String {
case female, male
}
let members: [Gender: [String]] = [
Gender.female: ["Emma", "Olivia", "Ava"], Gender.male: ["Liam", "Noah", "William"]
]
struct TabItem: View {
let image: String
let label: String
var body: some View {
VStack {
Image(systemName: image).imageScale(.large)
Text(label)
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
TabView {
ListView(gender: .female).tag(0).tabItem {
TabItem(image: "person.crop.circle", label: Gender.female.rawValue)
}
ListView(gender: .male).tag(1).tabItem {
TabItem(image: "person.crop.circle.fill", label: Gender.male.rawValue)
}
}
}
}
}
struct ListView: View {
let gender: Gender
var body: some View {
let names = members[gender]!
return List {
ForEach(0..<names.count, id: \.self) { index in
NavigationLink(destination: DetailView(name: names[index])) {
Text(names[index])
}
}
}.navigationBarTitle(Text(gender.rawValue), displayMode: .inline)
}
}
struct DetailView: View {
let name: String
var body: some View {
ZStack {
VStack {
Text("profile views")
}
VStack {
Spacer()
HStack {
Spacer()
TabItem(image: "pencil.circle", label: "Edit")
Spacer()
TabItem(image: "minus.circle", label: "Delete")
Spacer()
}
}
}
.navigationBarTitle(Text(name), displayMode: .inline)
}
}
What I could do is to have a #State var title in the root view and pass the binding to all the list views, then have those list views to set their title back to root view on appear. But I just don't feel so right about it, is there any better way of doing this? Thanks for any help.
The idea is to join TabView selection with NavigationView content dynamically.
Demo:
Here is simplified code depicting approach (with using your views). The NavigationView and TabView just position independently in ZStack, but content of NavigationView depends on the selection of TabView (which content is just stub), thus they don't bother each other. Also in such case it becomes possible to hide/unhide TabView depending on some condition - in this case, for simplicity, presence of root list view.
struct TestTabsOverNavigation: View {
#State private var tabVisible = true
#State private var selectedTab: Int = 0
var body: some View {
ZStack(alignment: .bottom) {
contentView
tabBar
}
}
var contentView: some View {
NavigationView {
ListView(gender: selectedTab == 0 ? .female : .male)
.onAppear {
withAnimation {
self.tabVisible = true
}
}
.onDisappear {
withAnimation {
self.tabVisible = false
}
}
}
}
var tabBar: some View {
TabView(selection: $selectedTab) {
Rectangle().fill(Color.clear).tag(0).tabItem {
TabItem(image: "person.crop.circle", label: Gender.female.rawValue)
}
Rectangle().fill(Color.clear).tag(1).tabItem {
TabItem(image: "person.crop.circle.fill", label: Gender.male.rawValue)
}
}
.frame(height: 50) // << !! might be platform dependent
.opacity(tabVisible ? 1.0 : 0.0)
}
}
This maybe a late answer, but the TabView items need to be assigned tag number else binding selection parameter won't happen. Here is how I do the same thing on my project:
#State private var selectedTab:Int = 0
private var pageTitles = ["Home", "Customers","Sales", "More"]
var body: some View {
NavigationView{
TabView(selection: $selectedTab, content:{
HomeView()
.tabItem {
Image(systemName: "house.fill")
Text(pageTitles[0])
}.tag(0)
CustomerListView()
.tabItem {
Image(systemName: "rectangle.stack.person.crop.fill")
Text(pageTitles[1])
}.tag(1)
SaleView()
.tabItem {
Image(systemName: "tag.fill")
Text(pageTitles[2])
}.tag(2)
MoreView()
.tabItem {
Image(systemName: "ellipsis.circle.fill")
Text(pageTitles[3])
}.tag(3)
})
.navigationBarTitle(Text(pageTitles[selectedTab]),displayMode:.inline)
.font(.headline)
}
}

SwiftUI ContextMenu navigation to another view

I am trying to get a context menu to navigate to another view using the following code
var body: some View
{
VStack
{
Text(self.event.name).font(.body)
...
Spacer()
NavigationLink(destination: EditView(event: self.event))
{
Image(systemName: "pencil")
}
}
.navigationBarTitle(Text(appName))
.contextMenu
{
NavigationLink(destination: EditView(event: self.event))
{
Image(systemName: "pencil")
}
}
}
The NavigationLink within the VStack works as expected and navigates to the edit view but I want to use a contextMenu. Although the context menu displays the image, when I tap on it it doesn't navigate to the edit view, instead it just cancels the context menu.
I am doing this within a watch app but don't think that should make a difference, is there anything special I have to do with context menu navigation?
I would use the isActive variant of NavigationLink that you can trigger by setting a state variable. Apple documents this here
This variant of NavigationLink is well fit for dynamic/programatic navigation.
Your .contextMenu sets the state variable to true and that activates the NavigationLink. Because you don't want the link to be visible, set the label view to EmptyView
Here's an example, not identical to your post but hopefully makes it clear.
struct ContentView: View {
#State private var showEditView = false
var body: some View {
NavigationView {
VStack {
Text("Long Press Me")
.contextMenu {
Button(action: {
self.showEditView = true
}, label: {
HStack {
Text("Edit")
Image(systemName: "pencil")
}
})
}
NavigationLink(destination: Text("Edit Mode View Here"), isActive: $showEditView) {
EmptyView()
}
}
.navigationBarTitle("Context Menu")
}
}
}
In Xcode 11.4 it's now possible to do this with sensible NavigationLink buttons. Yay! 🎉
.contextMenu {
NavigationLink(destination: VisitEditView(visit: visit)) {
Text("Edit visit")
Image(systemName: "square.and.pencil")
}
NavigationLink(destination: SegmentsEditView(timelineItem: visit)) {
Text("Edit individual segments")
Image(systemName: "ellipsis")
}
}
This works on Xcode 11.6
struct ContentView: View {
#State var isActiveFromContextMenu = false
var body: some View {
NavigationView{
VStack{
NavigationLink(destination : detailTwo(), isActive: $isActiveFromContextMenu ){
EmptyView()
}
List{
NavigationLink(destination: detail() ){
row(isActiveFromContextMenu: $isActiveFromContextMenu)
}
NavigationLink(destination: detail() ){
row(isActiveFromContextMenu: $isActiveFromContextMenu)
}
NavigationLink(destination: detail() ){
row(isActiveFromContextMenu: $isActiveFromContextMenu)
}
}
}
}
}
}
struct detail: View {
var body: some View{
Text("Detail view")
}
}
struct detailTwo: View {
var body: some View{
Text("DetailTwo view")
}
}
struct row: View {
#Binding var isActiveFromContextMenu : Bool
var body: some View {
HStack{
Text("item")
}.contextMenu{
Button(action: {
self.isActiveFromContextMenu = true
})
{
Text("navigate to")
}
}
}
}
I found success in masking the NavigationLink in the background and switching the context with a Button as the shortest yet simplest alternative.
struct ContentView: View {
#State private var isShowing = false
var body: some View {
NavigationView {
Text("Hello")
.background(NavigationLink("", destination: Text("World!"), isActive: $isShowing))
.contextMenu {
Button {
isShowing = true
} label: {
Label("Switch to New View", systemImage: "chevron.forward")
}
}
}
}
}