I try to switch from one screen to another by pressing a button (see full code below). The switch from the first view to the second view (and vice versa) works, but no animation is taking place.
Why does this behavior happen?
Full code:
struct ContentView: View {
#AppStorage("firstViewActive") var isFirstViewActive: Bool = true
var body: some View {
if isFirstViewActive {
FirstView()
} else {
SecondView()
}
}
}
struct FirstView: View {
#AppStorage("firstViewActive") var isFirstViewActive: Bool = true
var body: some View {
ZStack {
Color(.red).ignoresSafeArea(.all, edges: .all)
VStack {
Spacer()
Text("This is the first view")
Spacer()
Button {
withAnimation {
isFirstViewActive = false
}
} label: {
Text("Go to second view")
}
Spacer()
}
}
}
}
struct SecondView: View {
#AppStorage("firstViewActive") var isFirstViewActive: Bool = false
var body: some View {
ZStack {
Color(.blue).ignoresSafeArea(.all, edges: .all)
VStack {
Spacer()
Text("This is the second view")
Spacer()
Button {
withAnimation {
isFirstViewActive = true
}
} label: {
Text("Go to first view")
}
Spacer()
}
}
}
}
The problem is with
#AppStorage("firstViewActive") var isFirstViewActive: Bool = true
If you change that to
#State var isFirstViewActive: Bool = true
and use #Binding in subviews, you will get the default animations.
In iOS 16, there seems to be a problem with #AppStorage vars and animation. But you can refer to this workaround
I have a Mac Catalyst app I had built using SwiftUI,and I cant seem to add buttons to the trailing navigation bar?
I am also unsure where this navigationBar is defined, is it possible to remove? It only seems to have appeared in Ventura.
struct AppSidebarNavigation: View {
enum NavigationItem {
case home
}
#State private var selection: NavigationItem? = .home
init() {
#if !targetEnvironment(macCatalyst)
UITableView.appearance().backgroundColor = UIColor(named: "White")
UITableViewCell.appearance().selectionStyle = .none
UITableView.appearance().allowsSelection = false
#endif
}
var body: some View {
NavigationView {
sidebar
.navigationTitle("")
.navigationBarTitleDisplayMode(.inline)
// Main View
HomeView()
.navigationTitle("")
.edgesIgnoringSafeArea(.top)
.navigationBarHidden(isMac ? false : true)
.navigationBarBackButtonHidden(isMac ? false : true)
}
.accentColor(Color("Black"))
.navigationViewStyle(.columns)
}
}
HomeView I had the following to the View.
#if targetEnvironment(macCatalyst)
.navigationBarItems(trailing:
NavButtons
)
#endif
var NavButtons: some View {
HStack {
Button(action: {
Print("No")
}) {
Image(systemName: "plus")
.font(.system(size: 14, weight: .medium))
}
.buttonStyle(NavPlusButton())
}
}
I don't think it is possible to do that because:
#available(iOS, introduced: 13.0, deprecated: 100000.0, message: "Use toolbar(_:) with navigationBarLeading or navigationBarTrailing placement")
#available(tvOS, introduced: 13.0, deprecated: 100000.0, message: "Use toolbar(_:) with navigationBarLeading or navigationBarTrailing placement")
#available(macOS, unavailable)
#available(watchOS, unavailable)
public func navigationBarItems<T>(trailing: T) -> some View where T : View
This indicates on macOS the function in not available.
I slightly modified your code (to get it to compile) and saw that where it would be is at the red circle below:
My code is:
struct AppSidebarNavigation: View {
enum NavigationItem {
case home
}
#State private var selection: NavigationItem? = .home
var isMac: Bool
init() {
#if !targetEnvironment(macCatalyst)
UITableView.appearance().backgroundColor = UIColor(named: "White")
UITableViewCell.appearance().selectionStyle = .none
UITableView.appearance().allowsSelection = false
isMac = false
#else
isMac = true
#endif
}
var body: some View {
NavigationView {
// Main View
HomeView()
.navigationTitle("NavTitle")
.edgesIgnoringSafeArea(.top)
.navigationBarHidden(isMac ? false : true)
.navigationBarBackButtonHidden(isMac ? false : true)
.navigationBarItems(trailing: NavButtons)
}
.accentColor(Color("Black"))
.navigationViewStyle(.columns)
}
}
var NavButtons: some View {
HStack {
Button(action: {}) {
Image(systemName: "plus")
}
// Button(action: {
// print("No")
// }) {
// Image(systemName: "plus")
// .font(.system(size: 14, weight: .medium)).frame(width: 80)
// }
}
}
struct HomeView: View {
var body: some View {
Group {
Text("Home View")
NavButtons
}
}
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
struct NavButton_Previews: PreviewProvider {
static var previews: some View {
NavButtons
}
}
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
AppSidebarNavigation()
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can use Toolbar modifier with .primaryAction as placement to place button on the trailing side on Navigation Bar.
Just replace .navigationBarItem with below:
#if targetEnvironment(macCatalyst)
.toolbar {
ToolbarItem(placement: .primaryAction) {
NavButtons
}
}
#endif
Since iOS 16 there is a new feature for the ".sheet" modifier called ".presentationDetents". ".presentationDetents" has a parameter called "selection" where you can pass a Binding. You can programmatically resize the sheet with the "selection" parameter. As soon as you change the sheet size for example from PresentationDetent.medium to PresentationDetent.large right after changed the page with a "NavigationLink" the View below gets cut off:
But if I slightly move (resize) the sheet afterwards the cut off below is going to disappear:
The view hierarchy is also strange:
If you add a delay by 0.6s for resizing the sheet, the cut off won't happen.
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
currentSelection = .large
}
}
You can find the code below:
import SwiftUI
struct ContentView: View {
#State private var sheetIsOpened = false
#State private var currentSelection = PresentationDetent.medium
var body: some View {
Text("Click to open a sheet")
.padding()
.onTapGesture {
sheetIsOpened = true
}
.sheet(isPresented: $sheetIsOpened) {
NavigationStack {
List {
ScrollView {
ForEach(0..<100) { index in
VStack {
NavigationLink(destination: NavigatedView(currentSelection: $currentSelection)) {
Text("I have the index: \(index)")
.foregroundColor(.green)
}
}
.frame(maxWidth: .infinity)
}
}
.padding()
}
}
.presentationDetents([.medium, .large], selection: $currentSelection)
}
}
}
struct NavigatedView: View {
#Binding fileprivate var currentSelection: PresentationDetent
var body: some View {
ScrollView {
ForEach(0..<100) { index in
VStack {
Text("I'm a child and I have the index: \(index)")
.onAppear {
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
currentSelection = .large
// }
}
}
.frame(maxWidth: .infinity)
}
}
.background(.red)
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
So here's my problem:
I have a main view with a navigation view and then the list view.
The colors are just to distinguish different views.
At first everything works as intended but as soon as the app goes into the background (aka Home Screen) and then back to active again my list view is bugging out.
The whole list moves down roughly 3 times the height of one cell.
The main views background is blue so it's not the whole list which gets moved.
I tried to use PlainListStyle() and GroupedListStyle() but the header as well moves down.
If I use one of the links in the bottom blue bar (yeah I know, dumb choice of color and changed in the code snippet below) and then navigate back the list is back in it's original shape.
Edit:
Adding anything to the Stack above the list fixes the problem.
A text or a rectangle is all it takes.
As a workaround a rectangle with heigh 0.1 and the same color as the navigation bar is sufficient.
But any ideas to fix it permanently would be appreciated!
What it should look like:
What it looks like after the app has been in the background:
import SwiftUI
struct TestListView: View
{
#State var dummyList: [DummyCell] = []
#Environment(\.scenePhase) var scenePhase
#State private var listBlur : CGFloat = 0.0
var body: some View
{
List
{
ForEach(dummyList)
{ item in
item
.listRowBackground(Color.yellow)
}
.onDelete
{ indexSet in
dummyList.remove(atOffsets: indexSet)
}
.onMove
{ indices, newOffsets in
dummyList.move(fromOffsets: indices, toOffset: newOffsets)
}
}
.background
{
Color.green
}
.onAppear
{
if dummyList.isEmpty
{
let testData = testData()
testData.forEach
{
daten in
dummyList.append(daten)
}
}
}
.onDisappear { print("List OnDisappear") }
.blur(radius: listBlur)
.onChange(of: scenePhase)
{
newPhase in
if newPhase == .active
{
print("List Active")
dummyList.removeAll()
let list = testData()
list.forEach { cell in
dummyList.append(cell)
}
listBlur = 0
}
else if newPhase == .inactive
{
print("List Inactive")
listBlur = 10.0
}
}
}
private func testData() -> [DummyCell]
{
var list : [DummyCell] = []
for index in 0..<20
{
list.append(DummyCell(dummyNumber: index))
}
return list
}
}
struct TestListView_Previews: PreviewProvider {
static var previews: some View {
TestListView()
}
}
import SwiftUI
struct MainView: View {
#Environment(\.scenePhase) var scenePhase
var body: some View {
ZStack
{
NavigationView
{
VStack
{
// Main View
TestListView()
.navigationBarTitle("listTest", displayMode: .inline)
.navigationBarItems(leading: EditButton())
.navigationBarItems(trailing: EditButton())
// Footer
Footer()
}
.background
{
Color.green
}
}
.navigationBarColor(UIColor(Color.orange))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MainView()
}
}
struct Footer: View
{
var body: some View
{
HStack
{
NavigationLink(destination: Text("SomeView"), label: {
Text("SomeView")
})
.padding(.top)
.padding(.leading)
Spacer()
NavigationLink(destination: Text("SomeOtherView"), label: {
Text("SomeOtherView")
})
.padding(.top)
.padding(.trailing)
}
}
}
I have a view that can be shown either as a modal, or simply pushed onto a navigation stack. When it's pushed, it has the back button in the top left, and when it's shown as a modal, I want to add a close button (many of my testers were not easily able to figure out that they could slide down the modal and really expected an explicit close button).
Now, I have multiple problems.
How do I figure out if a View is shown modally or not? Or alternatively, if it's not the first view on a navigation stack? In UIKit there are multiple ways to easily do this. Adding a presentationMode #Environment variable doesn't help, because its isPresented value is also true for pushed screens. I could of course pass in a isModal variable myself but it seems weird that's the only way?
How do I conditionally add a leading navigationBarItem? The problem is that if you give nil, even the default back button is hidden.
Code to copy and paste into Xcode and play around with:
import SwiftUI
struct ContentView: View {
#State private var showModal = false
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("Open modally") {
self.showModal = true
}
NavigationLink("Push", destination: DetailView(isModal: false))
}
.navigationBarTitle("Home")
}
.sheet(isPresented: $showModal) {
NavigationView {
DetailView(isModal: true)
}
}
}
}
struct DetailView: View {
#Environment(\.presentationMode) private var presentationMode
let isModal: Bool
var body: some View {
Text("Hello World")
.navigationBarTitle(Text("Detail"), displayMode: .inline)
.navigationBarItems(leading: closeButton, trailing: deleteButton)
}
private var closeButton: some View {
Button(action: { self.presentationMode.wrappedValue.dismiss() }) {
Image(systemName: "xmark")
.frame(height: 36)
}
}
private var deleteButton: some View {
Button(action: { print("DELETE") }) {
Image(systemName: "trash")
.frame(height: 36)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If I change closeButton to return an optional AnyView? and then return nil when isModal is false, I don't get a back button at all. I also can't call navigationBarItems twice, once with a leading and once with a trailing button, because the latter call overrides the first call. I'm kinda stuck here.
Okay, I managed it. It's not pretty and I am very much open to different suggestions, but it works 😅
import SwiftUI
extension View {
func eraseToAnyView() -> AnyView {
AnyView(self)
}
public func conditionalNavigationBarItems(_ condition: Bool, leading: AnyView, trailing: AnyView) -> some View {
Group {
if condition {
self.navigationBarItems(leading: leading, trailing: trailing)
} else {
self
}
}
}
}
struct ContentView: View {
#State private var showModal = false
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("Open modally") {
self.showModal = true
}
NavigationLink("Push", destination: DetailView(isModal: false))
}
.navigationBarTitle("Home")
}
.sheet(isPresented: $showModal) {
NavigationView {
DetailView(isModal: true)
}
}
}
}
struct DetailView: View {
#Environment(\.presentationMode) private var presentationMode
let isModal: Bool
var body: some View {
Text("Hello World")
.navigationBarTitle(Text("Detail"), displayMode: .inline)
.navigationBarItems(trailing: deleteButton)
.conditionalNavigationBarItems(isModal, leading: closeButton, trailing: deleteButton)
}
private var closeButton: AnyView {
Button(action: { self.presentationMode.wrappedValue.dismiss() }) {
Image(systemName: "xmark")
.frame(height: 36)
}.eraseToAnyView()
}
private var deleteButton: AnyView {
Button(action: { print("DELETE") }) {
Image(systemName: "trash")
.frame(height: 36)
}.eraseToAnyView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I don't see any trouble, just add Dismiss button to your navigation bar. You only have to rearrange your View hierarchy and there is no need to pass any binding to your DetailView
import SwiftUI
struct DetailView: View {
var body: some View {
Text("Detail View")
}
}
struct ContentView: View {
#State var sheet = false
var body: some View {
NavigationView {
VStack(spacing: 20) {
Button("Open modally") {
self.sheet = true
}
NavigationLink("Push", destination: DetailView())
}.navigationBarTitle("Home")
}
.sheet(isPresented: $sheet) {
NavigationView {
DetailView().navigationBarTitle("Title").navigationBarItems(leading: Button(action: {
self.sheet.toggle()
}, label: {
Text("Dismiss")
}))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You still can dismiss it with swipe down, you can add some buttons (as part of DetailView declaration) ... etc.
When pushed, you have default back button, if shown modaly, you have dismiss
button indeed.
UPDATE (based od discussion)
.sheet(isPresented: $sheet) {
NavigationView {
GeometryReader { proxy in
DetailView().navigationBarTitle("Title")
.navigationBarItems(leading:
HStack {
Button(action: {
self.sheet.toggle()
}, label: {
Text("Dismiss").padding(.horizontal)
})
Color.clear
Button(action: {
}, label: {
Image(systemName: "trash")
.imageScale(.large)
.padding(.horizontal)
})
}.frame(width: proxy.size.width)
)
}
}
}
finally I suggest you to use
extension View {
#available(watchOS, unavailable)
public func navigationBarItems<L, T>(leading: L?, trailing: T) -> some View where L : View, T : View {
Group {
if leading != nil {
self.navigationBarItems(leading: leading!, trailing: trailing)
} else {
self.navigationBarItems(trailing: trailing)
}
}
}
}
Whenever we provide .navigationBarItems(leading: _anything_), ie anything, the standard back button has gone, so you have to provide your own back button conditionally.
The following approach works (tested with Xcode 11.2 / iOS 13.2)
.navigationBarItems(leading: Group {
if isModal {
closeButton
} else {
// custom back button here calling same dismiss
}
}, trailing: deleteButton)
Update: alternate approach might be as follows (tested in same)
var body: some View {
VStack {
if isModal {
Text("Hello")
.navigationBarItems(leading: closeButton, trailing: deleteButton)
} else {
Text("Hello")
.navigationBarItems(trailing: deleteButton)
}
}
.navigationBarTitle("Test", displayMode: .inline)
}