Skip to content

Instantly share code, notes, and snippets.

@RohanKapurDEV
Forked from m1guelpf/ColorExt.swift
Created April 27, 2024 11:30
Show Gist options
  • Save RohanKapurDEV/03d4614795ec6b917fa2da955dd773a9 to your computer and use it in GitHub Desktop.
Save RohanKapurDEV/03d4614795ec6b917fa2da955dd773a9 to your computer and use it in GitHub Desktop.
Source for the Underlay demo
import SwiftUI
extension Color {
static var background: Color {
return Color(uiColor: .systemBackground)
}
static var secondaryBackground: Color {
return Color(uiColor: .secondarySystemBackground)
}
}
import SwiftUI
private let window = {
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
return windowScene?.windows.first
}()
struct UnderlayPage: View {
@State var viewSize: CGSize = .zero
@State var isCollapsed: Bool = false
@State var transitionPercentage: CGFloat = 0
@State var url: URL = .init(string: "https://worldcoin.org/blog/announcements/introducing-world-id-2.0")!
var dragGesture: some Gesture {
DragGesture()
.onChanged { gesture in
transitionPercentage = min(1, max(0, gesture.translation.height / 400))
}
.onEnded { _ in
if transitionPercentage > 0.15 {
isCollapsed.toggle()
}
transitionPercentage = 0
}
}
var frameWidth: CGFloat {
if isCollapsed {
return viewSize.width * (0.9 + (0.1 * transitionPercentage))
}
return window?.bounds.width ?? viewSize.width
}
var frameHeight: CGFloat {
if isCollapsed {
return viewSize.height * (0.5 + (0.5 * transitionPercentage))
}
return viewSize.height
}
var grabberHeight: CGFloat {
if isCollapsed {
return 4 + (36 * transitionPercentage)
}
return 40
}
var body: some View {
ZStack {
GeometryReader { proxy in
Color.clear
.frame(maxHeight: .infinity)
.onAppear {
print(proxy.safeAreaInsets.top)
viewSize = proxy.size
}
}
Color.background
.ignoresSafeArea()
VStack {
ScrollViewReader { scroller in
ScrollView(showsIndicators: false) {
EmptyView()
.id("topOfScroll")
exampleContent
}.onChange(of: isCollapsed) {
withAnimation(.spring(.smooth(duration: 0.1))) {
scroller.scrollTo("topOfScroll", anchor: .top)
}
}
}
.overlay(alignment: .bottom) {
VariableBlur(maxBlurRadius: 1, direction: .blurredBottomClearTop, startOffset: -0.1)
.frame(height: 57)
.gesture(dragGesture)
.allowsHitTesting(isCollapsed)
}
.overlay(alignment: .bottom) {
Button(action: { withAnimation { isCollapsed.toggle() } }) {
HStack {
Image(systemName: "lock.fill")
.symbolRenderingMode(.hierarchical)
.foregroundStyle(.secondary)
.imageScale(.small)
Text(url.host()!)
.font(.footnote)
.foregroundStyle(.secondary)
}
.padding(.horizontal)
.opacity(isCollapsed ? transitionPercentage : 1)
}
.foregroundStyle(.primary)
.frame(height: grabberHeight)
.background(.ultraThinMaterial)
.shadow(color: .primary.opacity(0.2), radius: 10)
.clipShape(RoundedRectangle(cornerRadius: 24))
.offset(y: isCollapsed ? -10 : -20)
}
.clipShape(RoundedRectangle(cornerRadius: 12))
.shadow(color: .primary.opacity(0.15), radius: 18, x: 2, y: 2)
.frame(width: frameWidth, height: frameHeight, alignment: .top)
VStack(alignment: .center, spacing: 16) {
HStack {
Button(action: {}) {
Image(systemName: "chevron.left")
}
.foregroundStyle(.secondary)
Button(action: {}) {
Image(systemName: "chevron.right")
.foregroundStyle(.secondary)
}
.foregroundStyle(.secondary)
Spacer()
Image(systemName: "lock.fill")
.imageScale(.small)
.foregroundStyle(.secondary)
Text(url.host()!)
.font(.footnote)
.foregroundStyle(.secondary)
Spacer()
Button(action: { withAnimation { isCollapsed.toggle() } }) {
Image(systemName: "xmark")
}.foregroundStyle(.secondary)
}
HStack(spacing: 8) {
VStack(spacing: 6) {
Image(systemName: "shared.with.you")
.imageScale(.large)
.font(.system(size: 28))
.symbolRenderingMode(.hierarchical)
Text("Add to Circle")
}
.padding()
.frame(maxWidth: .infinity)
.frame(height: 100)
.background(Color.secondaryBackground)
.clipShape(RoundedRectangle(cornerRadius: 12))
VStack(spacing: 6) {
Image(systemName: "sparkles.rectangle.stack.fill")
.imageScale(.large)
.font(.system(size: 28))
.symbolRenderingMode(.hierarchical)
Text("Summarize")
}
.padding()
.frame(maxWidth: .infinity)
.frame(height: 100)
.background(Color.secondaryBackground)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
.foregroundStyle(.secondary)
VStack(spacing: 8) {
HStack {
Image(systemName: "doc.text.magnifyingglass")
.imageScale(.small)
.font(.system(size: 24))
.foregroundStyle(.secondary)
.symbolRenderingMode(.hierarchical)
Text("Find on Page")
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.secondaryBackground)
.clipShape(RoundedRectangle(cornerRadius: 12))
ShareLink(item: url) {
HStack {
Image(systemName: "square.and.arrow.up")
.imageScale(.small)
.font(.system(size: 24))
.foregroundStyle(.secondary)
.symbolRenderingMode(.hierarchical)
Text("Share")
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.secondaryBackground)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
.foregroundStyle(.primary)
HStack {
Image(systemName: "gear")
.imageScale(.small)
.font(.system(size: 24))
.foregroundStyle(.secondary)
.symbolRenderingMode(.hierarchical)
Text("Site Settings")
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.secondaryBackground)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
.padding(.vertical, 10)
.padding(.horizontal, 24)
.opacity(isCollapsed ? 1 : 0)
.frame(minHeight: 0, alignment: .top)
}
.sensoryFeedback(.impact(weight: .medium), trigger: isCollapsed)
.animation(.spring(.bouncy(duration: 0.35)), value: isCollapsed)
}
}
var exampleContent: some View {
ZStack {
Color.background
VStack(alignment: .center, spacing: 16) {
VStack {
Image("worldcoin-wordmark")
.resizable()
.scaledToFit()
.foregroundStyle(.primary)
.foregroundStyle(Color.primary.gradient)
}
.frame(maxWidth: .infinity, maxHeight: 100)
.background(Color.secondaryBackground.gradient)
VStack(alignment: .leading, spacing: 12) {
VStack {
Text("Introducing World ID 2.0")
.tracking(0.4)
.textCase(.uppercase)
.foregroundStyle(.secondary)
.font(.subheadline.bold())
.frame(maxWidth: .infinity, alignment: .leading)
Text("A human passport for the internet")
.tracking(-0.45)
.lineSpacing(1.00)
.foregroundStyle(.primary)
.font(.system(size: 28).weight(.semibold))
.frame(maxWidth: .infinity, alignment: .leading)
}
VStack(alignment: .leading, spacing: 16) {
Text("World ID lets you prove you’re a unique human on the internet while keeping your identity private.")
Text("It’s designed to give you full control, and is built as an open protocol meant to be owned by all people.")
Image("orb-passport")
.resizable()
.scaledToFill()
.clipShape(RoundedRectangle(cornerRadius: 8))
Text("World ID 2.0 will introduce Apps, a new way to build and use integrations to verify your online accounts using World ID.")
Text("You can explore available apps on the new Worldcoin App Store, including the new integrations with Reddit, Discord, Shopify, Minecraft and Telegram.")
Text("As part of this upgrade, World ID will have three Levels that enable a wider range of use cases, including Orb+ with face authentication so only you get to use your World ID on important actions.")
Text("Lastly, the second generation protocol features a series of core upgrades – such as the ability to return to an Orb to reset your World ID if it ever gets lost – that cement World ID as the most secure, private and inclusive proof of humanity.")
Text("The World ID 2.0 upgrade begins today and continues gradually through early 2024.")
Link("Learn more ↗", destination: URL(string: "https://worldcoin.org/blog/announcements/introducing-world-id-2.0")!)
.foregroundStyle(.blue.gradient)
.fontDesign(.default)
}
.foregroundStyle(.secondary)
.fontDesign(.serif)
}
.padding(.bottom, 100)
.frame(width: 320)
}
}
}
}
#Preview {
UnderlayPage()
}
import UIKit
import SwiftUI
import QuartzCore
import CoreImage.CIFilterBuiltins
public enum VariableBlurDirection {
case blurredTopClearBottom
case blurredBottomClearTop
}
// from https://github.com/nikstar/VariableBlur
public struct VariableBlur: UIViewRepresentable {
public var maxBlurRadius: CGFloat = 20
public var direction: VariableBlurDirection = .blurredTopClearBottom
/// By default, variable blur starts from 0 blur radius and linearly increases to `maxBlurRadius`. Setting `startOffset` to a small negative coefficient (e.g. -0.1) will start blur from larger radius value which might look better in some cases.
public var startOffset: CGFloat = 0
public func makeUIView(context _: Context) -> VariableBlurUIView {
VariableBlurUIView(maxBlurRadius: maxBlurRadius, direction: direction, startOffset: startOffset)
}
public func updateUIView(_: VariableBlurUIView, context _: Context) {}
}
/// credit https://github.com/jtrivedi/VariableBlurView
open class VariableBlurUIView: UIVisualEffectView {
public init(maxBlurRadius: CGFloat = 20, direction: VariableBlurDirection = .blurredTopClearBottom, startOffset: CGFloat = 0) {
super.init(effect: UIBlurEffect(style: .regular))
// `CAFilter` is a private QuartzCore class that we dynamically declare in `CAFilter.h`.
// let variableBlur = CAFilter.filter(withType: "variableBlur") as! NSObject
// Same but no need for `CAFilter.h`.
let CAFilter = NSClassFromString("CAFilter")! as! NSObject.Type
let variableBlur = CAFilter.perform(NSSelectorFromString("filterWithType:"), with: "variableBlur").takeRetainedValue() as! NSObject
// The blur radius at each pixel depends on the alpha value of the corresponding pixel in the gradient mask.
// An alpha of 1 results in the max blur radius, while an alpha of 0 is completely unblurred.
let gradientImage = makeGradientImage(startOffset: startOffset, direction: direction)
variableBlur.setValue(maxBlurRadius, forKey: "inputRadius")
variableBlur.setValue(gradientImage, forKey: "inputMaskImage")
variableBlur.setValue(true, forKey: "inputNormalizeEdges")
// We use a `UIVisualEffectView` here purely to get access to its `CABackdropLayer`,
// which is able to apply various, real-time CAFilters onto the views underneath.
let backdropLayer = subviews.first?.layer
// Replace the standard filters (i.e. `gaussianBlur`, `colorSaturate`, etc.) with only the variableBlur.
backdropLayer?.filters = [variableBlur]
// Get rid of the visual effect view's dimming/tint view, so we don't see a hard line.
for subview in subviews.dropFirst() {
subview.alpha = 0
}
}
@available(*, unavailable)
public required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override open func didMoveToWindow() {
// fixes visible pixelization at unblurred edge (https://github.com/nikstar/VariableBlur/issues/1)
guard let window, let backdropLayer = subviews.first?.layer else { return }
backdropLayer.setValue(window.screen.scale, forKey: "scale")
}
override open func traitCollectionDidChange(_: UITraitCollection?) {
// `super.traitCollectionDidChange(previousTraitCollection)` crashes the app
}
private func makeGradientImage(width: CGFloat = 100, height: CGFloat = 100, startOffset: CGFloat, direction: VariableBlurDirection) -> CGImage { // much lower resolution might be acceptable
let ciGradientFilter = CIFilter.smoothLinearGradient()
ciGradientFilter.color0 = CIColor.black
ciGradientFilter.color1 = CIColor.clear
ciGradientFilter.point0 = CGPoint(x: 0, y: height)
ciGradientFilter.point1 = CGPoint(x: 0, y: startOffset * height) // small negative value looks better with vertical lines
if case .blurredBottomClearTop = direction {
ciGradientFilter.point0.y = 0
ciGradientFilter.point1.y = height - ciGradientFilter.point1.y
}
return CIContext().createCGImage(ciGradientFilter.outputImage!, from: CGRect(x: 0, y: 0, width: width, height: height))!
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment