[ HOME ]

[ DEV NOTES ]

[ CONTACT ME ]

How to recreate the Arc/Dia onboarding intro using SwiftUI and AppKit

VIEW TWITTER THREAD ↗

First, let’s break down arc’s onboarding intro. There’s a few distinct elements we can observe:

  1. The desktop fades in/out to a semi-transparent black
  2. There’s an orb that starts small, grows bigger and reveals a window
  3. A staggered text animation is played

Let’s figure each out!

How does the desktop fade in/out?

That’s easy - we’ll use AppKit to achieve this. Let’s create a floating window and animate its alpha value. We can apply a borderless style mask so it doesn’t look or act like a typical macOS window (no rounded corners, borders or minimise/maximise/close buttons)

// calculate background window position + size
let screenRect = NSScreen.main?.frame ?? .zero
let visibleRect = NSScreen.main?.visibleFrame ?? .zero
let topMenuBarHeight = screenRect.maxY - visibleRect.maxY
let backgroundWindowRect = NSRect(
x: screenRect.origin.x,
y: screenRect.origin.y,
width: screenRect.width,
height: screenRect.height - topMenuBarHeight
)
// create borderless window
let backgroundWindow = NSWindow(
contentRect: backgroundWindowRect,
styleMask: [.borderless], // no typical macOS window elements
backing: buffered,
defer: false
)
// set initial appearance of window - fully transparent
backgroundWindow.alphaValue = 0
backgroundWindow.isOpaque = false
backgroundWindow.ignoresMouseEvents = true
backgroundWindow.backgroundColor = .black
backgroundWindow.level = .floating
// show the window and animate its alpha over 1 second
backgroundWindow.makeKeyAndOrderFront (nil)
NSAnimationContext.runAnimationGroup({ context in
context.duration = 1
backgroundWindow.animator().alphaValue = 0.9
})

How do we create an orb that reveals a window?

It’s magic 🪄💫
… or more like an illusion.

We can create videos with transparent backgrounds, and play them inside a swiftui view. I used Spline to make 2 animations - an orb that grows bigger and a moving gradient with simplex fractal noise

I’ll transition between the orb animation and the moving gradient (as the gradient video can be looped)

Once I was happy with the results, I exported the orb animation as a PNG image sequence and converted it to a .mov file using ffmpeg - so that transparency is preserved.

Terminal window
ffmpeg -framerate 60 -i %03d.png -c:v prores_ks -profile:v 5 -pix_fmt yuva444p10le output.mov

The moving gradient doesn’t need any transparency, so we can just export this straight to mp4 and use directly within an AVPlayer.

Now that the videos are ready, we’ll create the illusion of a window emerging from the orb:

  1. Make a normal window with a transparent background
  2. Hide the titlebar, shadow + other properties of a typical macOS window
  3. Render a swiftui view which plays the orb animation using an AVPlayer
  4. When the video animation finishes, restore the window’s normal properties and transition to the moving gradient video
// create normal window
let mainWindow = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
styleMask: [.titled, .fullSizeContentView],
backing: .buffered,
defer: false
)
// set initial appearance of window to
// display contents but hide window elements
mainWindow.titlebarAppearsTransparent = true
mainWindow.titleVisibility = .hidden
mainWindow.hasShadow = false
mainWindow.isOpaque = false
mainWindow.backgroundColor = .clear
mainWindow.level = .floating
// video player is in a swiftui content view
mainWindow.contentView = NSHostingView(
rootView: ContentView()
.ignoresSafeArea(.all)
)
mainWindow.makeKeyAnd0rderFront(nil)
mainWindow.center()
// restore normal window properties after video finishes
DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
self.animateWindowBackgroundAlpha(
for: mainWindow,
baseColor: .windowBackgroundColor,
fromAlpha: 0,
toAlpha: 1
)
mainWindow.isOpaque = true
mainWindow.hasShadow = true
// show close/min/max buttons
mainWindow.styleMask.insert(.resizable)
mainWindow.styleMask.insert(.miniaturizable)
mainWindow.styleMask.insert(.closable)
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
mainWindow.level = .normal
}
}

How is the text animation created?

I used the AnimateText swift package and built a custom effect that looks like this:

Each character rotates, blurs and fades in using a spring animation - with a slight stagger for a nice cascading feel.

public struct StaggerTextEffect: ATTextAnimateEffect {
public var data: ATElementData
public var userInfo: Any?
public init(_ data: ATElementData, _ userInfo: Any?) {
self.data = data
self.userInfo = userInfo
}
public func body(content: Content) -> some View {
content
.opacity(data.value)
.blur(radius: 10 - 10 * data.value)
.rotation3DEffect(Angle(degrees: 20 * data.invValue), axis: (x: 0, y: 0, z: 1), anchor: .bottomLeading, anchorZ: 0, perspective: 0.7)
.offset(x: 0, y: data.invValue * (data.size.height * 1))
.animation(.spring(duration: 0.6, bounce: 0.26).delay(Double(data.index) * 0.02), value: data.value)
}
}

You can then use it in a view like this:

AnimateText<StaggerTextEffect>($text, type: .letters)

Now to put it all together 🕺

Once the app has launched:

  1. Fade the desktop out with a background window
  2. Create the main window and immediately play the orb animation
  3. After x amount of seconds, enable the normal window properties
  4. Play the moving gradient video in the window and fade out the orb animation video
  5. Play the staggered text animation
  6. Fade the background window out so the user can see the rest of their desktop

And that’s it! I hope this gives you some ideas, and I’d love to see what you create.

I plan to do more breakdowns like this for other effects, so let me know if you have any suggestions - whether they’re for Swift or web based projects.