Files
purchases-ios/RevenueCatUI/Views/GradientView.swift
T

189 lines
6.8 KiB
Swift

//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// GradientView.swift
//
// Created by Mark Villacampa on 2024-11-24.
import SwiftUI
#if !os(macOS) && !os(tvOS) // For Paywalls V2
struct GradientView: View {
enum GradientStyle {
case linear(Int)
case radial
}
@Environment(\.colorScheme)
private var colorScheme
let lightGradient: Gradient
let darkGradient: Gradient?
let gradientStyle: GradientStyle
private var gradient: Gradient {
switch colorScheme {
case .light:
return lightGradient
case .dark:
return darkGradient ?? lightGradient
@unknown default:
return lightGradient
}
}
// Calculate the start and end points of the gradient following the CSS linear-gradient spec
// https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient
// Heavily inspired by this blog post by Mukhtar Bimurat
// https://link.medium.com/LczMO6j8YQb
private func calculatePoints(angle: Angle, rect: CGRect) -> (start: UnitPoint, end: UnitPoint) {
// Calculate the diagonal of the rectangle using the Pythagorean theorem
let diagonal = sqrt(pow(rect.width, 2) + pow(rect.height, 2))
// Calculates the angle between the rectangle's diagonal and its width
let angleBetweenDiagonalAndWidth = acos(rect.width / diagonal)
// Convert the angle to radians and negate to make clockwise
// Subtract 3π/2 (270 degrees) to follow CSS's angle convention.
let angleInRadians = -angle.radians - (3 * .pi / 2)
let normalizedAngle = angleInRadians.truncatingRemainder(dividingBy: .pi * 2)
// Calculate the angle between the diagonal and the gradient line
let angleBetweenDiagonalAndGradientLine: CGFloat
if (normalizedAngle > .pi / 2 && normalizedAngle < .pi) ||
(normalizedAngle > 3 * .pi / 2 && normalizedAngle < 2 * .pi) {
angleBetweenDiagonalAndGradientLine = CGFloat.pi - angleInRadians - angleBetweenDiagonalAndWidth
} else {
angleBetweenDiagonalAndGradientLine = angleInRadians - angleBetweenDiagonalAndWidth
}
// Get half the length of the gradient line, and calculate the vertical and horizontal offsets from the center
let halfGradientLine = abs(cos(angleBetweenDiagonalAndGradientLine) * diagonal) / 2
let horizontalOffset = halfGradientLine * cos(angleInRadians)
let verticalOffset = halfGradientLine * sin(angleInRadians)
// Convert the start and end points to UnitPoint coordinates (0-1 range)
let centerX = 0.5
let centerY = 0.5
let start = UnitPoint(x: centerX - horizontalOffset / rect.width, y: centerY + verticalOffset / rect.height)
let end = UnitPoint(x: centerX + horizontalOffset / rect.width, y: centerY - verticalOffset / rect.height)
return (start, end)
}
var body: some View {
switch gradientStyle {
case .linear(let degrees):
GeometryReader { geometry in
let points = calculatePoints(angle: .degrees(Double(degrees)), rect: geometry.frame(in: .local))
LinearGradient(
gradient: gradient,
startPoint: points.start,
endPoint: points.end
)
}
case .radial:
RadialGradient(
gradient: gradient,
center: .center,
startRadius: 0,
endRadius: 100
)
}
}
}
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
struct GradientView_Previews: PreviewProvider {
static private func gradientView(style: GradientView.GradientStyle) -> some View {
GradientView(
lightGradient: .init(stops: [
.init(color: .blue, location: 0),
.init(color: .red, location: 0.5),
.init(color: .black, location: 1)
]),
darkGradient: .init(stops: [
.init(color: .red, location: 0),
.init(color: .blue, location: 0.5),
.init(color: .black, location: 1)
]),
gradientStyle: style
)
}
static var previews: some View {
GradientView(
lightGradient: .init(colors: .init([.red, .white])),
darkGradient: .init(colors: .init([.blue, .white])),
gradientStyle: .radial
)
.preferredColorScheme(.dark)
.previewLayout(.sizeThatFits)
.previewDisplayName("Radial - Dark (should be blue)")
GradientView(
lightGradient: .init(colors: .init([.red, .white])),
darkGradient: .init(colors: .init([.blue, .white])),
gradientStyle: .radial
)
.previewLayout(.sizeThatFits)
.previewDisplayName("Radial - Light (should be red)")
GradientView(
lightGradient: .init(colors: .init([.red, .white])),
darkGradient: .init(colors: .init([.blue, .white])),
gradientStyle: .linear(45)
)
.preferredColorScheme(.dark)
.previewLayout(.sizeThatFits)
.previewDisplayName("Linear 45º - Dark (should be blue)")
GradientView(
lightGradient: .init(colors: .init([.red, .white])),
darkGradient: .init(colors: .init([.blue, .white])),
gradientStyle: .linear(90)
)
.previewLayout(.sizeThatFits)
.previewDisplayName("Linear 90º - Light (should be red)")
VStack {
LinearGradientPreview(label: "Linear 0º", degrees: 0)
LinearGradientPreview(label: "Linear 45º", degrees: 45)
LinearGradientPreview(label: "Linear 90º", degrees: 90)
LinearGradientPreview(label: "Linear 135º", degrees: 135)
LinearGradientPreview(label: "Linear 180º", degrees: 180)
LinearGradientPreview(label: "Linear 225º", degrees: 225)
LinearGradientPreview(label: "Linear 270º", degrees: 270)
}
.previewLayout(.sizeThatFits)
.previewDisplayName("Linear")
}
/// Helper view to preview linear gradients with different angles. This is useful so to keep down the
/// number of views in the container holding all of the previews to avoid compilation issues
/// with the preview.
private struct LinearGradientPreview: View {
let label: String
let degrees: Int
var body: some View {
VStack {
Text(label)
gradientView(style: .linear(degrees))
}
}
}
}
#endif