255 lines
7.5 KiB
Swift
255 lines
7.5 KiB
Swift
// Copyright 2022-present 650 Industries. All rights reserved.
|
|
|
|
import ExpoModulesCore
|
|
import React
|
|
|
|
public final class GlassView: ExpoView {
|
|
private var glassEffect: Any?
|
|
private var glassEffectView = UIVisualEffectView()
|
|
// TODO: Find a better fix
|
|
// Glass effect does not work sometimes if view has not been laid out yet
|
|
// https://github.com/expo/expo/issues/41024#issuecomment-3867466289
|
|
private var isMounted = false
|
|
|
|
private var glassStyle: GlassStyle?
|
|
private var glassTintColor: UIColor?
|
|
private var glassIsInteractive: Bool?
|
|
|
|
private var radius: CGFloat?
|
|
private var bottomLeftRadius: CGFloat?
|
|
private var bottomRightRadius: CGFloat?
|
|
private var topLeftRadius: CGFloat?
|
|
private var topRightRadius: CGFloat?
|
|
|
|
private var bottomStartRadius: CGFloat?
|
|
private var bottomEndRadius: CGFloat?
|
|
private var topStartRadius: CGFloat?
|
|
private var topEndRadius: CGFloat?
|
|
|
|
public required init(appContext: AppContext? = nil) {
|
|
super.init(appContext: appContext)
|
|
|
|
glassEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
|
|
addSubview(glassEffectView)
|
|
}
|
|
|
|
// UIGlassEffect initialiser is crashing for iOS 26 beta versions for some reason, so we need to check if it's available at runtime
|
|
// https://github.com/expo/expo/issues/40911
|
|
private func isGlassEffectAvailable() -> Bool {
|
|
#if compiler(>=6.2)
|
|
if #available(iOS 26.0, tvOS 26.0, macOS 26.0, *) {
|
|
guard let glassEffectClass = NSClassFromString("UIGlassEffect") as? NSObject.Type else {
|
|
return false
|
|
}
|
|
let respondsToSelector = glassEffectClass.responds(to: Selector(("effectWithStyle:")))
|
|
return respondsToSelector
|
|
}
|
|
#endif
|
|
return false
|
|
}
|
|
|
|
override public func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
if !isMounted {
|
|
isMounted = true
|
|
updateEffect()
|
|
}
|
|
}
|
|
|
|
func updateBorderRadius() {
|
|
guard isGlassEffectAvailable() else {
|
|
return
|
|
}
|
|
if #available(iOS 26.0, tvOS 26.0, macOS 26.0, *) {
|
|
#if compiler(>=6.2) // Xcode 26
|
|
let isRTL = RCTI18nUtil.sharedInstance()?.isRTL() ?? false
|
|
|
|
let finalTopLeft: CGFloat
|
|
let finalTopRight: CGFloat
|
|
let finalBottomLeft: CGFloat
|
|
let finalBottomRight: CGFloat
|
|
|
|
if isRTL {
|
|
finalTopLeft = topLeftRadius ?? topEndRadius ?? radius ?? 0
|
|
finalTopRight = topRightRadius ?? topStartRadius ?? radius ?? 0
|
|
finalBottomLeft = bottomLeftRadius ?? bottomEndRadius ?? radius ?? 0
|
|
finalBottomRight = bottomRightRadius ?? bottomStartRadius ?? radius ?? 0
|
|
} else {
|
|
finalTopLeft = topLeftRadius ?? topStartRadius ?? radius ?? 0
|
|
finalTopRight = topRightRadius ?? topEndRadius ?? radius ?? 0
|
|
finalBottomLeft = bottomLeftRadius ?? bottomStartRadius ?? radius ?? 0
|
|
finalBottomRight = bottomRightRadius ?? bottomEndRadius ?? radius ?? 0
|
|
}
|
|
|
|
let topLeft = UICornerRadius(floatLiteral: finalTopLeft)
|
|
let topRight = UICornerRadius(floatLiteral: finalTopRight)
|
|
let bottomLeft = UICornerRadius(floatLiteral: finalBottomLeft)
|
|
let bottomRight = UICornerRadius(floatLiteral: finalBottomRight)
|
|
|
|
glassEffectView.cornerConfiguration = .corners(
|
|
topLeftRadius: topLeft,
|
|
topRightRadius: topRight,
|
|
bottomLeftRadius: bottomLeft,
|
|
bottomRightRadius: bottomRight
|
|
)
|
|
#endif
|
|
}
|
|
}
|
|
|
|
func setGlassStyle(_ config: GlassEffectStyleConfig) {
|
|
applyGlassStyle(config.style, animate: config.animate, animationDuration: config.animationDuration)
|
|
}
|
|
|
|
func setGlassStyle(_ style: GlassStyle) {
|
|
applyGlassStyle(style)
|
|
}
|
|
|
|
private func applyGlassStyle(_ newStyle: GlassStyle, animate: Bool = false, animationDuration: Double? = nil) {
|
|
if glassStyle != newStyle {
|
|
glassStyle = newStyle
|
|
guard isGlassEffectAvailable() else {
|
|
return
|
|
}
|
|
if #available(iOS 26.0, tvOS 26.0, macOS 26.0, *) {
|
|
#if compiler(>=6.2) // Xcode 26
|
|
let applyEffect = {
|
|
if let uiStyle = newStyle.toUIGlassEffectStyle() {
|
|
self.glassEffect = UIGlassEffect(style: uiStyle)
|
|
self.updateEffect()
|
|
} else {
|
|
// TODO: revisit this in newer versions of iOS
|
|
// setting `nil` does not work as expected, so we need to set a visual effect with no effect
|
|
self.glassEffectView.effect = UIVisualEffect()
|
|
self.glassEffect = self.glassEffectView.effect
|
|
}
|
|
}
|
|
|
|
if animate {
|
|
if let duration = animationDuration {
|
|
UIView.animate(withDuration: duration, animations: applyEffect)
|
|
} else {
|
|
UIView.animate(animations: applyEffect)
|
|
}
|
|
} else {
|
|
applyEffect()
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: support UIVisualEffectView with ExpoFabricView?
|
|
func setBorderRadius(_ _radius: CGFloat?) {
|
|
if _radius != radius {
|
|
radius = _radius
|
|
updateBorderRadius()
|
|
}
|
|
}
|
|
func setBorderCurve(_: String?) {
|
|
glassEffectView.layer.cornerCurve = self.layer.cornerCurve
|
|
}
|
|
|
|
func setBorderBottomLeftRadius(_ radius: CGFloat?) {
|
|
if radius != bottomLeftRadius {
|
|
bottomLeftRadius = radius
|
|
updateBorderRadius()
|
|
}
|
|
}
|
|
|
|
func setBorderBottomRightRadius(_ radius: CGFloat?) {
|
|
if radius != bottomRightRadius {
|
|
bottomRightRadius = radius
|
|
updateBorderRadius()
|
|
}
|
|
}
|
|
|
|
func setBorderTopLeftRadius(_ radius: CGFloat?) {
|
|
if radius != topLeftRadius {
|
|
topLeftRadius = radius
|
|
updateBorderRadius()
|
|
}
|
|
}
|
|
|
|
func setBorderTopRightRadius(_ radius: CGFloat?) {
|
|
if radius != topRightRadius {
|
|
topRightRadius = radius
|
|
updateBorderRadius()
|
|
}
|
|
}
|
|
|
|
func setBorderBottomStartRadius(_ radius: CGFloat?) {
|
|
if radius != bottomStartRadius {
|
|
bottomStartRadius = radius
|
|
updateBorderRadius()
|
|
}
|
|
}
|
|
|
|
func setBorderBottomEndRadius(_ radius: CGFloat?) {
|
|
if radius != bottomEndRadius {
|
|
bottomEndRadius = radius
|
|
updateBorderRadius()
|
|
}
|
|
}
|
|
|
|
func setBorderTopStartRadius(_ radius: CGFloat?) {
|
|
if radius != topStartRadius {
|
|
topStartRadius = radius
|
|
updateBorderRadius()
|
|
}
|
|
}
|
|
|
|
func setBorderTopEndRadius(_ radius: CGFloat?) {
|
|
if radius != topEndRadius {
|
|
topEndRadius = radius
|
|
updateBorderRadius()
|
|
}
|
|
}
|
|
|
|
func setTintColor(_ color: UIColor?) {
|
|
if color != glassTintColor {
|
|
glassTintColor = color
|
|
updateEffect()
|
|
}
|
|
}
|
|
|
|
func setInteractive(_ interactive: Bool) {
|
|
if interactive != glassIsInteractive {
|
|
glassIsInteractive = interactive
|
|
updateEffect()
|
|
}
|
|
}
|
|
|
|
func setColorScheme(_ colorScheme: GlassColorScheme) {
|
|
overrideUserInterfaceStyle = colorScheme.toUIUserInterfaceStyle()
|
|
}
|
|
|
|
private func updateEffect() {
|
|
if !isMounted {
|
|
return
|
|
}
|
|
guard isGlassEffectAvailable() else {
|
|
return
|
|
}
|
|
if #available(iOS 26.0, tvOS 26.0, macOS 26.0, *) {
|
|
#if compiler(>=6.2) // Xcode 26
|
|
if let effect = glassEffect as? UIGlassEffect {
|
|
effect.tintColor = glassTintColor
|
|
effect.isInteractive = glassIsInteractive ?? false
|
|
// we need to set the effect again or it has no effect!
|
|
glassEffectView.effect = effect
|
|
updateBorderRadius()
|
|
setBorderCurve(nil)
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
public override func mountChildComponentView(_ childComponentView: UIView, index: Int) {
|
|
glassEffectView.contentView.insertSubview(childComponentView, at: index)
|
|
}
|
|
|
|
public override func unmountChildComponentView(_ childComponentView: UIView, index: Int) {
|
|
childComponentView.removeFromSuperview()
|
|
}
|
|
}
|