I have the following code:
#State private var couponId: Int = 0
var body: some View {
ScrollView {
VStack(spacing: -50) {
ForEach(1...20, id: \.self) { index in
CouponCell()
.zIndex(couponId == index ? 2 : 0)
.animation(.spring())
.onTapGesture {
withAnimation {
couponId = index
}
}
}
}
.padding().frame(maxWidth: .infinity, maxHeight: .infinity)
The the user selects a particular CouponCell then I update the Zindex so it appears on top of other cells. This works but it does not animate at all. What am I missing and why animation is not taking place.
I am using Xcode 12 Beta 2 on macOS Big Sur Beta 2
The .zIndex itself is not animatable modifier, because actually nothing to animate in view z position - it is either below or above.
Probably you meant (or would prefer) something combined like the following
Tested on replicated code (Xcode 12)
CouponCell()
.zIndex(couponId == index ? 2 : 0)
.scaleEffect(couponId == index ? 1.02 : 1)
.animation(.spring(), value: couponId)
.onTapGesture {
couponId = index
}
Related
I have a scrollview that holds "cards" with weather details of locations and the cards extend to show more information when tapped.
I have to use a LegacyScrollView so that the bottomsheet that encompasses the scrollview gets dragged down when the the scroll is at the top.
I can't figure out how to extend the VStack when one of the cards extend. Is there a way to make a GeometryReader or VStack recalculate the needed height to hold all of the views?
I start off with the minHeight set to the window size so when the 1st card is extended, it all shows, but 2 or more extend past the LazyVStack window and get cut off.
If the minHeight on the LaxyVStack isn't set to 0, the cells expand from the middle and their tops get cut off. When it's set to 0, the cells expand downward as desired.
GeometryReader { proxy in
LegacyScrollView(.vertical, showsIndicators: false) {
Spacer(minLength: 40)
LazyVStack(spacing: 0) {
ForEach(mapsViewModel.placedMarkers, id: \.id) { _placeObject1 in
InfoCell(placeObject: _placeObject1)
.environmentObject(mapsViewModel)
.frame(alignment: .top)
Divider().foregroundColor(.white)
}
}
.cornerRadius(25)
.frame(minHeight: 0, alignment: .top)
}
.onGestureShouldBegin{ pan, scrollView in
scrollView.contentOffset.y > 0 || pan.translation(in: scrollView).y < 0
}
.onScroll{ test in
print(test.contentOffset.y)
}
.padding(.top, 30) //padding for top of list
}
and the cell's code is :
struct InfoCell: View {
#StateObject var placeObject: PlaceInfo
#State var expandCell = false
var body: some View {
VStack(alignment: .center, spacing: 0) {
//blahblah
if expandCell {
CellExpanded(placeObject: placeObject, isFirstInList: true)
}
}
.frame(maxWidth: .infinity)
.onTapGesture {
withAnimation { expandCell.toggle() }
}
}
}
and the expand code:
struct InfoCellExpanded: View {
#State var forecast = "Daily"
var placeObject: PlaceInfo
var isFirstInList: Bool
var body: some View {
VStack {
//blah
}
.padding(.horizontal, 10)
.padding(.bottom, 20)
}
}
I have a LazyVGrid with a layout count: 2 when in portrait, and court: 3 when in landscape, in a scrollview. I use a ternary to change the count. Problem is when I scroll down than select a cell, when the model slides up and I rotate, the view dismisses by itself. I also notice the scroll seems to be in a totally different location.
Do I need to build this differently? Funny thing is it only happens at certain places down in the scrollview. Its not consistent. Sometimes it works fine then as I continue to scroll down it'll start to happen.
If I don't change the layout count in portrait or landscape, it works fine. It seems change the count causes this.
struct Feed_View: View {
#EnvironmentObject var viewModel : Post_View_Model
#Environment(\.verticalSizeClass) var sizeClass
#FocusState private var isFocused: Bool
var body: some View {
Color("BGColor").ignoresSafeArea()
ZStack {
VStack (alignment: .center, spacing: 0) {
//MARK: - NAVIGATION BAR
NavBar_View() // Top Navigation bar
.frame(maxHeight: 40, alignment: .center)
//MARK: - SCROLL VIEW
ScrollView (.vertical, showsIndicators: false) {
//MARK: - FEED FILL
let layout = Array(repeating: GridItem(.flexible(), spacing: 10), count: sizeClass == .compact ? 3 : 2)
LazyVGrid(columns: layout, spacing: 10) {
ForEach (viewModel.posts, id: \.self) { posts in
Feed_Cell(postModel: posts)
} //LOOP
} //LAZYV
.padding(.horizontal, 10).padding(.vertical, 10)
} //SCROLL
} //V
} //Z
}
}
In learning SwiftUI I'm trying to do transition animations I used with UIKit. I sometimes used pages with tables that changed with a parameter by using a containerView and transitioning between children. The SwiftUI equivalent seems to be a single List View dependent on the parameter. But it seems that .transitions don't work well since no view is being removed/inserted in the hierarchy (and simple animations don't give me the transition options I want). The only way I have made it work at all is by "manually" removing the old List and inserting the new one, but that seems a kludge and didn't work perfectly. Here's a toy version of the code that illustrated the problem -- the inserted green rectangle is just for comparison -- the transition works fine for it but not for the List.
struct ContentView: View {
let listItems:[[String]] = [["A0","B0"],["A1","B1"]]
#State var pointer:Int = 0
var body: some View {
VStack{
List(listItems[pointer], id:\.self)
{item in Text(item)
.foregroundColor(self.pointer == 0 ? Color.blue : Color.purple)
}
.transition(.offset(x: -600, y: 0))
if pointer == 1 {
RoundedRectangle(cornerRadius: 10)
.frame(width: 300, height: 200)
.foregroundColor(.green)
.transition(.offset(x: 500, y: 300))
}
}
.onTapGesture {
withAnimation(.easeInOut(duration: 1)){
self.pointer = (self.pointer + 1) % 2
}
}
}
}
I've found a partial solution, but it behaves strangely. Instead of a single List with varying content, transitions seem to require separate lists which are inserted/removed. Here is a simple implementation
struct ContentView: View {
let listItems:[[String]] = [["A0","B0"],["A1","B1"],["A2","B2"]]
#State var pointer:Int = 0
var body: some View {
ZStack{ForEach(0...2, id: \.self)
{index in
ZStack{
if index == self.pointer {
List(self.listItems[index], id:\.self)
{item in HStack{Text(item);Spacer()}
.foregroundColor(self.pointer == 0 ? Color.blue : Color.purple)
.frame(height:20)
.padding(2)
.background(Color.yellow)
}
.transition(.asymmetric(insertion: .offset(x: 400, y: -370), removal: .offset(x: 0, y: -870)))
}
}
}
}
.onTapGesture {
withAnimation(.easeInOut(duration: 1)){
self.pointer = (self.pointer + 1) % 3
}
}
But it responds oddly to choice of offsets -- I don't understand the -370 y-offset needed to make insertions come in horizontally -- it was found by trial/error. And the transition 2->0 is different from the other two.
I have a progress view whose progress / width can be animated. This animation works by itself. However, in some cases, the parent view changes its layout and a new view is added above the progress bar. Then, the progress view moves down to make space for the new view. This is also animated. When both animations happen in iOS 13, the progress view moves down and the width of the blue progress bar is animated at the same time. In iOS 14, the progress view moves down but the blue progress bar also uses its animation to animate the position change. In iOS 13 it was only used for animating the width. This makes the animation look as if the progress bar flies into the progress view and it is wrong and looks weird.
I reproduced the code I have in my app with the following code. This code was also used to record the video.
struct MainView: View {
#State var toggle = false
#State var trim: CGFloat = 0.2
var body: some View {
VStack {
Rectangle().frame(minHeight: 0, maxHeight: self.toggle ? 200 : 0)
ProgressBarView(trim: self.$trim)
Button(
action: {
withAnimation {
self.toggle.toggle()
self.trim = self.toggle ? 0.8 : 0.2
}
},
label: {
Text("Toggle")
})
}
}
}
struct ProgressBarView: View {
#State var grow: Bool = false
#Binding var trim: CGFloat
var body: some View {
ZStack(alignment: .leading) {
GeometryReader { geo in
Rectangle()
.opacity(0.1)
.zIndex(0)
Rectangle()
.frame(
minWidth: 0,
maxWidth: self.grow
? geo.frame(in: .global).width * self.trim
: 0
)
.animation(Animation.easeOut(duration: 1.2).delay(0.5))
.foregroundColor(Color(UIColor.systemBlue))
}
}
.cornerRadius(10)
.frame(height: 20)
.onAppear(perform: {
self.grow = true
})
}
}
Update
This is the original and working animation with the same code on my iPad that is still running iOS 13.7. I just removed the delay and increased the time to make the different animations more obvious.
Update 2
There was some confusion when answering the question so I copied some frames and put them into a screenshot. I hope this helps to understand my question. On the left is the animation on iOS 14 with the unwanted behavior. As you can see the blue progress bar doesn't appear in some images. In other images, it is only partly visible. On the right is the iOS 13 animation. The frames on the image (and all other frames) show the blue progress bar fully visible. It is always completely visible and on top of the background.
I don't understand what change in iOS 14 (or maybe Swift 5.3 or whatever else) caused the animation to be different and I cannot find a workaround to this problem.
Hey there! I made few changes in the ProgressBarView struct. Let's get to the less important changes first.
Removed the GeometryReader and replaced it with screen.width. If you are using the bar inside a container in order to get the width of the container, use GeometryReader.
Changed the Rectangle to a capsule.
To the important one
I target the animation modifier of the bar to the Boolean variable that trigged the change of the progress bar, i.e. grow
struct ProgressBarView: View {
#State var grow: Bool = false
#Binding var trim: CGFloat
let screen = UIScreen.main.bounds
var body: some View {
VStack {
ZStack(alignment: .leading) {
Capsule()
.foregroundColor(Color.black.opacity(0.1))
Capsule()
.frame(width: self.grow ? screen.width * trim : 0)
.foregroundColor(.blue)
.animation(Animation.easeOut(duration: 1).delay(0.5), value: self.grow)
}
.frame(width: screen.width, height: 20)
.onAppear {
self.grow = true
}
}
}
}
UPDATE 1.1
I added an animation to the ZStack for the initial increase of bar from 0 to 0.2
Changed the DispatchQueue delay to 0.18 sec.
The reason for the change in delay is because, since the expanding of the rectangle is changing the position of the bar in the y-direction, the animation effect with duration of 1.2 sec is being applied to the change in position which we don't want. Thus the delay of 0.18 starts the bar animation a bit after the expanding of the rectangle. This is a bit of a hack, but gets the work done. If I find a better solution, I'll update this answer with 'Update 2.0'. If you find a better solution, let me know.
Code
struct MainView: View {
#State var toggle = false
#State var trim: CGFloat = 0.2
var body: some View {
VStack {
Rectangle().frame(minHeight: 0, maxHeight: self.toggle ? 200 : 0)
ProgressBarView(trim: self.$trim)
Button(
action: {
withAnimation {
self.toggle.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.18) {
self.trim = self.toggle ? 0.8 : 0.2
}
}
},
label: {
Text("Toggle")
})
}
}
}
struct ProgressBarView: View {
#State var grow: Bool = false
#Binding var trim: CGFloat
let screen = UIScreen.main.bounds
var body: some View {
ZStack(alignment: .leading) {
Capsule()
.opacity(0.1)
Capsule()
.frame(width: self.grow ? screen.width * trim : 0)
.foregroundColor(.blue)
.animation(Animation.easeOut(duration: 1.2), value: self.trim)
}
.frame(width: screen.width, height: 20)
.onAppear {
self.grow = true
}
.animation(.easeOut(duration: 0.5), value: grow)
}
}
I hope this helps you.
In my app I have a ScrollView that holds a VStack. The VStack has an animation modifier. It also has one conditional view with a transition modifier. The transition works. However, when the view first appears, the content scales up with a starting width of 0. You can see this on the green border in the video below. This only happens if the VStack is inside the ScrollView or the VStack has the animation modifier.
I have tried different other things like setting a fixedSize or setting animation(nil) but I cannot get it to work.
I don't care if there is an animation at the beginning or if the view transitions onto the screen. But I definitely do not want the bad animation at the beginning. When I click on the button, the Text and the Button should both animate.
I also tested this behaviour with the following simplified code.
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
ScrollView {
VStack(spacing: 32) {
if self.viewModel.shouldShowText {
Text("Hello, World! This is a some text.")
.transition(
AnyTransition
.move(edge: .top)
.combined(
with:
.offset(
.init(width: 0, height: -100)
)
)
)
}
Button(
action: {
self.viewModel.didSelectButton()
},
label: {
Text("Button")
.font(.largeTitle)
}
)
}
.border(Color.green)
.animation(.easeInOut(duration: 5))
.border(Color.red)
HStack { Spacer() }
}
.border(Color.blue)
}
}
class ViewModel: ObservableObject {
#Published private var number: Int = 0
var shouldShowText: Bool {
return self.number % 2 == 0
}
func didSelectButton() {
self.number += 1
}
}
It works fine with Xcode 12 / iOS 14 (and stand-alone and in NavigationView), so I assume it is SwiftUI bug in previous versions.
Anyway, try to make animation be activated by value as shown below (tested & works)
// ... other code
}
.border(Color.green)
.animation(.easeInOut(duration: 5), value: viewModel.number) // << here !!
.border(Color.red)
and for this work it needs to make available published property for observation
class BlockViewModel: ObservableObject {
#Published var number: Int = 0 // << here !!
// ... other code