Today I was scrolling Tumblr and found that there is an alert that appears when you choose to hide a post. It’s a small green rounded rectangle that pops up in at the bottom of the screen with text that says Done!
I decided to continue scrolling, but I accidentally touched the alert and dragged it. This immediately made it clear that the alert can be dragged for as long as your finger maintains contact with the screen, and also that the alert shot off the side of the screen in whatever direction I had moved it.
There was also some animation going on.
\When the alert is being dragged, the rotation changes in an odd way, kind of like it’s facing the direction it’s going to be dismissed in. When the alert is let go it flies away at a constant rate, rotating as it does so. That’s another separate rotation animation that causes it to spin fast instead of turning to look in a specific direction.
I’m going to start by extending some types Apple’s CoreGraphics
framework.
I’m making it easier to get degrees from a CGFloat
that is representing an angle in radians. Then I’m allowing a CGPoint
to be created using a CGSize
, since the drag translation I will be working with will be given as a CGSize
. Finally I have a function that will calculate the angle between two points, which will be useful when I’m trying to decide what angle to rotate my alert to.
Now I can create ContentView
, but without conforming to the View
protocol just yet.
This allows me to do a bunch of work before I add the body
property that contains the alert.
First of all I have my dragOffset
which uses the @GestureState
property wrapper. This stores the state of the drag gesture, but I can’t use this directly as the position of the alert. After all, when the gesture ends, this value is reset to zero.
When my gesture ends, I want to keep the alert moving in an animation that takes it to a point that is not visible on the screen.
I have my endSpinRotation
property too, which will begin at zero and be animated switching to 360 degrees. This will cause one complete spin, which is plenty to keep it going until it is no longer visible.
The difference(_:,_:)
function can compare a length in a particular direction. When positionLength
, which could be the X or Y coordinate, has gone past the middle of the frameLength
, which could be the width
or height
, it keeps moving in that direction.
In other words, if you have moved the alert to the top right, it will leave in that direction. If it is in the bottom left it will also leave that way.
The frame is used in this way because it allows me to be sure that the end location is always an invisible location outside of the screen. This is also why I have adjusted the value by adding 50, because this ensures that the alert cannot remain visible when the drag is extremely small, and it simply moves from one side of the screen to the other.
Now I can conform to the View
protocol and add my body
property.
The .position(x:,y:)
modifier will position a view in the top left-hand corner of the screen. This is beginning of the coordinate system on Apple’s platforms, and while I thought about replicating Tumblr’s position for the alert, I thought the code would be simpler if we assume the alert is starting at the zero position.
Starting at zero causes a problem on many screens, because the alert is somewhat obscured by the edges of the screen. To fix this I have combined the GeometryReader
where I obtain the view’s frame in a VStack
with a plain Rectangle
.
This splits the screen in half vertically, so the alert is only at the left side of the middle of the screen.
Although it’s outside of the GeometryReader’s
bounds, the alert can still be dragged over the Rectangle and thrown off the top left or right corners of the screen just as easily as the bottom two.