Invisible transition removal animation - swiftui

I've encountered an issue that I was not able to tinker to my full satisfaction.
I have a MasterView that changes environmentObject SelectionObject to show ZStack content* from Link enum. The issue is that the removal transition is almost invisible when there is a background in MasterView (Color.gray, when I set opacity, the animation is visible a little bit but unless it gets to low number, the overall opacity of FirstView or SecondView is detrimented. It works as expected without any background in MasterView
Here is my code:
class SelectionObject: ObservableObject {
#Published var selection: Link? = nil
}
struct MasterView: View {
#EnvironmentObject var selection: SelectionObject
var body: some View {
ZStack {
Color.gray
VStack {
ForEach(Link.allCases) { menu in
Button(action: {
selection.selection = menu
}, label: {
Label(menu.title, systemImage: menu.image).padding()
}
)
.tag(menu)
}
}
ForEach(Link.allCases) { menu in
if menu == selection.selection {
menu.contentView
.transition(AnyTransition.slide)
.animation(.spring())
}
}
}
}
}
struct Menu_Previews: PreviewProvider {
static var previews: some View {
MasterView().environmentObject(SelectionObject())
}
}
struct FirstView: View {
#EnvironmentObject var selection: SelectionObject
var body: some View {
ZStack {
Color.orange
VStack {
Text("First View content")
Button(action: {
selection.selection = nil
}, label: {
Text("Get back with a nice animation").padding().foregroundColor(.white)
}
)
}
}
}
}
struct SecondView: View {
#EnvironmentObject var selection: SelectionObject
var body: some View {
ZStack {
Color.orange
VStack {
Text("Second View content")
Button(action: {
selection.selection = nil
}, label: {
Text("Get back with a nice animation")
}
)
}
}
}
}
enum Link: Int, CaseIterable, Identifiable {
var id: Int {
return self.rawValue
}
case first
case second
var title: LocalizedStringKey {
switch self {
case .first: return "First"
case .second: return "Second"
}
}
var image: String {
switch self {
case .first: return "icloud"
case .second: return "display"
}
}
var contentView: AnyView {
switch self {
case .first: return AnyView ( FirstView() )
case .second: return AnyView ( SecondView() )
}
}
}
I've tried to use a zIndex way (mentioned here: Transition animation not working in SwiftUI ) but was unable to make it work as it worked only once and did not show the content on second click.
Can you help me find a way around the issue?
I use this because I can't use NavigationView as my MasterView is used in overlay in a different NavigationView and there is a frame, offset, and cornerRadius issue that prevents to click on anything unless I delete either the offset or cornerRadius.

Just add a zIndex to your menu.contentView and it will be always on top. Hence, you can see the back animation.
menu.contentView
.id(UUID()) // << add id here
.transition(AnyTransition.slide)
.animation(.spring())
.zIndex(50) //<< set higher zIndex here
Works multiple times aswell, after toggling view multiple times
Edit: Transition will fade in from leading edge and will dismiss to trailing edge. As the view stay there it will fade in back (once it is called again) from the trailing edge. With id(UUID() you create a new one which fades back from leading to trailing

Related

Non-deprecated way to call NavigationLink on Buttons

This is the old way of calling NavigationLink on Buttons
struct ContentView: View {
#State private var selection: String? = nil
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: View1(), tag: "tag1", selection: $selection) {
EmptyView()
}
NavigationLink(destination: NotView1(), tag: "tag2", selection: $selection) {
EmptyView()
}
Button("Do work then go to View1") {
// do some work that takes about 1 second
mySleepFunctionToSleepOneSecond()
selection = "tag1"
}
Button("Instantly go to NotView1") {
selection = "tag2"
}
}
.navigationTitle("Navigation")
}
}
}
This code works perfectly. It can go to different View targets depending on which button is clicked. Not only that, it guarantees all work is done BEFORE navigating to the target view. However, the only issue is that 'init(destination:tag:selection:label:)' was deprecated in iOS 16.0: use NavigationLink(value:label:) inside a List within a NavigationStack or NavigationSplitView
I get NavigationStack is awesome and such. But how can I translate the code to use the new NavigationStack + NavigationLink. Especially, how can I make sure work is done Before navigation?
Using new NavigationStack and its path property you can do much more. Your example will be transformed to
struct ContentView: View {
#State private var path = [String]()
var body: some View {
NavigationStack(path: $path) {
VStack {
Button("Do work then go to View1") {
// do some work that takes about 1 second
mySleepFunctionToSleepOneSecond()
path.append("tag1")
}
Button("Instantly go to NotView1") {
path.append("tag2")
}
}
.navigationTitle("Navigation")
.navigationDestination(for: String.self) { route in
switch route {
case "tag1":
EmptyView()
case "tag2":
EmptyView()
default:
EmptyView()
}
}
}
}
}
Check this video. There you can find more use cases.
For using non deprecated and after doing some work if we want to go to next view or in anyview there is something called ".navigationDestination". Let's see that using simple example.
#State var bool : Bool = false
var body: some View {
NavigationStack {
VStack {
Text("Hello, world!")
Button {
//Code here before changing the bool value
bool = true
} label: {
Text("Navigate Button")
}
}.navigationDestination(isPresented: $bool) {
SwiftUIView()
}
}
}
In this code we change take bool value as false and change it to true when our work is done using button.
.navigationDestination(isPresented: Binding<Bool>, destination: () -> View)
In .navigationDestination pass the Binding bool and provide the view you want to navigate.
You can use .navigationDestination multiple times.
Hope you found this useful.

With SwiftUI 3.0 .swipeActions in a ForEach how do you have an action go to another view while passing that view the input argument of the ForEach?

I am tying to add a .swipeAction to a ForEach list in which I want to pass the element in the list that was selected by the user to another invoked View. In other words when the user swipes on an item in the list, I want the user to be taken to a new View which has the contents of that item in the list so that it can display details from that item in that new view.
With that said, I have mocked up this simple example which I hope helps show the issue I am having.
import SwiftUI
struct ContentView: View {
var colors : [Color] = [Color.red, Color.green,
Color.blue, Color.yellow,
Color.brown, Color.cyan, Color.pink]
var body: some View {
NavigationView {
VStack {
List {
ForEach(colors, id: \.self) { color in
ColorRowView(color: color)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(action: { print("Hello From The First Button") },
label: {Label("Hello", systemImage: "face.smiling")})
.tint(Color.orange)
NavigationLink(destination: ColorShowView(color: color),
label: {Image(systemName: "magnifyingglass")})
.tint(.yellow)
}
}
}
Spacer()
NavigationLink(destination: ColorShowView(color: Color.red),
label: { Image(systemName: "magnifyingglass" ) } ).tint(.yellow)
}
.navigationViewStyle(StackNavigationViewStyle())
.navigationTitle(Text("List Of Colors"))
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct ColorRowView: View {
var color: Color
var body: some View {
VStack {
Text("Color \(color.description)").foregroundColor(color)
}
}
}
struct ColorShowView: View {
var color: Color
var body: some View {
VStack {
Text("Color Name \(color.description)").foregroundColor(color)
Text("Color Hash Value \(color.hashValue)").foregroundColor(color)
}
}
}
What I find is that if I put a NavigationLink as a button on the .swipeActions it shows up correctly, but when tapped the NavigationLink does not execute and hence does not take you to a new View.
If I move that same Navigation Link down to after the .swipeActions, and invoke it with some #State it works, but it adds another row in-between each row in the list of the ForEach. In other words, the ForEach of course sees it as part of its list and adds it in with the other items in the list. Even if I add a .hidden() onto that NavigationLink, it still takes up space with a new row, it just hides the contents of the row, not the row itself.
If I move the NavigationLink outside of the ForEach, then the input argument of color from the ForEach is out of scope. It will correctly build the view and execute the link (using an action and some #State), but it can not pass the color input from the ForEach because of course it is out of scope. If you hard code a color in its place it works fine, except of course for the fact that it does not have the color from the users selection from the list.
Note I put a simple NavigationLink on the bottom of the view as well just so that I could see that it worked correctly outside of the issue with the .swipeActions, and it does work fine with a hard coded color value like Color.red.
This is of course a very made up example, but I think it does show the issue.
Has anyone used a .swipeActions to invoke a NavigationLink to a new view passing into that view the users selected item (in this case the color)? If so how do you get that to work. It feels like a chicken and the egg problem, I can not seem to both have access to the scope in which the input data (the color) is available, and a NavigationLink that does not become part of the view of the ForEach list.
Anyone have any ideas? Thanks in advance for any and all commentary, corrections, ideas, etc.
First solution: by using fullScreenCover and #State var selectedColor
#Environment(.presentationMode) var presentationMode
// ContentView.swift
// StackOverFlow
//
// Created by Mustafa T Mohammed on 12/31/21.
//
import SwiftUI
struct ContentView: View {
#State var isColorShowViewPresented = false
#State var selectedColor: Color = .yellow
var colors : [Color] = [Color.red, Color.green,
Color.blue, Color.yellow,
Color.brown, Color.cyan, Color.pink]
var body: some View {
NavigationView {
VStack {
// you can use List instead of ForEach loop it's the same
// less code :)
List(colors, id: \.self) { color in
ColorRowView(color: color)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(action: {
isColorShowViewPresented.toggle() // toggle isColorShowViewPresented to trigger the
// fullScreenCover
selectedColor = color
print("Hello From The First Button")
},
label: {Label("Hello", systemImage: "face.smiling")})
.tint(Color.orange)
}
}
Spacer()
.fullScreenCover(isPresented: $isColorRowViewPresented) {
// if you like to implement what happen when user dismiss the presented view
print("user dissmissed ColorRowView")
} content: {
ColorShowView(color: selectedColor) // view that you want to present
}
}
.navigationViewStyle(StackNavigationViewStyle())
.navigationTitle(Text("List Of Colors"))
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct ColorRowView: View {
var color: Color
var body: some View {
VStack {
Text("Color \(color.description)").foregroundColor(color)
}
}
}
struct ColorShowView: View {
#Environment(\.presentationMode) var presentationMode
var color: Color
var body: some View {
VStack {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("Dismiss")
}
Spacer()
Text("Color Name \(color.description)").foregroundColor(color)
Text("Color Hash Value \(color.hashValue)").foregroundColor(color)
Spacer()
}
}
}
Second solution: NavigationLink and #State var selectedColor
//
// ContentView.swift
// StackOverFlow
//
// Created by Mustafa T Mohammed on 12/31/21.
//
import SwiftUI
struct ContentView: View {
#State var isColorShowViewPresented = false
#State var selectedColor: Color = .yellow
var colors : [Color] = [Color.red, Color.green,
Color.blue, Color.yellow,
Color.brown, Color.cyan, Color.pink]
var body: some View {
NavigationView {
VStack {
// you can use List instead of ForEach loop it's the same
// less code :)
List(colors, id: \.self) { color in
ColorRowView(color: color)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(action: {
isColorShowViewPresented.toggle() // toggle isColorShowViewPresented to trigger the
// NavigationLink
selectedColor = color
print("Hello From The First Button")
},
label: {Label("Hello", systemImage: "face.smiling")})
.tint(Color.orange)
}
}
Spacer()
NavigationLink("", isActive: $isColorShowViewPresented) {
ColorShowView(color: selectedColor)
}
}
.navigationViewStyle(StackNavigationViewStyle())
.navigationTitle(Text("List Of Colors"))
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct ColorRowView: View {
var color: Color
var body: some View {
VStack {
Text("Color \(color.description)").foregroundColor(color)
}
}
}
struct ColorShowView: View {
var color: Color
var body: some View {
VStack {
Text("Color Name \(color.description)").foregroundColor(color)
Text("Color Hash Value \(color.hashValue)").foregroundColor(color)
}
}
}

SwiftUI sheet does not dismiss

Using Swift5.2.3, iOS14.4.2, XCode12.4,
Working with the .sheet modifier in SwiftUI made me feel excited at first since it seemed like an easy and efficient way to display a modal sheet.
However, inside a real-world application it turns out that .sheet is all but ready for integration.
Here are two bugs found:
Bug 1: The sheet does not close sporadically
Bug 2: The Picker with DefaultPickerStyle does not work when inside a sheet's SegmentPicker (See this Stackoverlow-question that I created)
Let's focus now on Bug Nr1 : "sheet does not close":
The cmd presentationMode.wrappedValue.dismiss() is supposed to close a sheet. It works 90% of the cases. But every so often and without giving a hin on its reasons, the modal-sheet does not close.
Here is a code-excerpt:
import SwiftUI
import Firebase
struct MyView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Form {
Section(header: Text("Login")) {
Button(action: {
UserDefaults.standard.set(true, forKey: AppConstants.UserDefaultKeys.justLogoutLoginPressed)
try? Auth.auth().signOut()
// supposedly should work all the time - but it only works 90% of the time.....
presentationMode.wrappedValue.dismiss()
}) {
HStack {
Text((Auth.auth().currentUser?.isAnonymous ?? true) ? "Login" : "Logout")
Spacer()
}
}
}
}
.ignoresSafeArea()
Spacer()
}
}
}
I also tried to wrap the closing call inside the main-thread:
DispatchQueue.main.async {
presentationMode.wrappedValue.dismiss()
}
But it did not help.
Any idea why SwiftUI .sheets would not close using the presentationMode to dismiss it ??
Here I added the way the sheet is called in the first place. Since taken out of a bigger App, I obviously only show an example here on how the sheet is called:
import SwiftUI
#main
struct TestKOS005App: App {
#StateObject var appStateService = AppStateService(appState: .startup)
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(appStateService)
}
}
}
class AppStateService: ObservableObject {
#Published var appState: THAppState
var cancellableSet = Set<AnyCancellable>()
init(appState: THAppState) {
self.appState = appState
}
// ...
}
enum THAppState: Equatable {
case startup
case downloading
case caching
case waiting
case content(tagID: String, name: String)
case cleanup
}
struct MainView: View {
#EnvironmentObject var appStateService: AppStateService
#State var sheetState: THSheetSelection?
init() {
UINavigationBar.appearance().tintColor = UIColor(named: "title")
}
var body: some View {
ZStack {
NavigationView {
ZStack {
switch appStateService.appState {
case .caching:
Text("caching")
case .waiting:
Text("waiting")
case .content(_, _):
VStack {
Text("content")
Button(action: {
sheetState = .sheetType3
}, label: {
Text("Button")
})
}
default:
Text("no screen")
}
}
.sheet(item: $sheetState) { state in
switch state {
case .sheetType1:
Text("sheetType1")
case .sheetType2:
Text("sheetType2")
case .sheetType3:
MyView()
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
enum THSheetSelection: Hashable, Identifiable {
case sheetType1
case sheetType2
case sheetType3
var id: THSheetSelection { self }
}
I think when signing out, you probably have an instance checking whether Firebase Auth has an active user session and changes the view to the login screen when you call try? Auth.auth().signOut() and it might prevent the presentationMode.wrappedValue.dismiss() is being called.
You might want to create a state property in MainView and a corresponding Binding property in MyView and manage the state of signing out with them like follows.
In the MyView; instead of calling signout() directly;
struct MyView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var logoutTapped: Bool
var body: some View {
VStack {
Form {
Section(header: Text("Login")) {
Button(action: {
UserDefaults.standard.set(true, forKey: AppConstants.UserDefaultKeys.justLogoutLoginPressed)
// try? Auth.auth().signOut() -> instead of this directly
logoutTapped = true // call this
// supposedly should work all the time - but it only works 90% of the time.....
presentationMode.wrappedValue.dismiss()
}) {
HStack {
Text((Auth.auth().currentUser?.isAnonymous ?? true) ? "Login" : "Logout")
Spacer()
}
}
}
}
.ignoresSafeArea()
Spacer()
}
}
}
and in the MainView, when creating sheet, in onDismissal block, set a condition on logoutTapped bool state, and logout there like below;
struct MainView: View {
#EnvironmentObject var appStateService: AppStateService
#State var sheetState: THSheetSelection?
#State var logoutTapped = false
init() {
UINavigationBar.appearance().tintColor = UIColor(named: "title")
}
var body: some View {
ZStack {
NavigationView {
ZStack {
switch appStateService.appState {
case .caching:
Text("caching")
case .waiting:
Text("waiting")
case .content(_, _):
VStack {
Text("content")
Button(action: {
sheetState = .sheetType3
}, label: {
Text("Button")
})
}
default:
Text("no screen")
}
}
.sheet(item: $sheetState) {
if logoutTapped { // if this is true call signout
Auth.auth().signout()
}
} content: { state in
switch state {
case .sheetType1:
Text("sheetType1")
case .sheetType2:
Text("sheetType2")
case .sheetType3:
MyView(logoutTapped: $logoutTapped) // send logoutTapped to MyView
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}

How can I use multiple fullScreenCover in IOS14

I want to present the two destinations view in full screen mode from a single view.
Below is a sample of my code. Seem that the function only works for single presentation, if I have a second fullScreenCover defined, the first fullScreenCover didn't work properly.Is that any workaround at this moment?
import SwiftUI
struct TesFullScreen: View {
init(game : Int){
print(game)
}
var body: some View {
Text("Full Screen")
}
}
ContentView
import SwiftUI
struct ContentView: View {
#State var showFullScreen1 : Bool = false
#State var showFullScreen2 : Bool = false
var body: some View {
NavigationView {
VStack {
Spacer()
Button(action: { self.showFullScreen1 = true }) {
Text("Show Full Screen 1")
}
Button(action: { self.showFullScreen2 = true }) {
Text("Show Full Screen 2")
}
Spacer()
}
.navigationBarTitle("TextBugs", displayMode: .inline)
}
.fullScreenCover(isPresented: self.$showFullScreen1){
TesFullScreen(game: 1)
}
.fullScreenCover(isPresented: self.$showFullScreen2){
TesFullScreen(game: 2)
}
}
}
Not always the accepted answer works (for example if you have a ScrollView with subviews (cells in former days) which holds the buttons, that set the navigational flags).
But I found out, that you also can add the fullScreen-modifier onto an EmptyView. This code worked for me:
// IMPORTANT: Has to be within a container (e.g. VStack, HStack, ZStack, ...)
if myNavigation.flag1 || myNavigation.flag2 {
EmptyView().fullScreenCover(isPresented: $myNavigation.flag1)
{ MailComposer() }
EmptyView().fullScreenCover(isPresented: $myNavigation.flag2)
{ RatingStore() }
}
Usually some same modifier added one after another is ignored. So the simplest fix is to attach them to different views, like
struct FullSContentView: View {
#State var showFullScreen1 : Bool = false
#State var showFullScreen2 : Bool = false
var body: some View {
NavigationView {
VStack {
Spacer()
Button(action: { self.showFullScreen1 = true }) {
Text("Show Full Screen 1")
}
.fullScreenCover(isPresented: self.$showFullScreen1){
Text("TesFullScreen(game: 1)")
}
Button(action: { self.showFullScreen2 = true }) {
Text("Show Full Screen 2")
}
.fullScreenCover(isPresented: self.$showFullScreen2){
Text("TesFullScreen(game: 2)")
}
Spacer()
}
.navigationBarTitle("TextBugs", displayMode: .inline)
}
}
}
Alternate is to have one .fullScreenCover(item:... modifier and show inside different views depending on input item.
The only thing that worked for me was the answer in this link:
https://forums.swift.org/t/multiple-sheet-view-modifiers-on-the-same-view/35267
Using the EmptyView method or other solutions always broke a transition animation on one of the two presentations. Either transitioning to or from that view and depending on what order I chose them.
Using the approach by Lantua in the link which is using the item argument instead of isPresented worked in all cases:
enum SheetChoice: Hashable, Identifiable {
case a, b
var id: SheetChoice { self }
}
struct ContentView: View {
#State var sheetState: SheetChoice?
var body: some View {
VStack {
...
}
.sheet(item: $sheetState) { item in
if item == .a {
Text("A")
} else {
Text("B")
}
}
}
}
The sheetState needs to be optional for it to work.

Show a new View from Button press Swift UI

I would like to be able to show a new view when a button is pressed on one of my views.
From the tutorials I have looked at and other answered questions here it seems like everyone is using navigation button within a navigation view, unless im mistaken navigation view is the one that gives me a menu bar right arrows the top of my app so I don't want that. when I put the navigation button in my view that wasn't a child of NavigationView it was just disabled on the UI and I couldn't click it, so I guess I cant use that.
The other examples I have seen seem to use presentation links / buttons which seem to show a sort of pop over view.
Im just looking for how to click a regular button and show another a view full screen just like performing a segue used to in the old way of doing things.
Possible solutions
1.if you want to present on top of current view(ex: presentation style in UIKit)
struct ContentView: View {
#State var showingDetail = false
var body: some View {
Button(action: {
self.showingDetail.toggle()
}) {
Text("Show Detail")
}.sheet(isPresented: $showingDetail) {
DetailView()
}
}
}
2.if you want to reset current window scene stack(ex:after login show home screen)
Button(action: goHome) {
HStack(alignment: .center) {
Spacer()
Text("Login").foregroundColor(Color.white).bold()
Spacer()
}
}
func goHome() {
if let window = UIApplication.shared.windows.first {
window.rootViewController = UIHostingController(rootView: HomeScreen())
window.makeKeyAndVisible()
}
}
3.push new view (ex: list->detail, navigation controller of UIKit)
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("Show Detail View")
}.navigationBarTitle("Navigation")
}
}
}
}
4.update the current view based on #state property, (ex:show error message on login failure)
struct ContentView: View {
#State var error = true
var body: some View {
...
... //login email
.. //login password
if error {
Text("Failed to login")
}
}
}
For simple example you can use something like below
import SwiftUI
struct ExampleFlag : View {
#State var flag = true
var body: some View {
ZStack {
if flag {
ExampleView().tapAction {
self.flag.toggle()
}
} else {
OtherExampleView().tapAction {
self.flag.toggle()
}
}
}
}
}
struct ExampleView: View {
var body: some View {
Text("some text")
}
}
struct OtherExampleView: View {
var body: some View {
Text("other text")
}
}
but if you want to present more view this way looks nasty
You can use stack to control view state without NavigationView
For Example:
class NavigationStack: BindableObject {
let didChange = PassthroughSubject<Void, Never>()
var list: [AuthState] = []
public func push(state: AuthState) {
list.append(state)
didChange.send()
}
public func pop() {
list.removeLast()
didChange.send()
}
}
enum AuthState {
case mainScreenState
case userNameScreen
case logginScreen
case emailScreen
case passwordScreen
}
struct NavigationRoot : View {
#EnvironmentObject var state: NavigationStack
#State private var aligment = Alignment.leading
fileprivate func CurrentView() -> some View {
switch state.list.last {
case .mainScreenState:
return AnyView(GalleryState())
case .none:
return AnyView(LoginScreen().environmentObject(state))
default:
return AnyView(AuthenticationView().environmentObject(state))
}
}
var body: some View {
GeometryReader { geometry in
self.CurrentView()
.background(Image("background")
.animation(.fluidSpring())
.edgesIgnoringSafeArea(.all)
.frame(width: geometry.size.width, height: geometry.size.height,
alignment: self.aligment))
.edgesIgnoringSafeArea(.all)
.onAppear {
withAnimation() {
switch self.state.list.last {
case .none:
self.aligment = Alignment.leading
case .passwordScreen:
self.aligment = Alignment.trailing
default:
self.aligment = Alignment.center
}
}
}
}
.background(Color.black)
}
}
struct ExampleOfAddingNewView: View {
#EnvironmentObject var state: NavigationStack
var body: some View {
VStack {
Button(action:{ self.state.push(state: .emailScreen) }){
Text("Tap me")
}
}
}
}
struct ExampleOfRemovingView: View {
#EnvironmentObject var state: NavigationStack
var body: some View {
VStack {
Button(action:{ self.state.pop() }){
Text("Tap me")
}
}
}
}
In my opinion this bad way, but navigation in SwiftUI much worse