SwiftUI conditional view will not animate/transition - swift

I’m trying to get my views to animate/transition using .transition() on views. I use similar code from here and put .transition() to both conditional views.
struct Base: View {
#State private var isSignedIn = false
var body: some View {
Group {
if(isSignedIn){
Home().transition(.slide)
}else{
AuthSignin(isSignedIn: self.$isSignedIn).transition(.slide)
}
}
}
}
struct AuthSignin: View {
#Binding var isSignedIn: Bool
var body: some View {
VStack {
Button(action: {
self.isSignedIn = true
}) {
Text("Sign In")
.bold()
.frame(minWidth: CGFloat(0), maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(CGFloat(10))
}.padding()
}
}
}
However, whenever I click on the "Sign In" button (with or without .transition()), the app will freeze for a second and then the Home() view will suddenly appear without any animation/transition. I've also tried to wrap self.isSignedIn = true in withAnimation but it still won't work. Any ideas or is there a better way to do this?

Place your .transition on the container of the views that will switch, not each conditional view. Here's a trivial example from some code I have done (which works).
In the main View that needs to transition conditionally:
import SwiftUI
struct AppWrapperView: View {
#State var showFirstRun:Bool = true
var body: some View {
ZStack {
if (showFirstRun) {
FirstRunView(showFirstRun: $showFirstRun)
} else {
Text("Some other view")
}
}
.transition(.slide)
}
}
Then, somewhere in the view that triggers the change in condition:
import SwiftUI
struct FirstRunView: View {
#Binding var showFirstRun:Bool
var body: some View {
Button(action: {
withAnimation {
self.showFirstRun = false
}
}) {
Text("Done")
}
}
}

I had to put my if..else statement inside ZStack container instead of Group. Seems that Group was the main reason for broken animation in my case. Also, I applied .transition in combination with .animation to container instead of views.
ZStack {
if(isSignedIn){
Home()
} else {
AuthSignin(isSignedIn: self.$isSignedIn)
}
}
.transition(.slide)
.animation(.easeInOut)

Put
WithAnimation before self.isSignedIn = true

Related

Dismiss view and push to navigation stack SwiftUI

I have my entire application wrapped in a NavigationView and am trying to duplicate the transition in the brief video listed below. Based on what I am seeing, it looks like they present a fullScreenCover, and when a link is pressed it dismisses the fullScreenCover and pushes whatever was tapped onto the navigation stack once the dismiss has completed.
Example video
I currently have this...
#Environment(\.dismiss) var dismiss
ScrollView {
VStack {
ForEach(viewModel.searchResults, id: \.self) { bottle in
NavigationLink(destination: BottleDetailView(bottle: bottle)) {
BottleCell(bottle: bottle) <-- tapping this would dismiss fullscreenCover and push this NavigationLink into the NavigationStack of my app
}
}
}
}
I have tried embedding another navigation view in the fullscreenCover but that was not even close to duplicating the transition above. How can I duplicate this?
You can make use of a programmatically controllable NavigationStack. In the fullscreenCover first dismiss, then wait for it to vanish, then set the navigation destination.
Here is a full example:
struct ContentView: View {
#State private var search = ""
#State private var showSheet = false
// programmatically controllable Navigation Stack
#State private var path = [Int]()
var body: some View {
NavigationStack(path: $path) {
VStack {
searchField
.disabled(true)
.onTapGesture {
showSheet = true
}
Spacer()
Text("Other stuff")
Spacer()
}
.padding()
.navigationTitle("Find something")
.fullScreenCover(isPresented: $showSheet) {
fullscreenSheet
}
// this defines the destination(s) for the programatically activated navigation stack
.navigationDestination(for: Int.self) { value in
Text("Detail View for Result \(value)")
}
}
}
var fullscreenSheet: some View {
VStack(alignment: .leading, spacing: 30) {
HStack {
searchField
Button("Cancel") { showSheet = false }
}
// dummy search results
ForEach(1..<6) { result in
Button("Result \(result) >") {
// dismiss sheet
showSheet = false
// wait and trigger navigation
Task {
try await Task.sleep(for: .seconds(0.1))
self.path = [result]
}
}
}
Spacer()
}
.padding()
}
var searchField: some View {
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.white)
TextField("", text: $search,
prompt: Text("What are you looking for?")
.foregroundColor(.white)
)
}
.padding()
.background(
Capsule().fill(.gray)
)
}
}

Navigation stack yellow warning triangle

