0

I've been banging my head against some strange (or not) SwiftUI .zIndex animation issue: I can't see how to animate the .zIndex View parameter of a random view (as I seem to be able to do it with .scaleEffect, .position, .rotation, etc...)

Is it a known fact I shouldn't be banging my head against?

I simplified some real life project code of mine I'm currently dealing with and created this Proof of Concept project, which is much smaller, for everyone to check:

https://github.com/friguron/swiftui_zindex_animation_test/tree/develop

The project is really unambitious (as it should be), it consists of some ContentView, some ViewModel attached to it and some struct/Model I need.

(TIP: for better views alignment on screen (it was never the main aim of the project anyway), I recommend using ios 15.5 iPhone 13ProMax iPhone Simulator. Activating "Slow Animation" mode in the simulator also helps a lot to understand my doubt)

Let's describe the demo:

-There are 9 pieces on screen, all siblings, painted via LazyVGrid.

-Every one of the 9 screen pieces has some connection to a model in memory. Inside of every piece model there are 2 booleans: superRaised, and raised (their meaning is: depending on their state they should trig different zIndex heights of the Views they're logically connected with).

There are also two buttons which trigger the 2 states in the study:

Button 1 interchanges the actual phisical view center positions of pieces 0 and 8 of my memory array (of 9 pieces). Pieces gracefully move from origin to destination with no problem, getting fantastically smooth animation (.position works as expected). It also sets some boolean flags to true, to get additional visual raised effect on screen.

Button 2 does the same thing but the other way back, that is: resetting the flags status and position of the pieces, so at the end of this animation the pieces will appear exactly in the same position as they were originally (in position, and in size/raised-ness)

Fancy additional visual stuff I try to perform (without success): I try to change/animate the .zindex of the affected moving pieces, so when I set the the "superraised" (boolean) attribute, I connect this with some View modifier to give the Piece/View a .zIndex "height" of 100 in the screen "virtual ZStack" (To make it "hover" the rest of its siblings)

 LazyVGrid(columns: vm.columns, content:
        {
            ForEach ($vm.arrayPieces) { $tmpPiece in
                Rectangle()
                    .fill(tmpPiece.color)
                    .frame(minWidth: tmpPiece.sizeLength , minHeight: tmpPiece.sizeLength)
                    .shadow(color: .black, radius: 5)
                    .scaleEffect(tmpPiece.superRaised ?  1.3 : tmpPiece.raised ? 1.2 : 1.0 )
                    .position(x:tmpPiece.baseX+tmpPiece.sizeLength/2,y:tmpPiece.baseY+tmpPiece.sizeLength/2)
                    .zIndex(tmpPiece.superRaised ? 100.0 : tmpPiece.raised ? 90.0 : 0.0 )
            }

        })
        
        Button(action: {
            withAnimation {
                vm.arrayPieces[0].baseX = 290.0
                vm.arrayPieces[0].baseY = 216.0
                
                vm.arrayPieces[8].baseX = -290.0
                vm.arrayPieces[8].baseY = -216.0
                
                vm.arrayPieces[0].superRaised = true
                vm.arrayPieces[0].raised = false
                
                vm.arrayPieces[8].superRaised = false
                vm.arrayPieces[8].raised = true
            }
        }, label: {
            Text("animate flags and positions of red and grey")
        })
        
        Button(action: {
            withAnimation {
                vm.arrayPieces[0].baseX = 0.0
                vm.arrayPieces[0].baseY = 0.0
                
                vm.arrayPieces[8].baseX = 0.0
                vm.arrayPieces[8].baseY = 0.0
                
                vm.arrayPieces[0].superRaised = false
                vm.arrayPieces[0].raised = false
                
                vm.arrayPieces[8].superRaised = false
                vm.arrayPieces[8].raised = false
            }
        }, label: {
            Text("animate resetting back flags and positions of red and grey")
        })

Same with the .raised attribute, when a pieces has this boolean set to true, it should paint its View zIndex "height" as "90", really on top of every other piece except the first one (100), and way above the default 0 value

That means that after clicking the First button and after waiting for the animation to end, both pieces seem to be visually "floating" on top of their siblings (very much expected and wanted behaviour).

The problem comes where I press Button number 2, to reset the visual position centers to 0.0 and both booleans (which control zIndex), to false. What's happening here? zIndex of the red piece (Array[0]), seems to be given zIndex = 0 from the very beginning of step 2 animation, immediately, so when said red piece returns to its original position, it moves passing (unwantedly) behind all the rest of the pieces on the board (Because as theory says and I read somewhere: "in case of a tie between .zindex values amongst Views, the views on the screen will be painted in sequential order". That means that as red piece is the frist one to be painted (in the Foreach), and it "shares" a zIndex value of 0 with the rest of its siblings, it will be treated as the most "background one".

That behaviour would be ok if I was giving zIndex a value of 0 without putting the booleans inside the withAnimation block. But when this background moving glitch happens on screen (after just pressing Button 2), it seems that zIndex (which I want to slowly animate from 100 to 0), is changing abruptly to zIndex = 0 just when the return animation starts (it doesn't move from 100.0 to 0.00 gradually).

Is it a known fact? Any hint regarding what to do to achieve this alleged zindex animated value capability of swiftui (if possible?) Am I doing some obvious wrong thing and I'm not aware of it? (I'm not a complete newbie of SwiftUI, but I still have many doubts here and there)

If some of the descriptions are unclear (they probably are) I'll gladly add the missing/unclear info to the question.


EDIT

NOTE1: I've also tried to animate the actual zIndex Double value directly (also stored into the Piece model) instead of having it linked to a boolean toggle status change. What happens? Apparently the same, not a single difference. For some reason, .zIndex render seems to be "non-animatable".

NOTE2: I also tried some ingenious solution explained in this article: https://medium.com/geekculture/swiftui-animation-completion-b6f0d167159e I managed to create some "animate + completion block" solution to be able to transform my current "withAnimation blocks" into 2 consecutive ones via the helper explained in the aforementioned article. What happened? Apparently zIndex seemed to be working as I needed! But then shady references about main thread problems arised, (as well as some FPS performance drop). I tried to fix it, but then I lost the actual animations (they turned into abrupt changes). And then, still worse, trying to move a piece with this new technique, before it had finished the actual movement of the animation itself, lead to a SUPER DEADLOCK on screen. Conclusion: I sadly had to discard it completely (I couldn't manage to modify it to my needs).


Greetings

Isaac
  • 1,794
  • 4
  • 21
  • 41

0 Answers0