Last active
April 24, 2025 04:50
-
-
Save stammy/27c4844c0148109fe89f8a0694694c8c to your computer and use it in GitHub Desktop.
Liquid Blob effect with SwiftUI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ContentView.swift | |
// LiquidCircles | |
// | |
// Created by Paul Stamatiou on 10/10/22. | |
// | |
import SwiftUI | |
struct ContentView: View { | |
@State var animate: Bool = false | |
var body: some View { | |
VStack { | |
Spacer() | |
Canvas { context, size in | |
// The real magic lies in these two filters. | |
// This is an approach common with SVG using feGaussianBlur and feColorMatrix | |
// https://developer.apple.com/documentation/swiftui/graphicscontext/filter/alphathreshold(min:max:color:)?changes=_7_3_5&language=objc | |
// Returns a filter that replaces each pixel with alpha components within a range by a constant color, or transparency otherwise. | |
context.addFilter(.alphaThreshold(min: 0.5, color: .black)) | |
// Gaussian blur | |
context.addFilter(.blur(radius: 15)) | |
// drawLayer creates a new transparency layer that you can draw into | |
// the above filters won't work without drawing the swiftui symbols into their single layer added to the main context | |
context.drawLayer { ctx in | |
// access the passed in symbols using their .tag() id | |
let circle0 = ctx.resolveSymbol(id: 0)! | |
let circle1 = ctx.resolveSymbol(id: 1)! | |
ctx.draw(circle0, at: CGPoint(x: 131, y: 50)) | |
ctx.draw(circle1, at: CGPoint(x: 262, y: 50)) | |
} | |
} symbols: { | |
// symbols is how you can tell canvas to accept a regular SwiftUI view to work with | |
// required to .tag() so you get an id to resolve the symbol inside canvas | |
Circle() | |
.fill(.black) | |
.frame(width: 90, height: 90) | |
.offset(x: animate ? 66 : -40) | |
.tag(0) | |
Circle() | |
.fill(.black) | |
.frame(width: 90, height: 90) | |
.offset(x: animate ? -66 : 40) | |
.tag(1) | |
} | |
.animation(.easeInOut(duration: 2).repeatForever(autoreverses: true), value: animate) | |
.frame(height: 100) | |
Spacer() | |
} | |
.onAppear { animate = true } | |
} | |
} | |
struct ContentView_Previews: PreviewProvider { | |
static var previews: some View { | |
ContentView() | |
} | |
} |
I had to do it like this
.onAppear {
// Initial animation trigger
animate = true
// Set up a timer to continuously toggle the animate value
Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { _ in
animate.toggle()
}
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks a lot for this gist 😊
It was not animating for me so I had to update the state variable from inside
Task{}