How is the Silky Smooth iOS Progress Bar Interaction Created?
Published: 2024-03-26 14:20:16

This article explores the intricate animation of the hidden easter egg within the [KikoCard](https://apps.apple.com/app/apple-store/id6463467787?pt=126475836&ct=dev&mt=8) app, a feature that piqued the interest of my fellow developers when I showcased it. Motivated by their curiosity, I've written this detailed breakdown. By explaining the foundational concepts and methodologies, I aim to make the animation's mechanics accessible to all. Newcomers will find a clear introduction to the principles involved, while experienced developers will gain insights into the nitty-gritty of the code.


![banner kiko card](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n5gbt1z3ap9w55emut54.png)



I aim to dissect the implementation principles of the [KikoCard](https://apps.apple.com/app/apple-store/id6463467787?pt=126475836&ct=dev&mt=8) mini easter egg in great detail, ensuring comprehension and mastery of each step. The animation effect utilizes SwiftUI, showcasing its powerful capabilities and versatility. I hope you enjoy this tutorial and learn new techniques and knowledge. I’ll include essential code snippets for those interested.

{% embed https://www.youtube.com/shorts/F-UePwrIePY %}


## Gradient Light

A critical aspect of this animation is the control over gradient lighting. The key lies in simulating real-world feelings through the diffusion and attenuation effects of point light sources. Additionally, controlling light spread at the edges with a mask layer ensures the light doesn't scatter indiscriminately.

![Gradient Light Progress Bar Loading](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9bhur8xlvqs9ahj2etkv.gif)

### Physics

By layering multiple gradients of shadows, we achieve a softer, more physically accurate diffusion of light. This effect mimics how light softens at the edges as it emanates from a source, creating a natural and realistic sensation.


![Shadows in a Homely Video Scene](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tvlt9p58kyj6hzt918af.png)

Typically, achieving this effect involves layering three or more gradients of shadow. The transparency, diffusion size, and shadow values don't follow a strict pattern; the aim is to mimic the natural world as closely as possible. Adjusting each layer's diffusion size and transparency value—smaller diffusion with higher transparency and vice versa—yields an ideal outcome through nonlinear layering.


![Masking](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/98150rh2jdnbedytth6l.png)
```
.shadow(color: .accent.opacity(0.3), radius: 80)
.shadow(color: .accent.opacity(0.5), radius: 60)
.shadow(color: .accent.opacity(0.6), radius: 20)
.shadow(color: .accent.opacity(0.7), radius: 8)
```


### Masking

In SwiftUI, if you need to remove excess parts, a simple method is to apply a rectangular mask. This technique precisely trims unnecessary light, aligning the design with our expectations. It’s straightforward and effective, enhancing design quality.

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b7kfa4hddw9oyoofpklh.png)
```
.contentShape(Rectangle())
```

## Progress Bar Implementation

### Percentage

GeometryReader in SwiftUI is invaluable for reading space sizes and positions. It enables dynamic width adjustments for our progress bar, flexibly changing according to set parameters.

![Image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/by3w7bonjtpq5ynqvj7t.png)

### Width Variation

Here, 'progress' represents a percentage of completion. Setting an interval increase of 0.01 every 0.01 seconds equates to 1% progress, requiring 1 second to complete the progress bar. This allows for speed adjustments, offering a desired loading pace. However, this may result in a linear growth, which might not be what we want.
```
@State private var progress: CGFloat = 0
@State private var timer: Timer?
@State private var isProgressCompleted: Bool = false

var body: some View {
    GeometryReader { geometry in
        RoundedRectangle(cornerRadius: 12, style: .continuous)
            .frame(width: isProgressCompleted ? geometry.size.width : geometry.size.width * progress)
    }
    .frame(maxWidth: .infinity)
}
```

## Building Momentum

This method encompasses three crucial features for user interaction:

1. Releasing returns to the starting point, a physical feedback ensuring unobstructed further operations.
2. Progressive width increase enhances user experience by avoiding sudden jumps.
3. Incremental vibration feedback occurs as the progress bar expands, enhancing user engagement.

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sj0hmci2pbo8f7ps640o.gif)

### Acceleration

For a physically intuitive progress bar with acceleration, we set a constant increment 'acceleration' to 0.00007. This exponential increase means starting with 0.007% + 0.07% = 0.077%, making initial progress slow but rapidly increasing, completing 0% to 100% in 1.72 seconds.

![Image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6e12ii9sb7k55hr8tz64.png)

### Vibration

Implemented through UIKit, varying vibration intensities can be chosen, with a preference for a lighter 'soft' setting. Vibration frequency is crucial; excessive vibrations can be overwhelming and potentially cause system lag, necessitating a threshold adjustment to reduce frequency.

![Image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/56vxshcvn89cxrc1994g.png)
```
func startProgress() {
        timer?.invalidate()
        isProgressCompleted = false 
        var increment = 0.0007 
        let acceleration = 0.00007 

        let feedbackGenerator = UIImpactFeedbackGenerator(style: .soft)
        feedbackGenerator.prepare()

        let vibrationThreshold: CGFloat = 0.01

        var lastVibrationProgress: CGFloat = 0

        timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { [self] timer in

            self.progress += increment

            increment += acceleration

            if self.progress >= lastVibrationProgress + vibrationThreshold {
                feedbackGenerator.impactOccurred()
                lastVibrationProgress = self.progress
            }

            if self.progress >= 1 {
                timer.invalidate()
                self.progress = 1
                self.isProgressCompleted = true
            }
        }
    }

    func resetProgress() {
        timer?.invalidate()
        progress = 0
        isProgressCompleted = false
    }
```

## Long Press Gesture

### Sequencing Gestures/Gesture Chains

Integration with gestures is achieved through sequencing, managing initial long presses and progress resets. Gestures, like layers, can have set priorities and sequences, ensuring synchronous operation.

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/81tshgr63fxb7clnkt14.png)
```
   LongPressGesture(minimumDuration: 0.5)
            .onEnded { _ in
                withAnimation {
                    if !isProgressCompleted {
                        startProgress()
                    }
                }
            }
            .sequenced(before: DragGesture(minimumDistance: 0))
            .onEnded { _ in
                withAnimation {
                    if !isProgressCompleted {
                        resetProgress()
                    } else {
                  //
                    }
                }
            }
```

## Rectangular Scaling

### Magic Movement

Scaling is elegantly handled with SwiftUI’s matchedGeometryEffect, akin to After Effects animation tagging. It simplifies animation between start and end frames, automatically managing intermediate frames for smooth transitions.

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u1m0jokzkzpe2wc50qz7.gif)

SwiftUI provides a property wrapper called Namespace to precisely identify start and end frames, ensuring the animation's correctness. Namespace plays a crucial role by assigning unique identifiers to both the initial and final frames. Moreover, we can leverage if-else statements for element transitions. For instance, we can make one element disappear at a certain moment, only to have another appear in its place. Throughout this process, all animation changes are automatically completed. This means there's no need to manually adjust each frame's changes—everything is automated, significantly simplifying development.


![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bfy0xmjx4jgcboh8di46.png)
```
  @State private var scaleIntoOneCard = false
  @Namespace private var shapeTransition
  
   if !scaleIntoOneCard {
         progressbar()
           .matchedGeometryEffect(id: "card", in: shapeTransition)
   } else{
				RoundedRectangle(cornerRadius: 4)
				   .matchedGeometryEffect(id: "card", in: shapeTransition)
   }
```


### From One to Six Cards

Transitioning from a single useless card to six stacked with ZStack reflects start and end frame logic. 

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tsf7d31p603p5tzn7zpb.gif)

The transition is smoothly managed by matchedGeometryEffect, crucially matching ids to avoid confusion and maintain animation integrity.


