Skip to content

Instantly share code, notes, and snippets.

@auramagi
Created September 20, 2022 13:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save auramagi/dde01a25df9d77ce81b0e8f93a935450 to your computer and use it in GitHub Desktop.
Save auramagi/dde01a25df9d77ce81b0e8f93a935450 to your computer and use it in GitHub Desktop.
import SwiftUI
struct BlobbySymbol<Primary: View, Alternative: View>: Animatable, View {
private var progress: Double = 0
var animatableData: Double {
get { progress }
set { progress = newValue }
}
let maxBlur: Double
let alphaThreshold: Double
let primary: Primary
let alternative: Alternative
init(
isPrimary: Bool,
maxBlur: Double = 25,
alphaThreshold: Double = 1 / 4,
@ViewBuilder primary: () -> Primary,
@ViewBuilder alternative: () -> Alternative
) {
self.progress = isPrimary ? 0 : 1
self.maxBlur = maxBlur
self.alphaThreshold = alphaThreshold
self.primary = primary()
self.alternative = alternative()
}
var body: some View {
Group {
// Place a hidden view for sizing, or else Canvas will take up all available space
if progress >= 0.5 {
alternative
} else {
primary
}
}
.hidden()
.overlay(
Canvas(opaque: false, colorMode: .nonLinear, rendersAsynchronously: false) { ctx, size in
let bounds = CGRect(origin: .zero, size: size)
// Drawing the symbol: apply blur and alpha threshold filters
// Alpha filter will make the view black, so we use it as a mask and fill bounds with foreground style
ctx.clipToLayer { ctx in
ctx.addFilter(.alphaThreshold(min: alphaThreshold))
// Progress → blur: 0% → 0, 50% → maxBlur, 100% → 0
ctx.addFilter(.blur(radius: (0.5 - abs(progress - 0.5)) * maxBlur))
ctx.drawLayer { ctx in
// Swap symbols at 50%
ctx.draw(ctx.resolveSymbol(id: progress >= 0.5)!, in: bounds)
}
}
ctx.fill(.init(.init(origin: .zero, size: size)), with: .foreground)
} symbols: {
primary.tag(false)
alternative.tag(true)
}
)
}
}
import SwiftUI
struct ContentView: View {
@State var flag = true
@State var selection: Int = 0
var body: some View {
VStack {
Group {
switch selection {
case 0:
Button {
flag.toggle()
} label: {
BlobbySymbol(isPrimary: flag) {
Image(systemName: "play")
} alternative: {
Image(systemName: "pause")
}
.foregroundStyle(.mint.gradient)
.font(.system(size: 75, weight: .medium, design: .rounded))
.symbolVariant(.fill)
.animation(.easeOut, value: flag)
}
.buttonStyle(.plain)
case 1:
Button {
flag.toggle()
} label: {
BlobbySymbol(isPrimary: flag) {
Image(systemName: "xmark")
} alternative: {
Image(systemName: "checkmark")
}
.foregroundStyle(.white)
.colorMultiply(flag ? Color.red : Color.green)
.font(.system(size: 75, weight: .medium, design: .rounded))
.animation(.easeOut, value: flag)
}
.buttonStyle(.plain)
case 2:
Button {
flag.toggle()
} label: {
BlobbySymbol(isPrimary: flag) {
Image(systemName: "questionmark")
} alternative: {
Image(systemName: "exclamationmark")
}
.foregroundStyle(.linearGradient(colors: [.purple, .pink], startPoint: .topLeading, endPoint: .bottomTrailing))
.font(.system(size: 75, weight: .medium, design: .rounded))
.animation(.easeOut, value: flag)
}
.buttonStyle(.plain)
default:
EmptyView()
}
}
.frame(maxHeight: .infinity)
.transition(.opacity.combined(with: .scale))
Picker(selection: $selection) {
Image(systemName: "play")
.symbolVariant(.fill)
.tag(0)
Image(systemName: "xmark")
.tag(1)
Image(systemName: "questionmark")
.tag(2)
} label: {
Text("Selection")
}
.pickerStyle(.segmented)
.padding()
}
.animation(.easeOut, value: selection)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment