Is it possible to change an asymmetric transition while on screen. For example, the text has a asymmetric transition of
.transition(.asymmetric(insertion: .move(edge: .trailing) removal: .move(edge: .leading)))
While the text is on the screen the transition needs to be changed to this
.transition(.asymmetric(insertion: .move(edge: .trailing) removal: .opacity))
I have created a test program which uses a #State variable that changes the transitions. The issue is when trying to change the removal transition after the view has been insertion
To simulate the problem with the code below simply press "animate text" then "change animation" then "animate text". You will see the change animation has no effect until you press "animate text" a third time (when the text is off the screen).
import SwiftUI
// Xcode 11.3
struct ContentView: View {
#State var buttonClick: Bool = false
#State var showText: Bool = false
var body: some View {
VStack(spacing: 10){
if showText == true{
Text("hello")
.transition(.asymmetric(insertion: buttonClick ? .move(edge: .trailing) : .opacity, removal: buttonClick ? .move(edge: .leading) : .opacity))
}
Button (action: { withAnimation { self.buttonClick.toggle() } }) {
Text("change transition")
}
Button (action: { withAnimation { self.showText.toggle() }}) {
Text("Animate text")
}
}
}
}
Related
I have this strange animation bug I've not been able to figure out. When transitioning on a view that contains a slider, the slider appears to animate independently of the rest of the view.
For reference, here is the code that animates the two views:
#main
struct TransitionAnimationBug: App {
#State var displaySettings = false
var body: some Scene {
WindowGroup {
Group {
if displaySettings {
SliderView(displaySettings: $displaySettings)
.transition(AnyTransition.asymmetric(
insertion: .move(edge: .trailing),
removal: .move(edge: .leading)
))
} else {
MainView(displaySettings: $displaySettings)
.transition(AnyTransition.asymmetric(
insertion: .move(edge: .leading),
removal: .move(edge: .trailing)
))
}
}
.animation(.easeInOut, value: displaySettings) // Shows bug
// .animation(.spring(), value: displaySettings) // Works
}
}
}
And here is the code from the settings screen that displays the slider:
struct SliderView: View {
var displaySettings: Binding<Bool>
#State var sliderValue: Double = 4.0
init(displaySettings: Binding<Bool>) {
self.displaySettings = displaySettings
}
var body: some View {
VStack(spacing: 30) {
Text("Hello, world!")
Slider(value: $sliderValue, in: 0 ... 4, step: 1)
.border(.red)
.padding(20)
Button("Close") { displaySettings.wrappedValue.toggle() }
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.black.opacity(0.2))
}
}
And for completeness the main view:
struct MainView: View {
var displaySettings: Binding<Bool>
init(displaySettings: Binding<Bool>) {
self.displaySettings = displaySettings
}
var body: some View {
VStack(spacing: 30) {
Text("Hello, world!")
.padding()
Button("Show transition bug") {
displaySettings.wrappedValue.toggle()
}
Text("The slider will animate incorrectly")
.padding()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.white)
}
}
As you can see this is a minimal app I built to show the bug (which I've submitted to Apple via their Feedback assistant), but I was wondering if anyone here as encountered it and found a solution.
Through experimenting I found that using a .spring() animation instead of .slideInOut correctly animates the slider, but any other animation fails.
Any thoughts?
This is effect of changing root view of window. The fix is to move the content of WindowGroup with all related states into separated view (to make root of window persistent), so it looks like
WindowGroup {
ContentView() // << everything else move here !!
}
Tested with Xcode 13.3 / iOS 15.4
Thanks Asperi, that worked. Do you know why? On first glance there's no obvious reason why it would work other than it being something to do with the fact that the body returns a Scene as opposed to a View.
The changed code looks like this:
#main
struct TransitionAnimationBug: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var displaySettings = false
var body: some View {
Group {
if displaySettings {
SliderView(displaySettings: $displaySettings)
.transition(AnyTransition.asymmetric(
insertion: .move(edge: .trailing),
removal: .move(edge: .leading)
))
} else {
MainView(displaySettings: $displaySettings)
.transition(AnyTransition.asymmetric(
insertion: .move(edge: .leading),
removal: .move(edge: .trailing)
))
}
}
.animation(.easeInOut, value: displaySettings)
}
}
I've been perusing SO, and elsewhere, trying to find a way, if possible, to change the direction of how a view is hidden. The examples I've found hide a view by replacing it with an EmptyView, or changing the view's frame dimensions to zero. I am trying to hide a view by 'collapsing' it vertically, but everywhere I've looked the collapse/hide animation happens upwards.
In my case, a view will have a button underneath it that will collapse (hide) or expand (show) the view. The view and button are embedded in a scroll view. What I'd like to have happen is that when the view is wholly or partially scrolled up off the top of the screen, and the collapse button is tapped, the view should 'collapse downward' such that the button remains where it is. Everything I've tried causes the button to move upward off the screen.
I think that what has to happen is that the view origin's y value needs to change. But what would happen to all other views above this view? I've tried to accomplish what I've described by changing the transition but it's not having any affect.
I feel like I'm really missing something basic here so thanks for any help.
struct Collapsible<Content: View>: View {
private var content: () -> Content
#Binding var isCollapsed: Bool
#State var isOffscreen: Bool = false
init(isCollapsed: Binding<Bool>, content: #escaping () -> Content) {
self._isCollapsed = isCollapsed
self.content = content
}
var body: some View {
VStack {
content()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: isCollapsed ? 0 : nil)
.clipped()
.animation(.default, value: isCollapsed)
.transition(transition.combined(with: .opacity))
}
.background(
GeometryReader { proxy -> Color in
DispatchQueue.main.async {
let frame = proxy.frame(in: CoordinateSpace.global)
self.isOffscreen = frame.origin.y < 0
var _ = print("isOffscreen: \(self.isOffscreen)")
}
return Color.clear
}
)
}
var transition: AnyTransition {
if isOffscreen {
return AnyTransition.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .top))
} else {
return AnyTransition.asymmetric(insertion: .move(edge: .top), removal: .move(edge: .bottom))
}
}
}
struct ContentView: View {
#State var isCollapsed = false
var body: some View {
ScrollView {
VStack {
Color.clear.frame(height: 250)
Collapsible(isCollapsed: $isCollapsed) {
HStack {
Text("Content")
}
.frame(maxWidth: .infinity)
.padding([.top, .bottom], 75)
.background(.secondary)
.border(.blue, width: 2)
}
Button(action: {
self.isCollapsed.toggle()
}, label: {
Text(isCollapsed ? "Expand" : "Collapse")
})
.buttonStyle(PlainButtonStyle())
Color.clear.frame(height: 1000)
}
.padding()
}
}
}
The RoundedRectangle "animates out" but "snaps in" if I use List.
Is it a SwiftUI bug or me?
Also, If the third navigation link is commented out then the button in the rectangles view pops the app back to the Navigation view. Next time I go to the rectangles it has changed the color.
This happens only with VStack, List is unaffected.
This makes no sense at all.
import SwiftUI
struct ContentView: View {
#State var fff: Bool = false
var body: some View {
NavigationView {
VStack { // <---- This works if I have three navigation links, but not if I have only two.
// List { // <------ This does not work.
NavigationLink(
destination:
ZStack {
if fff {
RoundedRectangle(cornerRadius: 100) .foregroundColor(.blue)
.transition(.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .top)))
.animation(.easeIn)
}
else {
RoundedRectangle(cornerRadius: 100) .foregroundColor(.purple)
.transition(.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .top)))
.animation(.easeIn)
}
Button(action: {
fff.toggle()
}, label: {
Text("Button")
})
}
,
label: {
Text("To rectangles")
}
)
NavigationLink(
destination: Text("Settings"),
label: {
HStack {
Image(systemName: "wrench")
Text("Settings")
}
}
)
// NavigationLink( <--- If this link is commented out then the button in the rectangles view pops the app back to the Navigation view (only when using VStack).
// destination: Text("Store"),
// label: {
// HStack {
// Image(systemName: "cart")
// Text("Store")
// }
// }
// )
}
}
}
}
Moving the destination to a different struct fixes the issue.
struct RectView: View {
#State var fff: Bool = false
var body: some View {
ZStack {
if fff {
RoundedRectangle(cornerRadius: 100) .foregroundColor(.blue)
.transition(.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .top)))
.animation(.easeIn)
} else {
RoundedRectangle(cornerRadius: 100) .foregroundColor(.purple)
.transition(.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .top)))
.animation(.easeIn)
}
Button(action: {
fff.toggle()
}, label: {
Text("Button")
})
}
}
}
I have a SheetAnimationView from which I want to show a sheet called SheetContentView.
When the sheet appears, I want to show a transition animation of its content (start the animation when the content appears) but am unable to make it work. All 3 views in VStack should ideally end their animation at the same time.
SheetContentView:
struct SheetContentView: View {
#Binding var showSheet: Bool
var body: some View {
VStack(spacing: 8) {
Text("A great content of my new sheet")
Label("still not done", systemImage: "guitars")
Text("I'm done now")
}
.transition(.asymmetric(insertion: .scale, removal: .opacity)) // <--- I want this to work
.animation(Animation.easeInOut(duration: 2)) // <--- for 2 seconds
}
}
SheetAnimationView:
struct SheetAnimationView: View {
#State var showSheet: Bool = false
var body: some View {
Button("show my sheet with animated content (hopefully)") {
showSheet = true
}
.sheet(isPresented: $showSheet) {
SheetContentView(showSheet: $showSheet)
}
}
}
Transition works when view is appeared in view hierarchy (not on screen), so to solve this we need another container and state.
Here is a fixed variant. Tested with Xcode 12.1 / iOS 14.1
struct SheetContentView: View {
#Binding var showSheet: Bool
#State private var isShown = false
var body: some View {
VStack { // container to animate transition !!
if isShown {
VStack(spacing: 8) {
Text("A great content of my new sheet")
Label("still not done", systemImage: "guitars")
Text("I'm done now")
}
.transition(.asymmetric(insertion: .scale, removal: .opacity))
}
}
.animation(Animation.easeInOut(duration: 2))
.onAppear {
isShown = true // << activate !!
}
}
}
I have a question about swift. How I can navigate into views ?
Now, I have my ContentView that display a launchScreen, or Login, or HomeView in a conditional (if).
var body: some View {
VStack {
if (sessionStore.session != nil) {
UserProfileView(userProfile: self.profile ?? UserProfile(uid: "", firstName: "", lastName: "", birth: "", email: "", phone: ""))
} else if show || UserDefaults.standard.bool(forKey: initialLaunchKey){
AuthentificationView().transition(.move(edge: .bottom))
} else {
PageViewContainer( viewControllers: Page.getAll.map({ UIHostingController(rootView: PageView(page: $0) ) }), presentSignupView: { withAnimation { self.show = true }; UserDefaults.standard.set(true, forKey: self.initialLaunchKey) }).transition(.scale)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.backgroundColor)
.onTapGesture { UIApplication.shared.endEditing() }
}
But if I am in the LoginView, even if my condition in contentView for HomeView become true, I'm not going to HomeView...
I navigate into view by a var in observable object (page =1 then page =2...) I think is not the better way...
struct AuthentificationView: View {
#EnvironmentObject var userSignup: UserSignup
var body: some View {
VStack {
if (userSignup.page >= 1) {
SignupView()
.transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)))
} else {
LoginView()
.transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing)))
}
}
}
}
Thanks
I cannot test your code (due to absent custom things), so just by experience try the following
use explicit condition for every view with transition
make transitions animatable
make container holding transitions animatable
Like below (scratchy)
var body: some View {
VStack {
if userSignup.page >= 1 {
SignupView()
.transition(AnyTransition.asymmetric(insertion: .move(edge: .trailing),
removal: .move(edge: .leading)).animation(.default))
}
if userSignup.page == 0 {
LoginView()
.transition(AnyTransition.asymmetric(insertion: .move(edge: .leading),
removal: .move(edge: .trailing)).animation(.default))
}
}.animation(.default)
}