I have an animation I made for a game totals list that saves the previous result and on the next time the view opens floats the users result view to the new position from the old ranking position. I built it with dummy data in a simple test app, but when I try to change the method to handle the data dynamically from a server response; the way it animates changes drastically.
How can I stop this from happening?
List View
import SwiftUI
struct ContentView: View {
@StateObject private var vm = DataHandler()
@State private var oldPositionX: CGFloat = 0
@State private var oldPositionY: CGFloat = 0
@State private var newPositionX: CGFloat = 0
@State private var newPositionY: CGFloat = 0
@State private var offsetValueX = 0.0
@State private var offsetValueY = 0.0
@State private var objectAtNewPosition: CompetitionResult? = nil
@State private var showNewData = false
@State private var isAnimating = false
var data: [CompetitionResult] {
if showNewData {
return vm.newData
}
return vm.oldData
}
var body: some View {
VStack {
ForEach(data) { item in
if !showNewData {
// OLD DATA
ListCell(item: item)
.overlay(
GeometryReader{ geo in
Color.clear
.onAppear {
setRanks(item: item, geo: geo)
}
}
)
.padding(.horizontal, isAnimating ? 8 : 0)
.padding(.vertical, isAnimating ? 2 : 0)
.background(item.uuid == vm.userUuid && isAnimating ? Color.blue : Color.clear)
.foregroundStyle(item.uuid == vm.userUuid ? Color.white : Color.black)
.clipShape(.rect(cornerRadius: item.uuid == vm.userUuid && isAnimating ? 8 : 0))
.offset(x: setXOffset(item), y: setYOffset(item))
.opacity(setOpacity(item))
} else {
// NEW DATA
ListCell(item: item)
}
}
}
.padding()
.onAppear() {
startAnimation()
}
}
}
// CELL
struct ListCell: View {
let item: CompetitionResult
var body: some View {
VStack {
HStack {
Text("(item.ranking)")
Text(item.name)
Spacer()
Text("(item.total)")
}
Divider()
}
}
}
View Methods
extension ContentView {
// START ---- Animation
private func startAnimation() {
isAnimating = true
DispatchQueue.main.asyncAfter(deadline: .now() + 4.5) {
withAnimation(.easeInOut(duration: 1.5)) {
showNewData = true
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation(.easeInOut(duration: 3.5)) {
offsetValueX = newPositionX - oldPositionX
offsetValueY = newPositionY - oldPositionY
isAnimating = false
}
}
}
// SET OPACITY ---
private func setOpacity(_ item: CompetitionResult) -> CGFloat {
withAnimation(.easeInOut) {
if let object = objectAtNewPosition {
if item.ranking == object.ranking && !isAnimating {
return 0
}
return 1
}
return 1
}
}
// SET OFFSETS ---
private func setXOffset(_ item: CompetitionResult) -> CGFloat {
if item.ranking == vm.oldRank {
return offsetValueX
}
return 0
}
private func setYOffset(_ item: CompetitionResult) -> CGFloat {
if item.ranking == vm.oldRank {
return offsetValueY
}
return 0
}
// SET RANKS ---
private func setRanks(item: CompetitionResult, geo: GeometryProxy) {
getOldRank(vm.oldRank, item: item, geo: geo)
getNewRank(vm.newRank, item: item, geo: geo)
}
private func getOldRank(_ oldRank: Int, item: CompetitionResult, geo: GeometryProxy) {
if item.ranking == oldRank {
oldPositionX = geo.frame(in: .global).midX
oldPositionY = geo.frame(in: .global).midY
}
}
private func getNewRank(_ newRank: Int, item: CompetitionResult, geo: GeometryProxy) {
if item.ranking == newRank {
newPositionX = geo.frame(in: .global).midX
newPositionY = geo.frame(in: .global).midY
self.objectAtNewPosition = item
}
}
}
Model & View Model
import SwiftUI
public struct CompetitionResult: Identifiable, Codable, Hashable, Equatable {
public var id: String { uuid }
public let uuid: String
public let name: String
public var total: Float
public var ranking: Int
public let date: String
public init(uuid: String, name: String, total: Float, ranking: Int, date: String) {
self.uuid = uuid
self.name = name
self.total = total
self.ranking = ranking
self.date = date
}
}
class DataHandler: ObservableObject {
let userUuid: String
@Published var oldData: [CompetitionResult] = []
@Published var newData: [CompetitionResult] = []
@Published var oldRank = 7
@Published var newRank = 3
init(uuid: String = "SET_TO_USER_UUID") {
self.userUuid = uuid
setUpAnimation()
}
func setUpAnimation() {
if let pastResult = getOldRank() {
let results = setResultData(pastResult, results: getData())
self.oldRank = pastResult.ranking
self.newRank = results.2
self.oldData = results.0
self.newData = results.1
}
}
// Need to save rank on get results fetch, then next time get previous result/rank from User Defaults, set old data with old result / remove old result from old data. New Data will be current fetch. SET old rank and new rank properties
private func setResultData(_ previousResult: CompetitionResult, results: [CompetitionResult]) -> ([CompetitionResult], [CompetitionResult], Int) {
//MARK: --- ISSUE ----
//MARK: IF I use the below code the animation style changes
// var oldResults = results
// oldResults.removeAll(where: {$0.uuid == previousResult.uuid})
// oldResults.insert(previousResult, at: previousResult.ranking - 1)
//MARK: THIS WORKS
let oldResults: [CompetitionResult] = [
CompetitionResult(uuid: "1", name: "One", total: 100, ranking: 1, date: ""),
CompetitionResult(uuid: "2", name: "Two", total: 99, ranking: 2, date: ""),
CompetitionResult(uuid: "3", name: "Three", total: 98, ranking: 3, date: ""),
CompetitionResult(uuid: "4", name: "Four", total: 97, ranking: 4, date: ""),
CompetitionResult(uuid: "5", name: "Five", total: 96, ranking: 5, date: ""),
CompetitionResult(uuid: "6", name: "Six", total: 95, ranking: 6, date: ""),
CompetitionResult(uuid: "SET_TO_USER_UUID", name: "Seven", total: 94, ranking: 7, date: ""), // OLD Rank
CompetitionResult(uuid: "8", name: "Eight", total: 93, ranking: 8, date: ""),
CompetitionResult(uuid: "9", name: "Nine", total: 92, ranking: 9, date: ""),
CompetitionResult(uuid: "10", name: "Ten", total: 91, ranking: 10, date: ""),
]
print(oldResults)
// (Old Data, New Data, Current Rank
return (oldResults, results, results.filter {$0.uuid == previousResult.uuid}.first?.ranking ?? 0)
}
// GET OLD Rank from User Defaults
private func getOldRank() -> CompetitionResult? {
CompetitionResult(uuid: "SET_TO_USER_UUID", name: "Seven", total: 94, ranking: 7, date: "") // OLD Rank
}
private func getData() -> [CompetitionResult] {
let data: [CompetitionResult] = [
CompetitionResult(uuid: "1", name: "One", total: 100, ranking: 1, date: ""),
CompetitionResult(uuid: "2", name: "Two", total: 99, ranking: 2, date: ""),
CompetitionResult(uuid: "SET_TO_USER_UUID", name: "Seven", total: 94, ranking: 3, date: ""), // NEW Rank - Current Rank
CompetitionResult(uuid: "3", name: "Three", total: 98, ranking: 4, date: ""),
CompetitionResult(uuid: "4", name: "Four", total: 97, ranking: 5, date: ""),
CompetitionResult(uuid: "5", name: "Five", total: 96, ranking: 6, date: ""),
CompetitionResult(uuid: "6", name: "Six", total: 95, ranking: 7, date: ""),
CompetitionResult(uuid: "8", name: "Eight", total: 93, ranking: 8, date: ""),
CompetitionResult(uuid: "9", name: "Nine", total: 92, ranking: 9, date: ""),
CompetitionResult(uuid: "10", name: "Ten", total: 91, ranking: 10, date: ""),
]
return data
}
}
The issue is when I change how the data is mutated in this method:
private func setResultData(_ previousResult: CompetitionResult, results: [CompetitionResult]) -> ([CompetitionResult], [CompetitionResult], Int) {
//MARK: --- ISSUE ----
//MARK: IF I use the below code the animation style changes
// var oldResults = results
// oldResults.removeAll(where: {$0.uuid == previousResult.uuid})
// oldResults.insert(previousResult, at: previousResult.ranking - 1)
//MARK: THIS WORKS
let oldResults: [CompetitionResult] = [
CompetitionResult(uuid: "1", name: "One", total: 100, ranking: 1, date: ""),
CompetitionResult(uuid: "2", name: "Two", total: 99, ranking: 2, date: ""),
CompetitionResult(uuid: "3", name: "Three", total: 98, ranking: 3, date: ""),
CompetitionResult(uuid: "4", name: "Four", total: 97, ranking: 4, date: ""),
CompetitionResult(uuid: "5", name: "Five", total: 96, ranking: 5, date: ""),
CompetitionResult(uuid: "6", name: "Six", total: 95, ranking: 6, date: ""),
CompetitionResult(uuid: "SET_TO_USER_UUID", name: "Seven", total: 94, ranking: 7, date: ""), // OLD Rank
CompetitionResult(uuid: "8", name: "Eight", total: 93, ranking: 8, date: ""),
CompetitionResult(uuid: "9", name: "Nine", total: 92, ranking: 9, date: ""),
CompetitionResult(uuid: "10", name: "Ten", total: 91, ranking: 10, date: ""),
]
print(oldResults)
// (Old Data, New Data, Current Rank
return (oldResults, results, results.filter {$0.uuid == previousResult.uuid}.first?.ranking ?? 0)
}
Working animation
NOT Working animation