Skip to content

Instantly share code, notes, and snippets.

@PimCoumans
Last active September 20, 2024 08:12
Show Gist options
  • Save PimCoumans/caa97676b3155f21bac05248fe7733f9 to your computer and use it in GitHub Desktop.
Save PimCoumans/caa97676b3155f21bac05248fe7733f9 to your computer and use it in GitHub Desktop.
Blurring a UIView using CALayer (private API)
// Based on https://github.com/aheze/VariableBlurView/blob/main/Sources/VariableBlurView.swift
import UIKit
public protocol ViewBlurring: UIView {
var blurRadius: CGFloat? { get set }
}
extension UIView: ViewBlurring {
private var blurRadiusKey: String { "inputRadius" }
private var blurFilterName: String { "blurFilter" }
private var blurKeyPath: String {
"filters.\(blurFilterName).\(blurRadiusKey)"
}
/// Adds a blur filter to the view’s `CALayer`, applying the provided radius. Removes the filter when set to `nil`.
/// Can be animated.
public var blurRadius: CGFloat? {
get {
blurFilter?.value(forKey: blurRadiusKey) as? CGFloat
}
set {
let startValue = blurRadius ?? 0
let endValue = newValue ?? 0
guard newValue != blurRadius else { return }
guard let newValue else {
// TODO: Maybe remove filter after any animations have completed
removeBlurFilter()
return
}
if blurFilter == nil {
addBlurFilter()
}
addBlurActionIfNeeded(from: startValue, to: endValue)
layer.setValue(newValue, forKeyPath: blurKeyPath)
}
}
}
extension UIView {
public func addBlurActionIfNeeded(from startValue: CGFloat, to endValue: CGFloat) {
guard
let action = action(for: layer, forKey: "backgroundColor") as? NSObject,
let animation = action.copy() as? CABasicAnimation
else {
return
}
animation.keyPath = blurKeyPath
animation.fromValue = startValue
animation.toValue = endValue
layer.add(animation, forKey: "blurAnimation")
}
}
extension UIView {
private var blurFilter: NSObject? {
layer.filters?.first as? NSObject
}
private func addBlurFilter() {
guard let gaussianBlurFilter = Self.createBlurFilter() else {
return
}
gaussianBlurFilter.setValue(blurFilterName, forKey: "name")
gaussianBlurFilter.setValue(0, forKey: blurRadiusKey)
layer.filters = [gaussianBlurFilter]
}
private func removeBlurFilter() {
layer.filters = []
}
}
extension UIView {
private static func decodedString(forKey key: String) -> String? {
Data(base64Encoded: key).flatMap { String(data: $0, encoding: .utf8) }
}
private static func createBlurFilter() -> NSObject? {
// Shenanigans
guard let selectorName = decodedString(forKey: "ZmlsdGVyV2l0aFR5cGU6") else {
return nil
}
let filterWithTypeSelector = Selector(selectorName)
guard
let className = decodedString(forKey: "Q0FGaWx0ZXI="),
let filterClass = NSClassFromString(className) as AnyObject as? NSObjectProtocol,
filterClass.responds(to: filterWithTypeSelector)
else {
return nil
}
guard
let filterType = decodedString(forKey: "Z2F1c3NpYW5CbHVy"),
let gaussianBlurFilter = filterClass
.perform(filterWithTypeSelector, with: filterType)
.takeUnretainedValue() as? NSObject else {
return nil
}
return gaussianBlurFilter
}
}
let view = UIView()
// .. Configure your view
view.blurRadius = 20
// or even animating
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut) {
view.blurRadius = 0
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment