RectangleView has a slide animation, his child TextView has a rotation animation. I suppose that RectangleView with his child(TextView) as a whole slide(easeInOut) into screen when Go! pressed, and TextView rotate(linear) forever. But in fact, the child TextView separates from his parent, rotating(linear) and sliding(linear), and repeats forever.
why animation applied to parent effect child animation?
struct AnimationTestView: View {
#State private var go = false
var body: some View {
VStack {
Button("Go!") {
go.toggle()
}
if go {
RectangleView()
.transition(.slide)
.animation(.easeInOut)
}
}.navigationTitle("Animation Test")
}
}
struct RectangleView: View {
var body: some View {
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(.pink)
.overlay(TextView())
}
}
struct TextView: View {
#State private var animationRotating: Bool = false
let animation = Animation.linear(duration: 3.0).repeatForever(autoreverses: false)
var body: some View {
Text("Test")
.foregroundColor(.blue)
.rotationEffect(.degrees(animationRotating ? 360 : 0))
.animation(animation)
.onAppear { animationRotating = true }
.onDisappear { animationRotating = false }
}
}
If there are several simultaneous animations the generic solution (in majority of cases) is to use explicit state value for each.
So here is a corrected code (tested with Xcode 12.1 / iOS 14.1, use Simulator or Device, Preview renders some transitions incorrectly)
struct AnimationTestView: View {
#State private var go = false
var body: some View {
VStack {
Button("Go!") {
go.toggle()
}
VStack { // container needed for correct transition !!
if go {
RectangleView()
.transition(.slide)
}
}.animation(.easeInOut, value: go) // << here !!
}.navigationTitle("Animation Test")
}
}
struct RectangleView: View {
var body: some View {
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(.pink)
.overlay(TextView())
}
}
struct TextView: View {
#State private var animationRotating: Bool = false
let animation = Animation.linear(duration: 3.0).repeatForever(autoreverses: false)
var body: some View {
Text("Test")
.foregroundColor(.blue)
.rotationEffect(.degrees(animationRotating ? 360 : 0))
.animation(animation, value: animationRotating) // << here !!
.onAppear { animationRotating = true }
.onDisappear { animationRotating = false }
}
}
PREVIOUS Q: swiftui, animation applied to parent effect child animation
Now the TextView has its own state. RectangleView with TextView slide into screen in 3 seconds, but state of TextView changed after a second while sliding. Now you can see TextView stops sliding, go to the destination at once. I Just want RectangleView with TextView as a whole when sliding, and TextView rotates or keeps still as I want.
import SwiftUI
struct RotationEnvironmentKey: EnvironmentKey {
static let defaultValue: Bool = false
}
extension EnvironmentValues {
var rotation: Bool {
get { return self[RotationEnvironmentKey] }
set { self[RotationEnvironmentKey] = newValue }
}
}
struct AnimationTestView: View {
#State private var go = false
#State private var rotation = false
var body: some View {
VStack {
Button("Go!") {
go.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
rotation.toggle()
print("set rotation = \(rotation)")
}
}
Group {
if go {
RectangleView()
.transition(.slide)
.environment(\.rotation, rotation)
}
}.animation(.easeInOut(duration: 3.0), value: go)
}.navigationTitle("Animation Test")
}
}
struct RectangleView: View {
var body: some View {
Rectangle()
.frame(width: 200, height: 100)
.foregroundColor(.pink)
.overlay(TextView())
}
}
struct TextView: View {
#Environment(\.rotation) var rotation
#State private var animationRotating: Bool = false
let animation = Animation.linear(duration: 3.0).repeatForever(autoreverses: false)
var body: some View {
print("refresh, rotation = \(rotation)"); return
HStack {
Spacer()
if rotation {
Text("R")
.foregroundColor(.blue)
.rotationEffect(.degrees(animationRotating ? 360 : 0))
.animation(animation, value: animationRotating)
.onAppear { animationRotating = true }
.onDisappear { animationRotating = false }
} else {
Text("S")
}
}
}
}
I found the issue here: cannot use if cause in TextView, because when rotation changed, will create a new Text in new position, so we can see the R 'jump' to the right position.
code as following:
Text(rotation ? "R" : "S")
.foregroundColor(rotation ? .blue : .white)
.rotationEffect(.degrees(animationRotating ? 360 : 0))
.animation(rotation ? animation : nil, value: animationRotating)
.onChange(of: rotation) { newValue in
animationRotating = true
}
Edit: With help of Asperi I decided to rewrite the description to better clarify the question with easy copy+paste code.
Expected behavior on all tests: The red rectangle will animate it's size from zero to the size of the parent view when the Present button in the top right corner is tapped. When Present is tapped again, the red rectangle will shrink from the size of the parent view to zero.
TEST #1 PROPERTY STATE CHANGE
Actual behavior:
Works as expected.
Code:
struct ContentView: View {
#State private var presentRedBox = false
var body: some View {
NavigationView {
GeometryReader { proxy in
ZStack {
// ------
Rectangle().fill(Color.red)
.frame(
width: self.presentRedBox ? proxy.size.width : 0.0,
height: self.presentRedBox ? proxy.size.height : 0.0
)
// ------
}
}.animation(.default)
.navigationBarItems(trailing: Button("Present") { self.presentRedBox.toggle() })
.navigationBarTitle(Text(""), displayMode: .inline)
}
}
}
TEST #2 ANIMATABLE/VIEW MODIFIER USING PROPERTY STATE CHANGE
Actual behavior:
Works as expected.
Code:
extension AnyTransition {
static func sizeTransition(from: CGSize, to: CGSize) -> AnyTransition {
.modifier(
active: SizeTransition(size: from),
identity: SizeTransition(size: to)
)
}
}
struct SizeTransition: AnimatableModifier {
var size: CGSize
var animatableData: AnimatablePair<cgfloat, cgfloat=""> {
get { AnimatablePair(size.width, size.height) }
set {
size.width = newValue.first
size.height = newValue.second
}
}
func body(content: Content) -> some View {
print(size)
return content.frame(
width: size.width,
height: size.height
)
}
}
struct ContentView: View {
#State private var presentRedBox = false
var body: some View {
NavigationView {
GeometryReader { proxy in
ZStack {
// ------
Rectangle().fill(Color.red)
.modifier(
SizeTransition(
size: self.presentRedBox ? proxy.size : .zero
)
)
// ------
}
}.animation(.default)
.navigationBarItems(trailing: Button("Present") { self.presentRedBox.toggle() })
.navigationBarTitle(Text(""), displayMode: .inline)
}
}
}
TEST #3 ANIMATABLE/VIEW MODIFIER WITH TRANSITION
Actual behavior:
The red rectanble will animate in as expected. However(!) it will NOT animate out but disappear immediately, although the log shows the correct values.
Log Animating In
(0.0, 0.0)
(1.8118343353271484, 3.3873424530029297)
(7.392631530761719, 13.821006774902344)
(16.9350643157959, 31.66120719909668)
(30.5800838470459, 57.17146110534668)
(48.38059616088867, 90.45067977905273)
(70.25803184509277, 131.35197257995605)
(95.95654678344727, 179.39702224731445)
(124.99998664855957, 233.6956272125244)
(156.67254066467285, 292.90953254699707)
(190.03098106384277, 355.27531242370605)
(223.97296714782715, 418.73206901550293)
(257.33140754699707, 481.0978488922119)
(289.00356674194336, 540.3110160827637)
(318.04700660705566, 594.6096210479736)
(343.7447319030762, 642.6531944274902)
(365.6217727661133, 683.5537490844727)
(383.42189025878906, 716.8322296142578)
(397.06651496887207, 742.3417453765869)
(406.60855293273926, 760.1812076568604)
(412.18856048583984, 770.613395690918)
(414.0, 774.0)
Log Animating Out
(413.61268043518066, 773.2758808135986)
(410.07547760009766, 766.6628494262695)
(402.6749496459961, 752.8270797729492)
(391.2381649017334, 731.4452648162842)
(375.6612854003906, 702.3232727050781)
(355.94628524780273, 665.4647941589355)
(332.24832916259766, 621.1599197387695)
(304.9215717315674, 570.070764541626)
(274.5523223876953, 513.2934722900391)
(241.9665470123291, 452.3722400665283)
(208.19354438781738, 389.231409072876)
(174.37908554077148, 326.0130729675293)
(141.67486381530762, 264.870397567749)
(111.12004852294922, 207.74617767333984)
(83.55758285522461, 156.21635055541992)
(59.59075355529785, 111.40880012512207)
(39.58871841430664, 74.01369094848633)
(23.71967124938965, 44.34547233581543)
(11.994667053222656, 22.42481231689453)
(4.315790176391602, 8.06865119934082)
(0.5136623382568359, 0.9603252410888672)
(0.0, 0.0)
Code:
extension AnyTransition {
static func sizeTransition(from: CGSize, to: CGSize) -> AnyTransition {
.modifier(
active: SizeTransition(size: from),
identity: SizeTransition(size: to)
)
}
}
struct SizeTransition: AnimatableModifier {
var size: CGSize
var animatableData: AnimatablePair<cgfloat, cgfloat=""> {
get { AnimatablePair(size.width, size.height) }
set {
size.width = newValue.first
size.height = newValue.second
}
}
func body(content: Content) -> some View {
print(size)
return content.frame(
width: size.width,
height: size.height
)
}
}
struct ContentView: View {
#State private var presentRedBox = false
var body: some View {
NavigationView {
GeometryReader { proxy in
ZStack {
// ------
if self.presentRedBox {
Rectangle().fill(Color.red)
.transition(
.modifier(
active: SizeTransition(size: .zero),
identity: SizeTransition(size: proxy.size)
)
)
}
// ------
}
}.animation(.default)
.navigationBarItems(trailing: Button("Present") { self.presentRedBox.toggle() })
.navigationBarTitle(Text(""), displayMode: .inline)
}
}
}
TEST #4 ANIMATABLE/VIEW MODIFIER WITH TRANSITION FOR OPACITY
Expected behavior:
The red rectangle will animate it's opacity from zero (hidden) to one (visible) when the Present button in the top right corner is tapped. When Present is tapped again, the red rectangle will hide from one (visible) to zero (hidden).
Actual behavior:
Works as expected.
Code:
extension AnyTransition {
static func sizeTransition(from: CGSize, to: CGSize) -> AnyTransition {
.modifier(
active: SizeTransition(size: from),
identity: SizeTransition(size: to)
)
}
}
struct SizeTransition: AnimatableModifier {
var size: CGSize
var animatableData: AnimatablePair<CGFloat, CGFloat> {
get { AnimatablePair(size.width, size.height) }
set {
size.width = newValue.first
size.height = newValue.second
}
}
func body(content: Content) -> some View {
print(size)
return content.opacity(Double(size.width))
}
}
struct ContentView: View {
#State private var presentRedBox = false
var body: some View {
NavigationView {
GeometryReader { proxy in
ZStack {
// ------
if self.presentRedBox {
Rectangle().fill(Color.red)
.transition(
.modifier(
active: SizeTransition(size: .zero),
identity: SizeTransition(size: CGSize(width: 1.0, height: 1.0))
)
)
}
// ------
}
}.animation(.default)
.navigationBarItems(trailing: Button("Present") { self.presentRedBox.toggle() })
.navigationBarTitle(Text(""), displayMode: .inline)
}
}
}
It is not exactly a solution for what you requested, I know, but under some circumstances can be useful, so I decided to post it.
Note: I started with your previous post code, so might be a bit not aligned with this post.
At first proposed alternate - only animation based.
struct TestReverseTransitions: View {
#State private var showRedBox = false
var body: some View {
VStack {
Button("Tap") { self.showRedBox.toggle() }
RedBox()
.modifier(SizeAnimation(size: showRedBox ?
CGSize(width: 200.0, height: 200.0) : .zero))
}.animation(.default)
}
}
The animatable modifier is used the same from investigation provided below.
Transitions investigation:
Actually I think this might be a bug, but can be a transition engine limitation, because transitions based on effects, but here is just change of physical view frame, while view is in fact already removed. So 50/50... maybe worth reporting feedback to Apple.
Here is why...
I use animatable modifier to make frame change explicitly via animatable data and as it seen on Demo2 debug log, frame is really animatable on box removed, but appearance is not, however button is moved as it should. Bug? Maybe.
Code for this case:
extension AnyTransition {
static func size(from: CGSize, to: CGSize) -> AnyTransition {
AnyTransition.modifier(
active: SizeAnimation(size: from),
identity: SizeAnimation(size: to)
)
}
}
struct SizeAnimation: AnimatableModifier {
var size: CGSize
var animatableData: AnimatablePair<CGFloat, CGFloat> {
get { AnimatablePair(size.width, size.height) }
set {
size.width = newValue.first
size.height = newValue.second
}
}
func body(content: Content) -> some View {
// print(size) // << uncomment for log sizes !!!
return content.frame(width: size.width, height: size.height)
}
}
struct TestReverseTransitions: View {
#State private var showRedBox = false
var body: some View {
VStack {
Button("Tap") { self.showRedBox.toggle() }
if self.showRedBox {
RedBox()
.transition(
.size(from: CGSize.zero, to: CGSize(width: 200.0, height: 200.0))
)
}
}.animation(.default)
}
}
struct RedBox: View {
var body: some View {
Rectangle().fill(Color.red)
}
}
i have a fullscreen list of element.
VStack{
List {
ForEach(books, id: \.id) { book in
Text(book.title)
.background(Color.yellow) // text background
.listRowBackground(Color.blue) // cell background
}
.frame(height: UIScreen.main.bounds.height)
}
}
.background(Color.red)
.edgesIgnoringSafeArea(.all)
Is possible to snap every cell on top when scrolling? I have no idea how to do this.
Thank you!
You can use DragGesture and ScrollViewReader to create a snapping List in SwiftUI.
We are calculating the velocity to move the list.
Here is a full sample Code:
struct SampleSnappingListView: View {
enum ScrollDirection {
case up
case down
case none
}
#State var scrollDirection: ScrollDirection = .none
var body: some View {
VStack {
ScrollViewReader { reader in
List {
ForEach(0...20, id:\.self) { index in
ZStack {
Color((index % 2 == 0) ? .red : .green)
VStack {
Text("Index \(index)")
.font(.title)
}
.frame(height: UIScreen.main.bounds.height/2)
}
.clipShape(Rectangle())
.id(index)
.simultaneousGesture(
DragGesture(minimumDistance: 0.0)
.onChanged({ dragValue in
let isScrollDown = 0 > dragValue.translation.height
self.scrollDirection = isScrollDown ? .down : .up
})
.onEnded { value in
let velocity = CGSize(
width: value.predictedEndLocation.x - value.location.x,
height: value.predictedEndLocation.y - value.location.y
)
if abs(velocity.height) > 100 {
withAnimation(.easeOut(duration: 0.5)) {
let next = index + (scrollDirection == .down ? 1 : -1)
reader.scrollTo(next, anchor: .top)
}
}
}
)
}
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
.listStyle(.plain)
}
}
}
}
struct SampleSnappingListView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
SampleSnappingListView()
.navigationTitle("Snapping list")
.navigationBarTitleDisplayMode(.inline)
}
}
}
I want to align the circle to the TileView "1"'s center at top left. Is there any other like centre constraint of UIView?
struct BoardView: View {
var body: some View {
ZStack {
VStack {
HStack {
TileView(number: 1)
TileView(number: 2)
}
HStack {
TileView(number: 3)
TileView(number: 4)
}
}
Circle()
.frame(width: 30, height: 30)
.foregroundColor(Color.red.opacity(0.8))
}
}
}
Here is possible approach using custom alignment guides
extension VerticalAlignment {
private enum VCenterAlignment: AlignmentID {
static func defaultValue(in dimensions: ViewDimensions) -> CGFloat {
return dimensions[VerticalAlignment.center]
}
}
static let vCenterred = VerticalAlignment(VCenterAlignment.self)
}
extension HorizontalAlignment {
private enum HCenterAlignment: AlignmentID {
static func defaultValue(in dimensions: ViewDimensions) -> CGFloat {
return dimensions[HorizontalAlignment.center]
}
}
static let hCenterred = HorizontalAlignment(HCenterAlignment.self)
}
struct BoardView: View {
var body: some View {
ZStack(alignment: Alignment(horizontal: .hCenterred, vertical: .vCenterred)) {
VStack {
HStack {
TileView(number: 1)
.alignmentGuide(.vCenterred) { $0[VerticalAlignment.center] }
.alignmentGuide(.hCenterred) { $0[HorizontalAlignment.center] }
TileView(number: 2)
}
HStack {
TileView(number: 3)
TileView(number: 4)
}
}
Circle()
.frame(width: 30, height: 30)
.foregroundColor(Color.red.opacity(0.8))
.alignmentGuide(.vCenterred) { $0[VerticalAlignment.center] }
.alignmentGuide(.hCenterred) { $0[HorizontalAlignment.center] }
}
}
}
Test module on GitHub
Using GeometryReader and preference
GeometryReader - let us to get geometryProxy of a view ( according to the parent view / space provide by parent )
Note:- child view stays at their own (parent cant force them position or size)
.preference(key:,value:) - this modifier let us get some values( geometryProxy ) out of the view. so that we can access child's geometry proxy from parent as well.
comment below for any question about these topics.
here is the code,
(plus point - you can animate your circle's position easily.(by adding .animation() modifier. Ex- if you want your circle move when user touch on a title)
complete code
import SwiftUI
struct ContentView: View {
#State var centerCoordinateOfRectangele: MyPreferenceData = MyPreferenceData(x: 0, y: 0)
var body: some View {
ZStack {
VStack {
HStack {
GeometryReader { (geometry) in
TileView(number: 1)
.preference(key: MyPreferenceKey.self, value: MyPreferenceData(x: geometry.frame(in: .named("spaceIWantToMoveMyView")).midX, y: geometry.frame(in: .named("spaceIWantToMoveMyView")).midY))
}
TileView(number: 2)
}.onPreferenceChange(MyPreferenceKey.self) { (value) in
self.centerCoordinateOfRectangele = value
}
HStack {
TileView(number: 3)
TileView(number: 4)
}
}
Circle()
.frame(width: 30, height: 30)
.foregroundColor(Color.red.opacity(0.8))
.position(x: centerCoordinateOfRectangele.x, y: centerCoordinateOfRectangele.y)
}
.coordinateSpace(name: "spaceIWantToMoveMyView") //to get values relative to this layout
// watch where i have use this name
}
}
struct TileView: View{
let number: Int
var body: some View{
GeometryReader { (geometry) in
RoundedRectangle(cornerRadius: 30)
}
}
}
struct MyPreferenceData:Equatable {
let x: CGFloat
let y: CGFloat
}
struct MyPreferenceKey: PreferenceKey {
typealias Value = MyPreferenceData
static var defaultValue: MyPreferenceData = MyPreferenceData(x: 0, y: 0)
static func reduce(value: inout MyPreferenceData, nextValue: () -> MyPreferenceData) {
value = nextValue()
}
}
Code explanation ->
data model which I want to grab from the child view
struct MyPreferenceData:Equatable {
let x: CGFloat
let y: CGFloat
}
Key
struct MyPreferenceKey: PreferenceKey {
typealias Value = MyPreferenceData
static var defaultValue: MyPreferenceData = MyPreferenceData(x: 0, y: 0)
static func reduce(value: inout MyPreferenceData, nextValue: () -> MyPreferenceData) {
value = nextValue()
}
}
defaultValue - SwiftUI use this, when there are no explicit value set
reduce() - getCalled when SwiftUI want to give values (we can use .preference() modifier on many views with the same key)
we add the .preference() modifier which we want to get values