How to get animation like this
Code: with this code i am able to navigate views normally but if i am in AttendanceSwipeView(viewModel: viewModel)
then if i tap on Dashboard then view should come with this kind of animation(like something from bottom back to front)...
i mean Dashboard > Attendance > Feed ... navigation can be normal but
from Feed to Attendance and Attendance to Dashboard... here need down to top kind of animation. if possible please guide me how to achieve this.
struct DashboardBottomView: View {
@StateObject var viewModel: AppDashboardViewModel
@State var selectedClassInd: Int = 0
@State private var stringArray = ["Dashboard", "Attendance", "Feed"]
@State private var selectedIndex: Int? = nil
@State private var selectedIndexStr: String? = "Dashboard"
private func viewForSelectedIndex() -> some View {
switch selectedIndexStr {
case "Dashboard":
return AnyView(DashboardView(viewModel: viewModel))
case "Attendance":
return AnyView(AttendanceSwipeView(viewModel: viewModel))
case "Feed":
return AnyView(DashboardFeedView())
default:
return AnyView(Text("Default View"))
}
}
var body: some View {
VStack(spacing: 0) {
Spacer()
ZStack {
viewForSelectedIndex()
.toolbar(.hidden, for: .navigationBar)
}
ZStack{
Color.appGreen2
ScrollView(.horizontal, showsIndicators: false) {
LazyHGrid(rows: [GridItem(.flexible(), spacing: 0)], spacing: 0) {
ForEach(stringArray, id: \.self) { data in
Button {
withAnimation {
selectedIndexStr = data
}
}
label: {
VStack {
Text(data)
.font(.calibriBold(with: 14))
.foregroundColor(Color.white)
.padding(.horizontal, 8)
if selectedIndexStr == data {
gradientDivider
.frame(height: 2)
}
}
.frame(height: 40)
.frame(minWidth: 108)
.animation(.default, value: selectedClassInd)
}
.buttonStyle(.plain)
}
}
.padding(.bottom, 0)
.background(Color.appGreen2)
}
.frame(height: 55)
}
}
.ignoresSafeArea()
.onAppear {
viewModel.fetchDashboardData { status in
if status {
stringArray = viewModel.dashboardButtonsArray
selectedIndexStr = stringArray.first
}
}
}
}
}
A custom transition could be used here.
A custom transition can apply a ViewModifier
to the view in its active
(before) and identity
(after) state. When the transition is animated, there is automatic interpolation between the two states. This is fine for a simple linear transition, as seems to be the case here.
The following ViewModifier
applies opacity, rotation and offset effects to the view:
struct SwipeMovement: ViewModifier {
let progress: Double
func body(content: Content) -> some View {
content
.opacity(progress)
.offset(y: -400)
.rotationEffect(.degrees((progress - 1) * 30))
.offset(y: 400 + ((1 - progress) * 100))
}
}
The custom transition can be defined as an extension
to AnyTransition
:
private extension AnyTransition {
static var swipeMovement: AnyTransition {
.modifier(
active: SwipeMovement(progress: 0),
identity: SwipeMovement(progress: 1)
)
}
}
Now the custom transition is ready to be used. The code below is an adapted version of your example, with the following changes:
A new state variable previousSelectedIndexStr
has been added, to record the previous navigation target.
The button action saves the current selection as previousSelectedIndexStr
, before changing selectedIndexStr
.
The function viewForSelectedIndex
has been tagged as a ViewBuilder
. This avoids having to wrap the different kinds of result as AnyView
.
isSpecialTransitionNeeded
is a computed property that determines whether the special transition is needed, based on the previous and next value of selectedIndexStr
. This is doing string comparison as you were using before, but I would suggest it might be better to be using an enum here.
The top ZStack
includes an if
statement that determines whether to apply a different transition. This is a fairly primitive but also easy way to switch between different types of transition.
The special transition is only used for the insertion
transition, the removal
transition is always .opacity
.
An .animation
modifier has been added to the ZStack
. This controls the speed of the transition. For demo purposes, .gray
has also been applied as background to the ZStack
.
Code that didn't compile in your original example has been commented out or replaced with simple alternatives.
struct DashboardBottomView: View {
// @StateObject var viewModel: AppDashboardViewModel
@State var selectedClassInd: Int = 0
@State private var stringArray = ["Dashboard", "Attendance", "Feed"]
@State private var selectedIndex: Int? = nil
@State private var previousSelectedIndexStr: String?
@State private var selectedIndexStr: String? = "Dashboard"
@ViewBuilder
private func viewForSelectedIndex() -> some View {
switch selectedIndexStr {
case "Dashboard":
Text("DashboardView")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.background)
case "Attendance":
Text("AttendanceSwipeView")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.yellow)
case "Feed":
Text("DashboardFeedView")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.orange)
default:
Text("Default View")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.red)
}
}
private var isSpecialTransitionNeeded: Bool {
if let previousSelectedIndexStr, let selectedIndexStr {
(previousSelectedIndexStr == "Feed" && selectedIndexStr == "Attendance") ||
(previousSelectedIndexStr == "Attendance" && selectedIndexStr == "Dashboard")
} else {
false
}
}
var body: some View {
VStack(spacing: 0) {
Spacer()
ZStack {
if isSpecialTransitionNeeded {
viewForSelectedIndex()
.transition(
.asymmetric(
insertion: .swipeMovement,
removal: .opacity
)
)
} else {
viewForSelectedIndex()
.transition(.opacity)
}
}
.background(.gray)
.animation(.easeInOut(duration: 1), value: selectedIndexStr)
.toolbar(.hidden, for: .navigationBar)
ZStack{
Color.green //appGreen2
ScrollView(.horizontal, showsIndicators: false) {
LazyHGrid(rows: [GridItem(.flexible(), spacing: 0)], spacing: 0) {
ForEach(stringArray, id: \.self) { data in
Button {
previousSelectedIndexStr = selectedIndexStr
withAnimation {
selectedIndexStr = data
}
}
label: {
VStack {
Text(data)
// .font(.calibriBold(with: 14))
.foregroundColor(Color.white)
.padding(.horizontal, 8)
// if selectedIndexStr == data {
// gradientDivider
// .frame(height: 2)
// }
}
.frame(height: 40)
.frame(minWidth: 108)
.animation(.default, value: selectedClassInd)
}
.buttonStyle(.plain)
}
}
.padding(.bottom, 0)
.background(Color.green) // appGreen2
}
.frame(height: 55)
}
}
.ignoresSafeArea()
// .onAppear {
// viewModel.fetchDashboardData { status in
// if status {
// stringArray = viewModel.dashboardButtonsArray
// selectedIndexStr = stringArray.first
// }
// }
// }
}
}