Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sliding one SwiftUI view out from underneath another

Tags:

swift

swiftui

I'm attempting to construct an animation using SwiftUI.

Start: [ A ][ B ][ D ]
End:   [ A ][ B ][    C    ][ D ]

The key elements of the animation are:

  • C should appear to slide out from underneath B (not expand from zero width)
  • The widths of all views are defined by subviews, and are not known
  • The widths of all subviews should not change during or after the animation (so, total view width is larger when in the end state)

I'm having a very difficult time satisfying all of these requirements with SwiftUI, but have been able to achieve similar affects with auto-layout in the past.

My first attempt was a transition using an HStack with layoutPriorities. This didn't really come close, because it affects the width of C during the animation.

My second attempt was to keep the HStack, but use a transition with asymmetrical move animations. This came really close, but the movement of B and C during the animation does not give the effect that C was directly underneath B.

My latest attempt was to scrap relying on an HStack for the two animating views, and use a ZStack instead. With this setup, I can get my animation perfect by using a combination of offset and padding. However, I can only get it right if I make the frame sizes of B and C known values.

Does anyone have any ideas on how to achieve this effect without requiring fixed frame sizes for B and C?

like image 612
Mattie Avatar asked Jun 17 '19 14:06

Mattie


People also ask

How do I animate a view in SwiftUI?

If you want a SwiftUI view to start animating as soon as it appears, you should use the onAppear() modifier to attach an animation.

What is withAnimation SwiftUI?

withAnimation() takes a parameter specifying the kind of animation you want, so you could create a three-second linear animation like this: withAnimation(.linear(duration: 3)) Explicit animations are often helpful because they cause every affected view to animate, not just those that have implicit animations attached.

How do I add a delay in SwiftUI?

You can use DispatchQueue to delay something. Trigger it with onAppear(perform:) (which happens when the view first appears). You could also hook the delay up to a Button instead if wanted.

How do I add animations to SwiftUI?

SwiftUI has built-in support for animations with its animation() modifier. To use this modifier, place it after any other modifiers for your views, tell it what kind of animation you want, and also make sure you attach it to a particular value so the animation triggers only when that specific value changes.


2 Answers

Since I originally replied to this question, I have been investigating GeometryReader, View Preferences and Anchor Preferences. I have assembled a detailed explanation that elaborates further. You can read it at: https://swiftui-lab.com/communicating-with-the-view-tree-part-1/

enter image description here

Once you get the CCCCCCCC view geometry into the textRect variable, the rest is easy. You simply use the .offset(x:) modifier and clipped().

import SwiftUI

struct RectPreferenceKey: PreferenceKey {
    static var defaultValue = CGRect()

    static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
        value = nextValue()
    }

    typealias Value = CGRect
}

struct ContentView : View {
    @State private var textRect = CGRect()
    @State private var slideOut = false

    var body: some View {

        return VStack {
            HStack(spacing: 0) {
                Text("AAAAAA")
                    .font(.largeTitle)
                    .background(Color.yellow)
                    .zIndex(4)


                Text("BBBB")
                    .font(.largeTitle)
                    .background(Color.red)
                    .zIndex(3)

                Text("I am a very long text")
                    .zIndex(2)
                    .font(.largeTitle)
                    .background(GeometryGetter())
                    .background(Color.green)
                    .offset(x: slideOut ? 0.0 : -textRect.width)
                    .clipped()
                    .onPreferenceChange(RectPreferenceKey.self) { self.textRect = $0 }

                Text("DDDDDDDDDDDDD").font(.largeTitle)
                    .zIndex(1)
                    .background(Color.blue)
                    .offset(x: slideOut ? 0.0 : -textRect.width)

            }.offset(x: slideOut ? 0.0 : +textRect.width / 2.0)

            Divider()
            Button(action: {
                withAnimation(.basic(duration: 1.5)) {
                    self.slideOut.toggle()
                }
            }, label: {
                Text("Animate Me")
            })
        }

    }
}

struct GeometryGetter: View {
    var body: some View {
        GeometryReader { geometry in
            return Rectangle()
                .fill(Color.clear)
                .preference(key: RectPreferenceKey.self, value:geometry.frame(in: .global))
        }
    }
}
like image 117
kontiki Avatar answered Oct 18 '22 21:10

kontiki


It's hard to tell what exactly you're going for or what's not working. It would be easier to help you if you showed the "wrong" animation you came up with or shared your code.

Anyway, here's a take. I think it sort of does what you specified, though it's certainly not perfect:

Animated GIF of my solution

Observations:

  • The animation relies on the assumptions that (A) and (B) together are wider than (C). Otherwise, parts of (C) would appear to the left of A at the start of the animation.

  • Similarly, the animation relies on the fact that there's no spacing between the views. Otherwise, (C) would be appear to the left of (B) when it's wider than (B).

    It may be possible to solve both problems by placing an opaque underlay view in the hierarchy such that it is below (A), (B), and (D), but above (C). But I haven't thought this through.

  • The HStack seems to expand a tad more quickly than (C) is sliding in, which is why a white portion appears briefly. I didn't manage to eliminate this. I tried adding the same animation(.basic()) modifier to the HStack, the transition, the withAnimation call, and the VStack, but that didn't help.

The code:

import SwiftUI

struct ContentView: View {
  @State var thirdViewIsVisible: Bool = false

  var body: some View {
    VStack(alignment: .leading, spacing: 20) {
      HStack(spacing: 0) {
        Text("Lorem ").background(Color.yellow)
          .zIndex(1)
        Text("ipsum ").background(Color.red)
          .zIndex(1)
        if thirdViewIsVisible {
          Text("dolor sit ").background(Color.green)
            .zIndex(0)
            .transition(.move(edge: .leading))
        }
        Text("amet.").background(Color.blue)
          .zIndex(1)
      }
        .border(Color.red, width: 1)
      Button(action: { withAnimation { self.thirdViewIsVisible.toggle() } }) {
        Text("Animate \(thirdViewIsVisible ? "out" : "in")")
      }
    }
      .padding()
      .border(Color.green, width: 1)
  }
}
like image 38
Ole Begemann Avatar answered Oct 18 '22 21:10

Ole Begemann