Files
letro-ios/ElementX/Sources/Screens/HomeScreen/View/BloomModifier.swift
2025-09-26 16:38:55 +03:00

118 lines
4.2 KiB
Swift

//
// Copyright 2023, 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Compound
import Foundation
import SwiftUI
import SwiftUIIntrospect
extension View {
@ViewBuilder
func bloom() -> some View {
if #available(iOS 26, *) {
modifier(BloomModifier())
} else {
modifier(OldBloomModifier())
}
}
}
private struct BloomModifier: ViewModifier {
@State private var height = CGFloat.zero
func body(content: Content) -> some View {
content
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.safeAreaInsets.top
} action: { height in
self.height = height
}
.overlay(alignment: .top) {
LinearGradient(gradient: .compound.subtle,
startPoint: .top,
endPoint: .init(x: 0.5, y: 0.35))
.ignoresSafeArea(edges: .all)
.frame(height: height)
.allowsHitTesting(false)
// Does not render properly on dark themes otherwise
.colorScheme(.light)
}
}
}
private struct OldBloomModifier: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
@State private var standardAppearance = UINavigationBarAppearance()
@State private var scrollEdgeAppearance = UINavigationBarAppearance()
@State private var bloom = Bloom()
func body(content: Content) -> some View {
content
.introspect(.viewController, on: .supportedVersions, customize: configureBloom)
}
private func configureBloom(controller: UIViewController) {
if controller.navigationItem.standardAppearance == standardAppearance,
controller.navigationItem.scrollEdgeAppearance == scrollEdgeAppearance,
canUse(bloom) {
return
}
let bloom = makeBloom()
standardAppearance.configureWithDefaultBackground()
standardAppearance.backgroundImage = bloom.image
standardAppearance.backgroundImageContentMode = .scaleToFill
controller.navigationItem.standardAppearance = standardAppearance
scrollEdgeAppearance.configureWithTransparentBackground()
scrollEdgeAppearance.backgroundImage = bloom.image
scrollEdgeAppearance.backgroundImageContentMode = .scaleToFill
scrollEdgeAppearance.backgroundColor = .compound.bgCanvasDefault
controller.navigationItem.scrollEdgeAppearance = scrollEdgeAppearance
}
private func makeBloom() -> Bloom {
if bloom.image != nil, canUse(bloom) {
return bloom
}
// There's a bug somewhere when rendering in dark mode (which we've mistakenly not been doing)
// which results in the first 5 stops not having any alpha, only the last one
let newImage = ImageRenderer(content: bloomGradient /* .colorScheme(colorScheme) */ ).uiImage
bloom.image = newImage
bloom.colorScheme = colorScheme
bloom.baseColor = .compound.gradientSubtleStop1
return bloom
}
private var bloomGradient: some View {
LinearGradient(gradient: .compound.subtle,
startPoint: .top,
endPoint: .init(x: 0.5, y: 0.7))
.ignoresSafeArea(edges: .all)
.frame(width: 256, height: 256)
}
private func canUse(_ bloom: Bloom) -> Bool {
// Don't check for a nil image in here, there's no point re-rendering over and over if the render fails.
bloom.colorScheme == colorScheme && bloom.baseColor == .compound.gradientSubtleStop1
}
// This is a class to avoid a "Modifying state during view update" warning when storing
// the result on the same run-loop - we want to avoid dispatching that to the next loop as
// that can result in further (unnecessary) renders being made.
class Bloom {
var image: UIImage?
var colorScheme: ColorScheme?
var baseColor: Color?
}
}