![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tjg6vmkcrvzz2456h7xi.png)
```
	 ZStack {
            ForEach(0 ..< 6) { index in
                card()
                    .matchedGeometryEffect(id: "card\(index)", in: shapeTransition)
            }
        }
```
```
     HStack {
            ForEach(0 ..< 6) { index in
                card()
                    .matchedGeometryEffect(id: "card\(index)", in: shapeTransition)
            }
        }
```

In summary, after a long press fills the progress bar, it instantly transforms into a single card. Then, this card vanishes, followed by its transformation into six cards. These six cards then disappear, revealing a password input field. The transition effect of this animation is akin to the magic move effect found in Keynote presentations.

## Input Field

### Counting Numbers

A verification-like input can be crafted with six TextFields and managed focus but I opted for a simpler approach. 

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e9kawjojyztbdp02iqyj.gif)

The background of the input field is not real; it’s one complete TextField, with spacing controlled for equal-width numbers.

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kua30rwsus4kjthsodv9.png)
```
 .kerning(20)
 .font(.system(size: 13).weight(.bold).monospaced())
```


### Shake Animation

A clever, minimal approach was used for error shakes, typically controlled by x-axis offsets. 

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l2hmh7etvvzkj3q27x6t.gif)
Here, a spring animation halts the movement towards an offset, creating a ‘shake’ effect.

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/38i73clfkodif26whegv.png)


### Disappearing with a Dissolving Transition

For the disappearing and diffusing transition effect, the successful disappearance of the input field utilizes a custom composite transition effect, involving changes in opacity and size. This is a transition style Apple often uses, such as the vanishing effect of the iPad's spotlight.


![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2hvak2r4u1mzqdr4eo4x.gif)

The concept is straightforward: the ending frame is in an enlarged and blurred state. You can control the overall effect by adjusting the radius and scale, where the intensity of the adjustments is closely tied to the size of the object about to disappear.


![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8n1rxt6g3grqvscf1ixo.png)
```
private struct BlurModifier: ViewModifier {
    public let isIdentity: Bool
    public var intensity: CGFloat

    public func body(content: Content) -> some View {
        content
            .blur(radius: isIdentity ? intensity : 0)
            .opacity(isIdentity ? 0 : 1)
    }
}

public extension AnyTransition {
    static var blur: AnyTransition {
        .blur()
    }

    static var blurWithoutScale: AnyTransition {
        .modifier(
            active: BlurModifier(isIdentity: true, intensity: 5),
            identity: BlurModifier(isIdentity: false, intensity: 5)
        )
    }

    static func blur(
        intensity: CGFloat = 5,
        scale: CGFloat = 0.9,
        scaleAnimation animation: Animation = .spring()
    ) -> AnyTransition {
        .scale(scale: scale)
            .animation(animation)
            .combined(
                with: .modifier(
                    active: BlurModifier(isIdentity: true, intensity: intensity),
                    identity: BlurModifier(isIdentity: false, intensity: intensity)
                )
            )
    }
}
```

### Animation Control

Finale and Reflections – I'm delighted to share these design nuances, offering a blend of design and technology insights. It's evident that individuals proficient in visual design may lack technical expertise, often constrained by implementation limits. 


![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aoc3nlrv0iirejxl8beo.png)
```
  DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                                withAnimation {
                                    showSixCards = true
                                }
                                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                                    withAnimation {
                                        showPasswordInput = true
                                    }
                                }
                            }
```

Conversely, while tech professionals possess the means to realize visions, they might lack a granular understanding of design, leading to a common refrain: "It's unnecessary; the difference is imperceptible." Yet, a significant gap remains, a chasm as vast as the Mariana Trench, between these fields. The division of labor in societal development seemingly narrows our focus to specialized areas, whereas the essence of many achievements lies in a cohesive, multidisciplinary approach – this is my understanding.

The article was written hastily, with an aim to elucidate the principles through diagrams as much as possible. Should there be any confusion or questions about the content, feedback is always welcome.