Declarative, chainable UIView animations for iOS — clean and simple.
FluidKit makes UIView animations easier to read and write. Instead of nesting callbacks inside callbacks, you chain animation steps in a straight line.
// Before — nested and hard to read
UIView.animate(withDuration: 0.3, animations: {
button.alpha = 0
}, completion: { _ in
UIView.animate(withDuration: 0.4, animations: {
button.transform = .identity
})
})
// After — FluidKit
FluidAnimation()
.animate(duration: 0.3) { $0.setAlpha(0) }
.animate(duration: 0.4, curve: .spring) { $0.resetTransform() }
.run(on: button)In Xcode go to File → Add Package Dependencies and paste:
https://github.com/codewithswiftly/FluidKit.git
Or add it to Package.swift:
.package(url: "https://github.com/codewithswiftly/FluidKit.git", from: "1.0.4")FluidAnimation()
.animate(duration: 0.3, curve: .easeOut) { view in
view.setAlpha(0)
view.setTranslation(y: -20)
}
.wait(0.1)
.animate(duration: 0.5, curve: .spring) { view in
view.resetTransform()
view.setAlpha(1)
}
.then {
print("All done!")
}
.run(on: myView)// Fade in
myLabel.fadeIn()
// Fade out and hide when done
myLabel.fadeOut(hideAfter: true)
// Shake left and right (great for form errors)
emailField.shake()
// Scale up and back down (great for like buttons)
heartButton.pulse(scale: 1.3)
// Slide in from an edge with a spring bounce
cardView.bounceIn(from: .bottom)// Stagger a list of views in one by one
FluidPreset.staggerIn(views: [titleLabel, descriptionLabel, startButton])
// Stagger them out
FluidPreset.staggerOut(views: cells) {
self.dismiss(animated: false)
}
// Shimmer effect on a placeholder while loading
FluidPreset.startShimmer(view: skeletonCard)
// ...after data loads:
FluidPreset.stopShimmer(view: skeletonCard)
// Pulsing loader
FluidPreset.startPulse(view: loadingView)
FluidPreset.stopPulse(view: loadingView)
// Cross-dissolve between two views
FluidPreset.crossDissolve(from: loadingView, to: contentView)Use these inside .animate() blocks to change view properties:
| Modifier | What it does |
|---|---|
setScale(_ scale) |
Scale up or down. 1.0 = normal, 2.0 = double |
setTranslation(x:y:) |
Slide the view by x and y points |
setRotation(degrees:) |
Rotate by degrees (e.g. 45 for 45°) |
resetTransform() |
Reset to original size, position, rotation |
setAlpha(_ value) |
Transparency: 0 = invisible, 1 = visible |
setCornerRadius(_ r) |
Rounded corners |
setBackground(_ color) |
Background color |
| Curve | Best used for |
|---|---|
.easeIn |
Dismissing/exiting views |
.easeOut |
Entering/appearing views |
.easeInOut |
General purpose (default) |
.linear |
Looping or continuous animations |
.spring |
Interactive elements like buttons and cards |
@MainActor
class LoginViewController: UIViewController {
@IBOutlet weak var emailField: UITextField!
@IBOutlet weak var loginButton: UIButton!
@IBOutlet weak var spinner: UIActivityIndicatorView!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Stagger in the form elements
FluidPreset.staggerIn(views: [emailField, loginButton])
}
@IBAction func loginTapped() {
// Give the button a pressed feel, then fade it out
FluidAnimation()
.animate(duration: 0.1, curve: .easeIn) { [weak self] _ in
self?.loginButton.setScale(0.95)
}
.animate(duration: 0.2, curve: .easeOut) { [weak self] _ in
self?.loginButton.setAlpha(0)
}
.then { [weak self] in
self?.spinner.startAnimating()
}
.run(on: loginButton)
}
func showError() {
emailField.shake()
}
}FluidKit/
├── Sources/FluidKit/
│ ├── AnimationCurve.swift # Timing options: easeIn, spring, etc.
│ ├── FluidAnimation.swift # Chainable animation builder
│ ├── UIView+Fluid.swift # setScale(), fadeIn(), shake(), etc.
│ ├── FluidPreset.swift # staggerIn, shimmer, pulse, crossDissolve
│ └── Examples.swift # Code examples (safe to delete)
└── Tests/FluidKitTests/
└── FluidKitTests.swift
- iOS 15+
- Swift 5.9+
- Xcode 15+
- Zero dependencies
MIT © 2026 Rahul Das Gupta
If you found this project useful, consider giving it a ⭐️ on GitHub!