I'm attempting to listen for a change in a boolean value & changing the view once it has been heard which it does successfully, however, results in a yellow triangle. I haven't managed to pinpoint the issue but it doesn't seem to have anything to do with the view that it's transitioning to as even when changed the error still persists.
My code is below
import SwiftUI
struct ConversationsView: View {
#State var isShowingNewMessageView = false
#State var showChat = false
#State var root = [Root]()
var body: some View {
NavigationStack(path: $root) {
ZStack(alignment: .bottomTrailing) {
ScrollView {
LazyVStack {
ForEach(0..<20) { _ in
Text("Test")
}
}
}.padding()
}
Button {
self.isShowingNewMessageView.toggle()
} label: {
Image(systemName: "plus.message.fill")
.resizable()
.renderingMode(.template)
.frame(width: 48, height: 48)
.padding()
.foregroundColor(Color.blue)
.sheet(isPresented: $isShowingNewMessageView, content: {
NewMessageView(show: $isShowingNewMessageView, startChat: $showChat)
})
}
}
.onChange(of: showChat) { newValue in
guard newValue else {return}
root.append(.profile)
}.navigationDestination(for: Root.self) { navigation in
switch navigation {
case .profile:
ChatView()
}
}
}
enum Root {
case profile
}
}
ChatView() Code:
import SwiftUI
struct ChatView: View {
#State var messageText: String = ""
var body: some View {
VStack {
ScrollView {
VStack(alignment: .leading, spacing: 12) {
ForEach(MOCK_MESSAGES) { message in
MessageView(message: message)
}
}
}.padding(.top)
MessageInputView(messageText: $messageText)
.padding()
}
}
}
Any support is much appreciated.
You should use navigationDestination modifier inside your NavigationStack component, just move it.
NavigationStack(path: $root) {
ZStack(alignment: .bottomTrailing) {
ScrollView {
LazyVStack {
ForEach(0..<20) { _ in
Text("Test")
}
}
}.padding()
}.navigationDestination(for: Root.self) { navigation in
switch navigation {
case .profile:
ChatView()
}
}
//...
}
Basically this yellow triangle means NavigationStack can't find suitable component for path. And when you using navigationDestination directly on NavigationStack View or somewhere outside it is ignored
You must set .environmentObject(root) to NavigationStack in order to provide the NavigationPath to the view subhierarchy (ChatView in your case). Also you must have a #EnvironmentObject property of type Root in your ChatView so that it can read the path.

SwiftUI: FocusState animations stop working when using NavigationViews

I am trying to implement an animation relating to a TextField. A Cancel button slide in when the textfield is clicked. However, it only works correctly when it's in a standalone view. When I try to nest the view within a NavigationLink, the animation stops working. Here's the code:
struct TestView: View {
#FocusState private var isEditing: Bool
var body: some View {
VStack {
Button("click me", action: { isEditing.toggle() })
HStack {
TextField("Search", text: .constant("test"))
.focused($isEditing)
.padding(8)
.padding(.leading, 25)
.padding(.trailing, 22)
.background(Color.gray)
.cornerRadius(10)
.padding(.horizontal)
if isEditing {
Button {} label: {
ZStack {
Text("Cancel")
.foregroundColor(.primary)
.padding(.trailing)
}
}
.transition(.move(edge: .trailing))
}
}
.animation(.spring(), value: isEditing)
.navigationBarHidden(true)
}
}
}
Correct animation:
https://imgur.com/iqGr7fx
However, when I have a second view with a NavigationLink containing the previous view:
struct TestView2: View {
#State var test: Bool = false
var body: some View {
NavigationView {
VStack {
NavigationLink(isActive: $test, destination: { TestView() }, label: {})
Button("click me", action: { test.toggle() })
}
.navigationBarHidden(true)
}
}
}
The animation looks like this:
https://imgur.com/a/LK9pxf2
Is this a bug related to SwiftUI? Or am I not supposed to use FocusState for animations? If so, how can I change the code to have the animation work in both versions?
Fixed the animations by changing the FocusState variable to a State variable and then removing the .focused modifier and changing it to .onTapGesture, setting the isEditing variable to true

PopUp don't want to dismiss SwiftUI

I am having a problem while I want to dismiss a popup (that appears automatically depending on a specific condition) by clicking a button.
This is the PopUp struct:
struct dataPrivacyPopUp: View {
let model: OffersView.Model
let termsOfUseText = "Nutzungsbedingungen"
let privacyPolicyText = "Datenschutzerklärung"
#State var termsOfUseChecked = false
#State var privacyPolicyChecked = false
#State var buttonDisabled = true
#State private var showPopUp: Bool = false
#Binding var showModal: Bool
var body: some View {
ZStack {
if ( model.showPopUp == true) {
// PopUp Window
VStack(alignment: .center){
Image("logo")
.aspectRatio(contentMode: .fill)
.frame(alignment: .center)
.padding()
VStack(alignment: .leading) {
Text((model.acceptance?.salutation)!)
.multilineTextAlignment(.leading)
.padding()
.foregroundColor(Color.black)
Text((model.acceptance?.statement)!)
.multilineTextAlignment(.leading)
.padding()
.foregroundColor(Color.black)
Text((model.acceptance?.declarationIntro)!)
.multilineTextAlignment(.leading)
.padding()
.foregroundColor(Color.black)
if ((model.acceptance?.dpr)! == true) {
VStack(alignment: .leading){
HStack {
CheckBoxView(checked: $privacyPolicyChecked)
HStack(spacing: 0){
Text(R.string.localizable.dataPrivacyPopupText())
.foregroundColor(Color.black)
Button(privacyPolicyText) {
model.openUrl(url: API.privacyPolicyURL)
}
}
}
Text((model.acceptance?.declarationOutro)!)
.multilineTextAlignment(.leading)
.padding()
}
.padding()
Button(action: {
model.setTos()
print("showModal PopUpView2 1: \(showModal)")
self.showModal.toggle()
print("showModal PopUpView2 2: \(showModal)")
}, label: {
Text(R.string.localizable.dataPrivacyButton())
.foregroundColor(Color.white)
.font(Font.system(size: 23, weight: .semibold))
})
.disabled(model.buttonDisabledForOne(privacyPolicyChecked: privacyPolicyChecked, termsOfUseChecked: termsOfUseChecked))
.padding()
}
}
}
// .onAppear(perform: )
.background(Color.white01)
.padding()
}
}
}
}
and this is where I call it (contentView):
struct OffersView: View {
#StateObject var model = Model()
#State private var showingPopUp = false
#State private var showModal = false
#State private var showingAddUser = false
// var showPopup : Bool = true
var body: some View {
NavigationView {
Group {
switch model.sections {
case .loading:
ActivityIndicator(animate: true)
case .success(let sections):
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 0) {
Text(R.string.localizable.offersHello(model.firstName))
.aplFont(.headline02)
.padding(.bottom, 24)
VStack(spacing: 48) {
ForEach(sections) { section in
OffersSectionView(section: section, model: model)
}
}
}
.useFullWidth(alignment: .leading)
.padding()
}
default:
Color.clear
if ( model.showPopUp == true) {
ZStack {
Color.black.opacity(model.showPopUp ? 0.3 : 0).edgesIgnoringSafeArea(.all)
dataPrivacyPopUp(model: model, showModal: self.$showModal)
.onAppear(perform: {
self.showModal.toggle()
})
}
}
}
}
.navigationBarHidden(true)
.handleNavigation(model.navigationPublisher)
.onAppear(perform: model.onAppear)
.onDisappear(perform: model.onDisappear)
.environment(\.dynamicTypeEnabled, false)
.safariView(isPresented: model.showSafari) {
SafariView(url: model.safariUrl!)
}
}
}
}
I need help about this, I tried the traditional method to set a #Binding variable etc .. but that's not working, the boolean value is changing but the UI is not updating (the popup is not dismissing), thank you
I tried to look at your code - I suggest you simplify it to the bare minimum to exemplify your issue - and it seems that you are using 2 properties to show your pop-up: showingPopUp and showModal. It is quite likely that you are having trouble keeping them both in sync.
For starters, I would suggest to use only one variable, either it is true or false - "a man with two watches never knows what time it is".
For the solution:
If you prefer keeping your ZStack approach, the solution would look something like:
struct MyPrivacy: View {
#Binding var showMe: Bool
var body: some View {
VStack {
Text("The content of the pop-up")
.padding()
Button {
withAnimation {
showMe.toggle()
}
} label: {
Text("Dismiss")
}
}
}
}
struct Offers: View {
#State private var showPopup = false
var body: some View {
NavigationView {
ZStack {
VStack {
Text("View behind the pop-up")
.padding()
Button {
withAnimation {
showPopup.toggle()
}
} label: {
Text("Pop")
}
}
if showPopup {
Color.white
MyPrivacy(showMe: $showPopup)
}
}
}
}
}
If instead you want to go for a more flexible approach, if you are developing for iOS, SwiftUI has a convenient object - Sheets. You can use it as suggested in the documentation, or build a specific struct that manages all the modal views of this type and use your model to handle the presentation.
The process goes like:
Create a struct that will handle all kinds of Sheets of your app.
Add to your view-model the property to present any sheet.
Create the Views that will be the content of each sheet.
Call the .sheet(item:content:) method on each View the requires a sheet.
Here's the sample code:
SheetView handler:
struct SheetView: Identifiable {
// This struct controls what modal view will be presented.
// The enum SheetScreenType can grow to as many as different
// modal views your app needs - add the content in the switch below.
let id = UUID()
var screen: SheetScreenType
#ViewBuilder
var content: some View {
switch screen {
case .dataPrivacy:
DataPrivacy()
default:
EmptyView()
}
}
enum SheetScreenType {
case dataPrivacy
case none
}
}
Presenter in your view-model:
class MyViewModel: ObservableObject {
// This code can fit anywhere within your view-model.
// It controls the presentation of the modal view, which in
// this case is a Sheet.
private let sharedSheet = SheetView(screen: .none)
// Show the selected sheet
#Published var sheetView: SheetView?
var showSheet: SheetView.SheetScreenType {
get {
return sheetView?.screen ?? .none
}
set {
switch newValue {
case .none:
sheetView = nil
default:
sheetView = sharedSheet
}
sheetView?.screen = newValue
}
}
}
Content of your modal view:
struct DataPrivacy: View {
#EnvironmentObject var model: MyViewModel // Pass YOUR model here
var body: some View {
VStack(alignment: .center){
Text("Respecting your privacy, no details are shown here")
.padding()
Button {
print("Anything you need")
// Set the showSheet property of your model to
// present a modal view. Setting it to .none dismisses
// the modal view.
model.showSheet = .none
} label: {
Text("Time do dismiss the modal view")
}
.padding()
}
}
}
Enable your view to listen to your model to present the sheet:
struct OffersView: View {
#ObservedObject var model = MyViewModel() // Pass YOUR model here
var body: some View {
VStack {
Text("Anything you wish")
.padding()
Button {
withAnimation {
// Set the showSheet property of your model to
// present a modal view. Set it to any choice
// among the ones in the SheetScreen.SheetScreenType enum.
model.showSheet = .dataPrivacy
}
} label: {
Text("Tap here for the privacy in modal view")
}
}
// Show a modal sheet.
// Add this property at the top level of every view that
// requires a modal view presented - whatever content it might have.
.sheet(item: $model.sheetView) { sheet in
sheet.content
.environmentObject(model)
}
}
}
Good luck with your project!

Button, how to open a new View in swiftUI embedded in navigation bar

I embedded a button on on the NavigationBar.
I'm try to make button to open a new View called DetailView
I try to use NavigationLink but it does't work inside a button.
import SwiftUI
struct ContentView: View {
#ObservedObject var dm: DataManager
#State var isAddPresented = false
var body: some View {
NavigationView {
HStack {
List () {
ForEach (dm.storage) { data in
StileCella(dm2: data)
}
}
.navigationBarTitle("Lista Rubrica")
.navigationBarItems(trailing: Button(action: {
self.isAddPresented = true
// Load here the DetailView??? How??
DetailView()
}) {
Text("Button")
})
}
}
}
}
struct DetailView: View {
var body: some View {
VStack(alignment: .center) {
Text("CIAO").bold()
Spacer()
Image(systemName: "star")
.resizable()
}
}
}
You just need to add a sheet modifier to your view, which presents your view depending on the value of isAddPresented, just like this:
struct ContentView: View {
#State var isAddPresented = false
var body: some View {
NavigationView {
List(dm.storage){ data in
StileCella(dm2: data)
}
.navigationBarTitle("Lista Rubrica")
.navigationBarItems(trailing: Button("Button") {
self.isAddPresented = true
})
} .sheet(isPresented: $isAddPresented,
onDismiss: { self.isAddPresented = false }) {
DetailView()
}
}
}
The important bit is to remember to set isAddPresented back to false in on dismiss to prevent it form presenting again.
If you want to open a new view just like we used to open through storyboard other than sheet, you can update the code in the following way:
import SwiftUI
struct ContentView: View {
#ObservedObject var dm: DataManager
#State var isAddPresented = false
var body: some View {
NavigationView {
HStack {
List () {
ForEach (dm.storage) { data in
StileCella(dm2: data)
}
}
.navigationBarTitle("Lista Rubrica")
.navigationBarItems(leading:
NavigationLink(destination: DetailView()) {
Text("Button")
})
}
}
}
}
struct DetailView: View {
var body: some View {
VStack(alignment: .center) {
Text("CIAO").bold()
Spacer()
Image(systemName: "star")
.resizable()
}
}
}
Instead of button, simply add NavigationLink inside navigationBarItems. This would do the trick! I wrote the complete for guidance but main change point is, I used
.navigationBarItems(leading:
NavigationLink(destination: DetailView()) {
Text("Button")
})
instead of:
.navigationBarItems(trailing: Button(action: {
self.isAddPresented = true
// Load here the DetailView??? How??
DetailView()
}) {
Text("Button")
})