Skip to content

Instantly share code, notes, and snippets.

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 nathantannar4/f25e52cfbc6e92c95f54a8867ce21e62 to your computer and use it in GitHub Desktop.
Save nathantannar4/f25e52cfbc6e92c95f54a8867ce21e62 to your computer and use it in GitHub Desktop.
SwiftUI ChangeEffect ViewModifier
import SwiftUI
struct ContentView: View {
@State var counter: Int = 0
var body: some View {
VStack(spacing: 24) {
Image(systemName: "exclamationmark.triangle.fill")
.font(Font.system(size: 50))
.changeEffect(.shake, value: counter, animation: .spring(response: 0.25, dampingFraction: 0.1))
Image(systemName: "exclamationmark.triangle.fill")
.font(Font.system(size: 50))
.changeEffect(
.pulse {
RoundedRectangle(cornerRadius: 5)
.stroke(lineWidth: 2)
},
value: counter,
animation: .asymmetric(insertion: .easeIn, removal: .easeOut(duration: 1))
)
.containerShape(RoundedRectangle(cornerRadius: 5))
.foregroundColor(.yellow)
Button("Run") {
counter += 1
}
.buttonStyle(.bordered)
}
}
}
extension View {
public func changeEffect<
Effect: ChangeEffect,
Value: Equatable
>(
_ effect: Effect,
value: Value,
animation: Animation,
isEnabled: Bool = true
) -> some View {
changeEffect(
effect,
value: value,
animation: .continuous(animation),
isEnabled: isEnabled
)
}
public func changeEffect<
Effect: ChangeEffect,
Value: Equatable
>(
_ effect: Effect,
value: Value,
animation: ChangeEffectAnimation = .default,
isEnabled: Bool = true
) -> some View {
modifier(
ChangeEffectModifier(
effect: effect,
value: value,
animation: animation,
isEnabled: isEnabled
)
)
}
}
public struct ChangeEffectAnimation {
var insertion: Animation
var removal: Animation
public static let `default`: ChangeEffectAnimation = .continuous(.linear(duration: 0.35))
public static func continuous(
_ animation: Animation
) -> ChangeEffectAnimation {
ChangeEffectAnimation(
insertion: animation.speed(2),
removal: animation.speed(2)
)
}
public static func asymmetric(
insertion: Animation,
removal: Animation
) -> ChangeEffectAnimation {
ChangeEffectAnimation(
insertion: insertion,
removal: removal
)
}
}
public struct ChangeEffectModifier<
Effect: ChangeEffect,
Value: Equatable
>: ViewModifier, Animatable {
var effect: Effect
var value: Value
var animation: ChangeEffectAnimation
var isEnabled: Bool
@State var id: UInt = 0
@State var isActive = false
public init(
effect: Effect,
value: Value,
animation: Animation,
isEnabled: Bool = true
) {
self.init(
effect: effect,
value: value,
animation: .continuous(animation),
isEnabled: isEnabled
)
}
public init(
effect: Effect,
value: Value,
animation: ChangeEffectAnimation = .default,
isEnabled: Bool = true
) {
self.effect = effect
self.value = value
self.animation = animation
self.isEnabled = isEnabled
}
public func body(content: Content) -> some View {
content
.modifier(
ChangeEffectModifierBody(
effect: effect,
isActive: $isActive,
id: id
)
.animation(isActive ? animation.insertion : animation.removal)
)
.onChange(of: value) { _ in
if isEnabled {
id = id &+ 1
isActive = true
}
}
}
}
struct ChangeEffectModifierBody<
Effect: ChangeEffect
>: ViewModifier, Animatable {
var effect: Effect
var isActive: Binding<Bool>
var id: UInt
var animatableData: Double
init(
effect: Effect,
isActive: Binding<Bool>,
id: UInt
) {
self.effect = effect
self.isActive = isActive
self.id = id
self.animatableData = isActive.wrappedValue ? 1 : 0
}
func body(content: Content) -> some View {
content
.modifier(
effect.makeEffect(
configuration: ChangeEffectConfiguration(
id: ChangeEffectID(value: id),
isActive: isActive.wrappedValue,
progress: animatableData
)
)
.transaction { $0.disablesAnimations = true }
)
.onChange(of: animatableData >= 1) { isComplete in
if isComplete {
isActive.wrappedValue = false
}
}
}
}
public protocol ChangeEffect {
associatedtype Modifier: ViewModifier
@MainActor func makeEffect(configuration: Configuration) -> Modifier
typealias Configuration = ChangeEffectConfiguration
}
public struct ChangeEffectID: Hashable {
var value: UInt
}
public struct ChangeEffectConfiguration {
public let id: ChangeEffectID
public let isActive: Bool
public let progress: Double
}
extension ChangeEffect where Self == ShakeEffect {
public static var shake: ShakeEffect { .init() }
}
public struct ShakeEffect: ChangeEffect {
public func makeEffect(configuration: Configuration) -> Modifier {
Modifier(configuration: configuration)
}
public struct Modifier: ViewModifier {
var configuration: Configuration
public func body(content: Content) -> some View {
content
.offset(x: configuration.progress * 25, y: 0)
}
}
}
extension ChangeEffect {
public static func pulse<S: View>(
@ViewBuilder shape: () -> S
) -> PulseEffect<S> where Self == PulseEffect<S> {
PulseEffect(shape: shape())
}
}
public struct PulseEffect<S: View>: ChangeEffect {
public var shape: S
public init(shape: S) {
self.shape = shape
}
public func makeEffect(configuration: Configuration) -> Modifier {
Modifier(configuration: configuration, shape: shape)
}
public struct Modifier: ViewModifier {
var configuration: Configuration
var shape: S
public func body(content: Content) -> some View {
content
.overlay {
if configuration.isActive {
shape
.transition(
.asymmetric(
insertion: .scale(scale: 0.1),
removal: .scale(scale: 1.5)
)
.combined(with: .opacity)
)
.id(configuration.id)
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment