import Foundation import UIKit import Display import ComponentFlow import GlassBackgroundComponent private final class RestingBackgroundView: UIVisualEffectView { var isDark: Bool? static func colorMatrix(isDark: Bool) -> [Float32] { if isDark { return [1.082, -0.113, -0.011, 0.0, 0.135, -0.034, 1.003, -0.011, 0.0, 0.135, -0.034, -0.113, 1.105, 0.0, 0.135, 0.0, 0.0, 0.0, 1.0, 0.0] } else { return [1.185, -0.05, -0.005, 0.0, -0.2, -0.015, 1.15, -0.005, 0.0, -0.2, -0.015, -0.05, 1.195, 0.0, -0.2, 0.0, 0.0, 0.0, 1.0, 0.0] } } init() { let effect = UIBlurEffect(style: .light) super.init(effect: effect) for subview in self.subviews { if subview.description.contains("VisualEffectSubview") { subview.isHidden = true } } self.clipsToBounds = true } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func update(isDark: Bool) { if self.isDark == isDark { return } self.isDark = isDark if let sublayer = self.layer.sublayers?[0], let _ = sublayer.filters { sublayer.backgroundColor = nil sublayer.isOpaque = false if let classValue = NSClassFromString("CAFilter") as AnyObject as? NSObjectProtocol { let makeSelector = NSSelectorFromString("filterWithName:") let filter = classValue.perform(makeSelector, with: "colorMatrix").takeUnretainedValue() as? NSObject if let filter { var matrix: [Float32] = RestingBackgroundView.colorMatrix(isDark: isDark) filter.setValue(NSValue(bytes: &matrix, objCType: "{CAColorMatrix=ffffffffffffffffffff}"), forKey: "inputColorMatrix") sublayer.filters = [filter] sublayer.setValue(1.0, forKey: "scale") } } } } } public final class LiquidLensView: UIView { public enum Kind { case externalContainer case builtinContainer case noContainer } private struct Params: Equatable { var size: CGSize var cornerRadius: CGFloat? var selectionOrigin: CGPoint var selectionSize: CGSize var inset: CGFloat var liftedInset: CGFloat var isDark: Bool var isLifted: Bool var isCollapsed: Bool init(size: CGSize, cornerRadius: CGFloat?, selectionOrigin: CGPoint, selectionSize: CGSize, inset: CGFloat, liftedInset: CGFloat, isDark: Bool, isLifted: Bool, isCollapsed: Bool) { self.size = size self.cornerRadius = cornerRadius self.selectionOrigin = selectionOrigin self.selectionSize = selectionSize self.inset = inset self.liftedInset = liftedInset self.isLifted = isLifted self.isDark = isDark self.isCollapsed = isCollapsed } } private struct LensParams: Equatable { var baseFrame: CGRect var inset: CGFloat var liftedInset: CGFloat var isLifted: Bool init(baseFrame: CGRect, inset: CGFloat, liftedInset: CGFloat, isLifted: Bool) { self.baseFrame = baseFrame self.inset = inset self.liftedInset = liftedInset self.isLifted = isLifted } } private let containerView: UIView private let backgroundContainer: GlassBackgroundContainerView? private let genericBackgroundContainer: UIView? private let backgroundView: GlassBackgroundView? private var lensView: UIView? private let liftedContainerView: UIView public let contentView: UIView private let restingBackgroundView: RestingBackgroundView private var legacySelectionView: GlassBackgroundView.ContentImageView? private var legacyContentMaskView: UIView? private var legacyContentMaskBlobView: UIImageView? private var legacyLiftedContentBlobMaskView: UIImageView? public var selectedContentView: UIView { return self.liftedContainerView } private var params: Params? private var appliedLensParams: LensParams? private var isApplyingLensParams: Bool = false private var pendingLensParams: LensParams? private var liftedDisplayLink: SharedDisplayLinkDriver.Link? public var selectionOrigin: CGPoint? { return self.params?.selectionOrigin } public var selectionSize: CGSize? { return self.params?.selectionSize } public private(set) var isAnimating: Bool = false { didSet { if self.isAnimating != oldValue { self.onUpdatedIsAnimating?(self.isAnimating) } } } public var onUpdatedIsAnimating: ((Bool) -> Void)? public var isLiftedAnimationCompleted: (() -> Void)? public init(kind: Kind) { self.containerView = UIView() switch kind { case .builtinContainer: self.backgroundContainer = GlassBackgroundContainerView() self.genericBackgroundContainer = nil case .externalContainer, .noContainer: self.backgroundContainer = nil self.genericBackgroundContainer = UIView() } if case .noContainer = kind { self.backgroundView = nil } else { self.backgroundView = GlassBackgroundView() } self.contentView = UIView() self.liftedContainerView = UIView() self.restingBackgroundView = RestingBackgroundView() super.init(frame: CGRect()) if let backgroundContainer = self.backgroundContainer { self.addSubview(backgroundContainer) if let backgroundView = self.backgroundView { backgroundContainer.contentView.addSubview(backgroundView) backgroundView.contentView.addSubview(self.containerView) } } else if let genericBackgroundContainer = self.genericBackgroundContainer { self.addSubview(genericBackgroundContainer) if let backgroundView = self.backgroundView { genericBackgroundContainer.addSubview(backgroundView) backgroundView.contentView.addSubview(self.containerView) } else { genericBackgroundContainer.addSubview(self.containerView) } } self.containerView.isUserInteractionEnabled = false if #available(iOS 26.0, *) { if let viewClass = NSClassFromString("_UILiquidLensView") as AnyObject as? NSObjectProtocol { let allocSelector = NSSelectorFromString("alloc") let initSelector = NSSelectorFromString("initWithRestingBackground:") let objcAlloc = viewClass.perform(allocSelector).takeUnretainedValue() let instance = objcAlloc.perform(initSelector, with: UIView()).takeUnretainedValue() self.lensView = instance as? UIView } } if let lensView = self.lensView { if let backgroundContainer = self.backgroundContainer { backgroundContainer.layer.zPosition = 1 } else if let genericBackgroundContainer = self.genericBackgroundContainer{ genericBackgroundContainer.layer.zPosition = 1 } lensView.layer.zPosition = 10.0 self.liftedContainerView.addSubview(self.restingBackgroundView) self.containerView.addSubview(self.liftedContainerView) self.containerView.addSubview(lensView) self.containerView.addSubview(self.contentView) if let backgroundContainer = self.backgroundContainer { lensView.perform(NSSelectorFromString("setLiftedContainerView:"), with: backgroundContainer.contentView) } else if let genericBackgroundContainer = self.genericBackgroundContainer { lensView.perform(NSSelectorFromString("setLiftedContainerView:"), with: genericBackgroundContainer) } lensView.perform(NSSelectorFromString("setLiftedContentView:"), with: self.liftedContainerView) lensView.perform(NSSelectorFromString("setOverridePunchoutView:"), with: self.contentView) do { let selector = NSSelectorFromString("setLiftedContentMode:") if let method = lensView.method(for: selector) { typealias ObjCMethod = @convention(c) (AnyObject, Selector, Int32) -> Void let function = unsafeBitCast(method, to: ObjCMethod.self) function(lensView, selector, 1) } } do { let selector = NSSelectorFromString("setStyle:") if let method = lensView.method(for: selector) { typealias ObjCMethod = @convention(c) (AnyObject, Selector, Int32) -> Void let function = unsafeBitCast(method, to: ObjCMethod.self) function(lensView, selector, 1) } } do { let selector = NSSelectorFromString("setWarpsContentBelow:") if let method = lensView.method(for: selector) { typealias ObjCMethod = @convention(c) (AnyObject, Selector, Bool) -> Void let function = unsafeBitCast(method, to: ObjCMethod.self) function(lensView, selector, true) } } lensView.setValue(UIColor(white: 0.0, alpha: 0.1), forKey: "restingBackgroundColor") } else { let legacySelectionView = GlassBackgroundView.ContentImageView() self.legacySelectionView = legacySelectionView if let backgroundView = self.backgroundView { backgroundView.contentView.insertSubview(legacySelectionView, at: 0) } else { self.containerView.insertSubview(legacySelectionView, at: 0) } let legacyContentMaskView = UIView() legacyContentMaskView.backgroundColor = .white self.legacyContentMaskView = legacyContentMaskView self.contentView.mask = legacyContentMaskView if let filter = CALayer.luminanceToAlpha() { legacyContentMaskView.layer.filters = [filter] } let legacyContentMaskBlobView = UIImageView() self.legacyContentMaskBlobView = legacyContentMaskBlobView legacyContentMaskView.addSubview(legacyContentMaskBlobView) self.containerView.addSubview(self.contentView) let legacyLiftedContentBlobMaskView = UIImageView() self.legacyLiftedContentBlobMaskView = legacyLiftedContentBlobMaskView self.liftedContainerView.mask = legacyLiftedContentBlobMaskView self.containerView.addSubview(self.liftedContainerView) } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } public func setLiftedContainer(view: UIView) { guard let lensView = self.lensView else { return } lensView.perform(NSSelectorFromString("setLiftedContainerView:"), with: view) } public func update(size: CGSize, cornerRadius: CGFloat? = nil, selectionOrigin: CGPoint, selectionSize: CGSize, inset: CGFloat, liftedInset: CGFloat = 4.0, isDark: Bool, isLifted: Bool, isCollapsed: Bool = false, transition: ComponentTransition) { let params = Params(size: size, cornerRadius: cornerRadius, selectionOrigin: selectionOrigin, selectionSize: selectionSize, inset: inset, liftedInset: liftedInset, isDark: isDark, isLifted: isLifted, isCollapsed: isCollapsed) if self.params == params { return } self.update(params: params, transition: transition) } private func update(transition: ComponentTransition) { guard let params = self.params else { return } self.update(params: params, transition: transition) } private func updateLens(params: LensParams, transition: ComponentTransition) { guard let lensView = self.lensView else { return } if self.isApplyingLensParams { self.pendingLensParams = params return } self.isApplyingLensParams = true let previousParams = self.appliedLensParams self.appliedLensParams = params if previousParams?.isLifted != params.isLifted { self.isAnimating = true let selector = NSSelectorFromString("setLifted:animated:alongsideAnimations:completion:") var shouldScheduleUpdate = false var didProcessUpdate = false self.pendingLensParams = params if let lensView = self.lensView, let method = lensView.method(for: selector) { typealias ObjCMethod = @convention(c) (AnyObject, Selector, Bool, Bool, @escaping () -> Void, (() -> Void)?) -> Void let function = unsafeBitCast(method, to: ObjCMethod.self) function(lensView, selector, params.isLifted, !transition.animation.isImmediate, { [weak self] in guard let self else { return } let liftedInset: CGFloat = params.isLifted ? params.liftedInset : (-params.inset) lensView.bounds = CGRect(origin: CGPoint(), size: CGSize(width: params.baseFrame.width + liftedInset * 2.0, height: params.baseFrame.height + liftedInset * 2.0)) didProcessUpdate = true if shouldScheduleUpdate { DispatchQueue.main.async { [weak self] in guard let self, let pendingLensParams = self.pendingLensParams else { return } self.isApplyingLensParams = false self.pendingLensParams = nil self.updateLens(params: pendingLensParams, transition: transition) } } }, { [weak self] in guard let self else { return } if !self.isApplyingLensParams { self.isAnimating = false } self.isLiftedAnimationCompleted?() }) } if didProcessUpdate { transition.animateView { lensView.center = CGPoint(x: params.baseFrame.midX, y: params.baseFrame.midY) } self.pendingLensParams = nil self.isApplyingLensParams = false } else { shouldScheduleUpdate = true } } else { let liftedInset: CGFloat = params.isLifted ? params.liftedInset : (-params.inset) let lensBounds = CGRect(origin: CGPoint(), size: CGSize(width: params.baseFrame.width + liftedInset * 2.0, height: params.baseFrame.height + liftedInset * 2.0)) let lensCenter = CGPoint(x: params.baseFrame.midX, y: params.baseFrame.midY) let previousBounds: CGRect = lensView.bounds transition.animateView { lensView.bounds = lensBounds } lensView.layer.removeAllAnimations() lensView.bounds = lensBounds if !transition.animation.isImmediate { self.isAnimating = true } transition.setPosition(view: lensView, position: lensCenter, completion: { [weak self] flag in guard let self, flag else { return } if !self.isApplyingLensParams { self.isAnimating = false } }) // No idea why transition.animatePosition(layer: lensView.layer, from: CGPoint(x: (lensBounds.width - previousBounds.width) * 0.5, y: 0.0), to: CGPoint(), additive: true) self.isApplyingLensParams = false } } private func updateLiftedLensPosition() { // Without this, the lens won't update its bouncing animations unless it's being moved if self.isApplyingLensParams { return } guard let lensView = self.lensView else { return } guard let params = self.appliedLensParams else { return } lensView.center = CGPoint(x: params.baseFrame.midX, y: params.baseFrame.midY) } private func update(params: Params, transition: ComponentTransition) { let isFirstTime = self.params == nil let transition: ComponentTransition = isFirstTime ? .immediate : transition self.params = params transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(), size: params.size)) if let backgroundContainer = self.backgroundContainer { transition.setFrame(view: backgroundContainer, frame: CGRect(origin: CGPoint(), size: params.size)) backgroundContainer.update(size: params.size, isDark: params.isDark, transition: transition) } else if let genericBackgroundContainer = self.genericBackgroundContainer { transition.setFrame(view: genericBackgroundContainer, frame: CGRect(origin: CGPoint(), size: params.size)) } if let backgroundView = self.backgroundView { transition.setFrame(view: backgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) backgroundView.update(size: params.size, cornerRadius: params.cornerRadius ?? (params.size.height * 0.5), isDark: params.isDark, tintColor: GlassBackgroundView.TintColor.init(kind: .panel, color: UIColor(white: params.isDark ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition) } if self.contentView.bounds.size != params.size { self.contentView.clipsToBounds = true transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: params.size), completion: { [weak self] completed in guard let self, completed else { return } self.contentView.clipsToBounds = false }) transition.setCornerRadius(layer: self.contentView.layer, cornerRadius: params.cornerRadius ?? (params.size.height * 0.5)) self.liftedContainerView.clipsToBounds = true transition.setFrame(view: self.liftedContainerView, frame: CGRect(origin: CGPoint(), size: params.size), completion: { [weak self] completed in guard let self, completed else { return } self.liftedContainerView.clipsToBounds = false }) transition.setCornerRadius(layer: self.liftedContainerView.layer, cornerRadius: params.cornerRadius ?? (params.size.height * 0.5)) } let baseLensFrame = CGRect(origin: params.selectionOrigin, size: params.selectionSize) self.updateLens(params: LensParams(baseFrame: baseLensFrame, inset: params.inset, liftedInset: params.liftedInset, isLifted: params.isLifted), transition: transition) if let legacyContentMaskView = self.legacyContentMaskView { transition.setFrame(view: legacyContentMaskView, frame: CGRect(origin: CGPoint(), size: params.size)) } if let legacyContentMaskBlobView = self.legacyContentMaskBlobView, let legacyLiftedContentBlobMaskView = self.legacyLiftedContentBlobMaskView, let legacySelectionView = self.legacySelectionView { let lensFrame = baseLensFrame.insetBy(dx: params.inset, dy: params.inset) let effectiveLensFrame = lensFrame.insetBy(dx: params.isLifted ? -2.0 : 0.0, dy: params.isLifted ? -2.0 : 0.0) if legacyContentMaskBlobView.image?.size.height != lensFrame.height { legacyContentMaskBlobView.image = generateStretchableFilledCircleImage(diameter: lensFrame.height, color: .black) legacyLiftedContentBlobMaskView.image = legacyContentMaskBlobView.image legacySelectionView.image = generateStretchableFilledCircleImage(diameter: lensFrame.height, color: .white)?.withRenderingMode(.alwaysTemplate) } transition.setFrame(view: legacyContentMaskBlobView, frame: effectiveLensFrame) transition.setFrame(view: legacyLiftedContentBlobMaskView, frame: effectiveLensFrame) legacySelectionView.tintColor = UIColor(white: params.isDark ? 1.0 : 0.0, alpha: params.isDark ? 0.1 : 0.075) transition.setFrame(view: legacySelectionView, frame: effectiveLensFrame) } transition.setFrame(view: self.restingBackgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) self.restingBackgroundView.update(isDark: params.isDark) transition.setAlpha(view: self.restingBackgroundView, alpha: (params.isLifted || params.isCollapsed) ? 0.0 : 1.0) if params.isLifted { if self.liftedDisplayLink == nil { self.liftedDisplayLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] _ in guard let self else { return } self.updateLiftedLensPosition() }) } } else if let liftedDisplayLink = self.liftedDisplayLink { self.liftedDisplayLink = nil liftedDisplayLink.invalidate() } } }