Skip to content

Instantly share code, notes, and snippets.

@mattyoung
Forked from alikaragoz/RubberBandSwitch.swift
Last active October 13, 2023 04:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mattyoung/2f6999de06f4d76da640b267e1171e07 to your computer and use it in GitHub Desktop.
Save mattyoung/2f6999de06f4d76da640b267e1171e07 to your computer and use it in GitHub Desktop.
iOS Control Center Rubber Band Swift
import SwiftUI
// had to move this out here due to RubberBandSwitch is now generic
enum Const {
static let shapeSize: CGSize = .init(width: 90.0, height: 190.0)
static let cornerRadius: CGFloat = 26.0
}
struct RubberBandSwitch<Icon: View>: View {
// @State var value = 0.5
@Binding var value: Double
var icon: (() -> Icon)?
var onLongPress: (() -> ())?
@State var hScale = 1.0
@State var vScale = 1.0
@State var anchor: UnitPoint = .center
@State var yOffset: CGFloat = 0.0
@State var isTouching = false
@State var scale: CGFloat = 1.0
@State var startValue: CGFloat = 0.0
var body: some View {
ZStack {
wallpaper
slider
.clipShape(RoundedRectangle(cornerRadius: Const.cornerRadius, style: .continuous))
.frame(width: Const.shapeSize.width, height: Const.shapeSize.height)
.scaleEffect(x: hScale, y: vScale, anchor: anchor)
.offset(x: 0, y: yOffset)
.gesture(onLongPress == nil ?
AnyGesture(dragNoLongPress.map { _ in () }) :
AnyGesture(dragWithLongPress.map { _ in () }))
}
.animation(isTouching ? .none : .smooth(duration: 0.5), value: vScale)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
var dragNoLongPress: some Gesture {
DragGesture(minimumDistance: 0)
.onChanged { drag in
withAnimation {
handleDrag(drag: drag)
}
}
.onEnded { _ in
isTouching = false
vScale = 1.0
hScale = 1.0
anchor = .center
yOffset = 0.0
}
}
var dragWithLongPress: some Gesture {
LongPressGesture(maximumDistance: 0)
.onEnded { _ in onLongPress!() }
.exclusively(before: dragNoLongPress)
}
func handleDrag(drag: DragGesture.Value) {
if !isTouching {
startValue = value
}
isTouching = true
var value = startValue - (drag.translation.height / Const.shapeSize.height)
self.value = min(max(value, 0.0), 1.0)
var anchor: UnitPoint = .center
var yOffset: CGFloat = 0.0
if value > 1 {
value = sqrt(sqrt(sqrt(value)))
yOffset = Const.shapeSize.height * (1 - value) / 2.0
anchor = .bottom
} else if value < 0 {
value = sqrt(sqrt(sqrt(1 - value)))
yOffset = -Const.shapeSize.height * (1 - value) / 2.0
anchor = .top
} else {
value = 1.0
anchor = .center
}
vScale = value
hScale = 2 - sqrt(value)
self.yOffset = yOffset
self.anchor = anchor
}
@ViewBuilder
var wallpaper: some View {
Image(.wallpaper)
.resizable()
.padding(-40)
.ignoresSafeArea()
.blur(radius: 40)
}
@ViewBuilder
var slider: some View {
ZStack {
Rectangle()
.background(.ultraThickMaterial)
VStack {
Spacer()
.frame(minHeight: 0)
Rectangle()
.frame(width: Const.shapeSize.width, height: value * Const.shapeSize.height)
.foregroundStyle(Color(uiColor: .systemGray5))
}
// if there is an icon
icon.map { icon in
VStack {
Spacer()
Spacer()
Spacer()
icon()
Spacer()
}
}
}
}
}
extension RubberBandSwitch {
init(value: Binding<Double>, icon: (() -> Icon)? = nil) where Icon == Never {
self._value = value
self.icon = icon
}
}
struct RubberBandSwitchPreviewHelper: View {
@State private var volume = 0.5
@State private var showDetailView = false
func icon() -> some View {
SpeakerVolumeView(level: volume)
.frame(height: 30)
.foregroundColor(.primary)
}
var firstView: some View {
VStack {
HStack {
RubberBandSwitch(value: $volume, icon: icon) {
withAnimation {
showDetailView = true
}
}
RubberBandSwitch<Never>(value: $volume)
}
(Text("Volume ") +
Text(volume, format: .number.precision(.fractionLength(2)))
.font(.system(size: 40).monospacedDigit()))
.contentTransition(.numericText(value: volume))
}
}
var body: some View {
if showDetailView {
DetailView(volume: $volume)
.onTapGesture {
withAnimation {
showDetailView = false
}
}
.transition(.move(edge: .leading))
} else {
firstView
.transition(.move(edge: .trailing))
}
}
}
struct DetailView: View {
@Binding var volume: Double
var body: some View {
VStack {
Spacer()
SpeakerVolumeView(level: volume)
.frame(height: 50)
.foregroundColor(.primary)
RubberBandSwitch(value: $volume)
Spacer()
}
}
}
#Preview {
RubberBandSwitchPreviewHelper()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment