I have a test code here and I am trying to animate the count of my array, It is not working as I wanted, but I think I am close, here is my code:
It is about single TextEditor view animation in chain, not all together, let say we have 20 TextEditor, I want TextEditors from down number 20 start moving and after some move of 20 number 19 and 18, In fact all would start moving but a delay effeckt, same when they are coimg to view first TextEditor number 1 comes then with small delay number 2, chain animation, the most important factor is array count, because of that TextEditors would start moving inside view or outside. from right to left coming inside, from left to right going out.
import SwiftUI
struct ContentView: View {
@State private var arrayOfCustomType: [MyCustomType] = [MyCustomType]()
var body: some View {
VStack {
if (!arrayOfCustomType.isEmpty) {
ScrollView {
AnimatableForEach(arrayOfCustomType: $arrayOfCustomType)
}
.scrollIndicators(.visible)
} else {
Text("Please run the function!")
}
}
.frame(height: 500.0)
.padding()
.animation(.easeInOut(duration: 1), value: CGFloat(arrayOfCustomType.count))
Button("run") {
if (!arrayOfCustomType.isEmpty) {
arrayOfCustomType.removeAll()
}
else {
arrayOfCustomType = myFunc(string: myString, separatedBy: " ")
}
}
.padding()
}
}
func myFunc(string: String, separatedBy: String) -> [MyCustomType] {
let stringComponents: [String] = string.components(separatedBy: separatedBy)
var arrayOfCustomType: [MyCustomType] = [MyCustomType]()
for index in stringComponents.indices {
arrayOfCustomType.append(MyCustomType(intValue: index, stringValue: stringComponents[index]))
}
return arrayOfCustomType
}
let myString: String = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit.
"""
struct AnimatableForEach: View, Animatable {
@Binding var arrayOfCustomType: [MyCustomType]
var animatableData: CGFloat {
get { return CGFloat(arrayOfCustomType.count) }
set { }
}
var body: some View {
ForEach(arrayOfCustomType) { element in
TextEditor(text: Binding(
get: { arrayOfCustomType[element.intValue].stringValue },
set: { newValue in
arrayOfCustomType[element.intValue].stringValue = newValue
}
))
.font(.title)
.fontDesign(.monospaced)
.cornerRadius(5.0)
.padding(.trailing)
.transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing)))
}
}
}
struct MyCustomType: Identifiable, Animatable {
let id: UUID = UUID()
var intValue: Int
var stringValue: String
var animatableData: CGFloat {
get { CGFloat(intValue) }
set { intValue = Int(newValue) }
}
}
extension Array {
var animatableData: CGFloat {
get { return CGFloat(self.count) }
set { }
}
}
the idea of animation is measuring changes and I think i made it easy to SwiftUI to measuring, what is posibale way that this codes works?
Update 2: This code I think is much better, but still not working
import SwiftUI
struct ContentView: View {
@State private var arrayOfCustomType: [MyCustomType] = [MyCustomType]()
var body: some View {
VStack {
if (!arrayOfCustomType.isEmpty) {
ScrollView {
AnimatableForEach(arrayOfCustomType: $arrayOfCustomType)
}
.scrollIndicators(.visible)
}
else {
Text("Please run the function!")
}
}
.frame(height: 500.0)
.padding()
.animation(.easeInOut(duration: 1), value: CGFloat(arrayOfCustomType.count))
Button("run") {
if (!arrayOfCustomType.isEmpty) {
animationFunction(from: CGFloat(arrayOfCustomType.count), to: .zero, animation: .easeInOut(duration: 1), animatableData: { value in
if ((arrayOfCustomType.count - Int(value)) >= 0) {
arrayOfCustomType.removeLast()
}
})
}
else {
let myArray = myFunc(string: myString, separatedBy: " ")
animationFunction(from: CGFloat(arrayOfCustomType.count), to: .zero, animation: .easeInOut(duration: 1), animatableData: { value in
if ((arrayOfCustomType.count - Int(value)) >= 0) {
arrayOfCustomType.append(myArray[Int(value)])
}
})
}
}
.padding()
}
}
func myFunc(string: String, separatedBy: String) -> [MyCustomType] {
let stringComponents: [String] = string.components(separatedBy: separatedBy)
var arrayOfCustomType: [MyCustomType] = [MyCustomType]()
for index in stringComponents.indices {
arrayOfCustomType.append(MyCustomType(intValue: index, stringValue: stringComponents[index]))
}
return arrayOfCustomType
}
let myString: String = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit.
"""
struct AnimatableForEach: View {
@Binding var arrayOfCustomType: [MyCustomType]
var body: some View {
ForEach(arrayOfCustomType) { element in
TextEditor(text: Binding(
get: { arrayOfCustomType[element.intValue].stringValue },
set: { newValue in
arrayOfCustomType[element.intValue].stringValue = newValue
}
))
.font(.title)
.fontDesign(.monospaced)
.cornerRadius(5.0)
.padding(.trailing)
.transition(.asymmetric(insertion: .move(edge: .leading).combined(with: .opacity), removal: .move(edge: .trailing).combined(with: .opacity)))
}
}
}
struct MyCustomType: Identifiable {
let id: UUID = UUID()
var intValue: Int
var stringValue: String
}
func animationFunction(from: CGFloat, to: CGFloat, animation: Animation, animatableData: (CGFloat) -> Void ) {
// sending animatableData from to to view ...
animatableData(0.25)
}
This 2 animation is my goal, i am looking to that the count of arrays item do this, with adding items should the animation of enter happen, and with deleting items the animation of exit.
This is actually quite a difficult animation to implement. For the transitions to work on the individual rows, the container has to be visible, even when it is empty, then the rows must be added to the view one-by-one. For the removal transition to work, the items must be removed from the view one-by-one, so this is not as trivial as just replacing the array with an empty array.
I tried making the container view Animatable
, as you were doing in your first example, but couldn't get it to work this way. So I resorted to a timer approach. This answer by lorem ipsum gives a good overview of ways of implementing regular updates. I opted for the approach using task(id:priority:_:)
.
So here is an updated version of your example. It uses simple Text
elements, instead of TextEditor
.
struct ContentView: View {
@State private var arrayOfCustomType: [MyCustomType] = [MyCustomType]()
@State private var nRuns = 0
var body: some View {
VStack {
ScrollView {
AnimatableForEach(arrayOfCustomType: arrayOfCustomType)
}
.scrollIndicators(.visible)
if nRuns == 0 {
Text("Please run the function!")
}
Button(arrayOfCustomType.isEmpty ? "run" : "rewind") {
if arrayOfCustomType.isEmpty {
arrayOfCustomType = myFunc(string: myString, separatedBy: " ")
} else {
arrayOfCustomType.removeAll()
}
nRuns += 1
}
.buttonStyle(.bordered)
}
.frame(maxHeight: 500.0)
.padding()
}
private func myFunc(string: String, separatedBy: String) -> [MyCustomType] {
let stringComponents: [String] = string.components(separatedBy: separatedBy)
var arrayOfCustomType: [MyCustomType] = [MyCustomType]()
for index in stringComponents.indices {
arrayOfCustomType.append(MyCustomType(intValue: index, stringValue: stringComponents[index]))
}
return arrayOfCustomType
}
}
struct AnimatableForEach: View {
let arrayOfCustomType: [MyCustomType]
@State private var arrayToShow = [MyCustomType]()
@State private var nVisibleElements = 0
var body: some View {
VStack(spacing: 10) {
ForEach(arrayToShow.prefix(nVisibleElements)) { element in
Text(element.stringValue)
.font(.title)
.fontDesign(.monospaced)
.frame(maxWidth: .infinity)
.background {
RoundedRectangle(cornerRadius: 5)
.fill(.background.secondary)
}
.padding(.trailing)
.transition(
.move(edge: .trailing)
.combined(with: .opacity)
)
}
}
.animation(.default, value: nVisibleElements)
.onChange(of: arrayOfCustomType.count, initial: true) { oldVal, newVal in
if newVal > 0 {
arrayToShow = arrayOfCustomType
if nVisibleElements == 0 {
// Kick off the reveal animation
nVisibleElements = 1
} else if nVisibleElements > arrayOfCustomType.count {
nVisibleElements = arrayOfCustomType.count
}
} else if oldVal > 0 && !arrayToShow.isEmpty && nVisibleElements >= arrayToShow.count {
// Kick off the hide animation
nVisibleElements = arrayToShow.count - 1
}
}
.task(id: nVisibleElements) {
if !arrayToShow.isEmpty && nVisibleElements > 0 && nVisibleElements < arrayToShow.count {
// The array is partially visible, wait a short time
// before performing the next reveal/hide
try? await Task.sleep(for: .milliseconds(250))
if !arrayOfCustomType.isEmpty && !arrayToShow.isEmpty && nVisibleElements < arrayToShow.count {
// Reveal the next entry
nVisibleElements += 1
} else if arrayOfCustomType.isEmpty && !arrayToShow.isEmpty {
if nVisibleElements > 0 {
// Hide the next entry
nVisibleElements -= 1
} else {
// The animation has completed
arrayToShow = arrayOfCustomType
}
}
}
}
.frame(maxWidth: .infinity)
}
}
// MyCustomType, myString: see original example