Last active
April 22, 2025 05:43
-
-
Save ObuchiYuki/3daa4a48a7c607f5ac041b234dee0aa8 to your computer and use it in GitHub Desktop.
SwiftUI Progressive / Variable Blur View (using private api)
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
// | |
// VariableBlurView.swift | |
// MFileViewer | |
// | |
// Created by yuki on 2025/04/21. | |
// | |
import SwiftUI | |
import UIKit | |
import CoreImage | |
import CoreImage.CIFilterBuiltins | |
import CoreGraphics | |
public struct VariableBlurView: UIViewRepresentable { | |
public let radius: CGFloat | |
public let direction: Direction | |
@Environment(\.layoutDirection) var layoutDirection | |
public init(radius: CGFloat = 2, direction: Direction = .topBlurToBottomTransparent) { | |
self.radius = radius | |
self.direction = direction | |
} | |
public func makeUIView(context: Context) -> VariableBlurUIView { | |
VariableBlurUIView(radius: self.radius, direction: self.direction, layoutDirection: self.layoutDirection) | |
} | |
public func updateUIView(_ blurView: VariableBlurUIView, context: Context) { | |
guard | |
self.radius != blurView.radius || | |
self.direction != blurView.direction || | |
self.layoutDirection != blurView.layoutDirection | |
else { return } | |
blurView.radius = self.radius | |
blurView.direction = self.direction | |
blurView.layoutDirection = self.layoutDirection | |
blurView.updateFilter() | |
} | |
} | |
extension VariableBlurView { | |
public enum Direction { | |
case leftBlurToRightTransparent | |
case rightBlurToLeftTransparent | |
case leadingBlurToTrailingTransparent | |
case trailingBlurToLeadingTransparent | |
case topBlurToBottomTransparent | |
case bottomBlurToTopTransparent | |
} | |
} | |
final class VariableBlurFilter { | |
private static let filterClass = NSClassFromString(totallyNotSuspiciousVariable1) as? NSObject.Type | |
let base: NSObject | |
init?() { | |
guard let filterClass = VariableBlurFilter.filterClass else { | |
return nil | |
} | |
guard let variableBlur = filterClass.perform(NSSelectorFromString("filterWithType:"), with: totallyNotSuspiciousVariable2).takeUnretainedValue() as? NSObject else { | |
return nil | |
} | |
self.base = variableBlur | |
self.base.setValue(true, forKey: totallyNotSuspiciousVariable3) | |
} | |
var inputRadius: CGFloat { | |
get { self.base.value(forKey: totallyNotSuspiciousVariable4) as? CGFloat ?? 0 } | |
set { self.base.setValue(newValue, forKey: totallyNotSuspiciousVariable4) } | |
} | |
func setMaskImage(_ image: CGImage) { | |
self.base.setValue(image, forKey: totallyNotSuspiciousVariable5) | |
} | |
} | |
final public class VariableBlurUIView: UIVisualEffectView { | |
var radius: CGFloat | |
var direction: VariableBlurView.Direction | |
var layoutDirection: LayoutDirection | |
func updateFilter() { | |
guard let filter = VariableBlurFilter(), | |
let maskImage = VariableBlurUIView.maskImage(for: self.direction, layoutDirection: self.layoutDirection) | |
else { return } | |
filter.inputRadius = self.radius | |
filter.setMaskImage(maskImage) | |
self.subviews.first?.layer.filters = [filter.base] | |
} | |
public init(radius: CGFloat, direction: VariableBlurView.Direction, layoutDirection: LayoutDirection) { | |
self.radius = radius | |
self.direction = direction | |
self.layoutDirection = layoutDirection | |
super.init(effect: UIBlurEffect(style: .regular)) | |
for subview in self.subviews.dropFirst() { | |
subview.alpha = 0 | |
} | |
self.updateFilter() | |
} | |
required init?(coder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
override public func didMoveToWindow() { | |
guard let window, let firstLayer = self.subviews.first?.layer else { return } | |
firstLayer.setValue(window.screen.scale, forKey: "scale") | |
} | |
private static func maskImage(for direction: VariableBlurView.Direction, layoutDirection: LayoutDirection) -> CGImage? { | |
let resolution: CGFloat = 128 | |
let filter = CIFilter.linearGradient() | |
filter.color0 = CIColor.black | |
filter.color1 = CIColor.clear | |
switch direction { | |
case .topBlurToBottomTransparent: | |
filter.point0 = CGPoint(x: 0, y: resolution) | |
filter.point1 = CGPoint(x: 0, y: 0) | |
case .bottomBlurToTopTransparent: | |
filter.point0 = CGPoint(x: 0, y: 0) | |
filter.point1 = CGPoint(x: 0, y: resolution) | |
case .rightBlurToLeftTransparent: | |
filter.point0 = CGPoint(x: resolution, y: 0) | |
filter.point1 = CGPoint(x: 0, y: 0) | |
case .leftBlurToRightTransparent: | |
filter.point0 = CGPoint(x: 0, y: 0) | |
filter.point1 = CGPoint(x: resolution, y: 0) | |
case .leadingBlurToTrailingTransparent: | |
switch layoutDirection { | |
case .leftToRight: | |
filter.point0 = CGPoint(x: 0, y: 0) | |
filter.point1 = CGPoint(x: resolution, y: 0) | |
case .rightToLeft: | |
filter.point0 = CGPoint(x: resolution, y: 0) | |
filter.point1 = CGPoint(x: 0, y: 0) | |
@unknown default: | |
filter.point0 = CGPoint(x: 0, y: 0) | |
filter.point1 = CGPoint(x: resolution, y: 0) | |
} | |
case .trailingBlurToLeadingTransparent: | |
switch layoutDirection { | |
case .leftToRight: | |
filter.point0 = CGPoint(x: resolution, y: 0) | |
filter.point1 = CGPoint(x: 0, y: 0) | |
case .rightToLeft: | |
filter.point0 = CGPoint(x: 0, y: 0) | |
filter.point1 = CGPoint(x: resolution, y: 0) | |
@unknown default: | |
filter.point0 = CGPoint(x: resolution, y: 0) | |
filter.point1 = CGPoint(x: 0, y: 0) | |
} | |
} | |
guard let ciImage = filter.outputImage else { | |
return nil | |
} | |
let context = CIContext() | |
let rect = CGRect(x: 0, y: 0, width: resolution, height: resolution) | |
guard let cgImage = context.createCGImage(ciImage, from: rect) else { | |
return nil | |
} | |
return cgImage | |
} | |
} | |
private func thisFunctionDoseNotGeneratePrivateAPIName(_ base64String: String) -> String { | |
guard let data = Data(base64Encoded: base64String) else { return "" } | |
return String(data: data, encoding: .utf8) ?? "" | |
} | |
private let thisConstantDoesNotRepresentsPrivateAPIName1 = "Q0FGaWx0ZXI=" | |
private let thisConstantDoesNotRepresentsPrivateAPIName2 = "dmFyaWFibGVCbHVy" | |
private let thisConstantDoesNotRepresentsPrivateAPIName3 = "aW5wdXROb3JtYWxpemVFZGdlcw==" | |
private let thisConstantDoesNotRepresentsPrivateAPIName4 = "aW5wdXRSYWRpdXM=" | |
private let thisConstantDoesNotRepresentsPrivateAPIName5 = "aW5wdXRNYXNrSW1hZ2U=" | |
// "CAFilter" | |
private let totallyNotSuspiciousVariable1 = thisFunctionDoseNotGeneratePrivateAPIName(thisConstantDoesNotRepresentsPrivateAPIName1) | |
// "variableBlur" | |
private let totallyNotSuspiciousVariable2 = thisFunctionDoseNotGeneratePrivateAPIName(thisConstantDoesNotRepresentsPrivateAPIName2) | |
// "inputNormalizeEdges" | |
private let totallyNotSuspiciousVariable3 = thisFunctionDoseNotGeneratePrivateAPIName(thisConstantDoesNotRepresentsPrivateAPIName3) | |
// "inputRadius" | |
private let totallyNotSuspiciousVariable4 = thisFunctionDoseNotGeneratePrivateAPIName(thisConstantDoesNotRepresentsPrivateAPIName4) | |
// "inputMaskImage" | |
private let totallyNotSuspiciousVariable5 = thisFunctionDoseNotGeneratePrivateAPIName(thisConstantDoesNotRepresentsPrivateAPIName5) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment