Skip to content

Instantly share code, notes, and snippets.

@raproenca
Created March 12, 2025 19:57
Show Gist options
  • Save raproenca/fde28b0c162f3f97714b9db5cb79dad6 to your computer and use it in GitHub Desktop.
Save raproenca/fde28b0c162f3f97714b9db5cb79dad6 to your computer and use it in GitHub Desktop.
SplashScreen and Icon created using SwiftUI for my app Steady Workout Tracker
//
// SplashView.swift
// Steady Gym Workout Tracker
// https://steady.rocks
// Created by Rafael Proença
//
import SwiftUI
struct ScaledRoundedRectangle: Shape {
var cornerRadius: CGFloat
var rectSize: CGSize
var offsetY: CGFloat
var scale: CGFloat
var animatableData: CGFloat {
get { scale }
set { scale = newValue }
}
func path(in rect: CGRect) -> Path {
let scaledWidth = rectSize.width * scale
let scaledHeight = rectSize.height * scale
let x = rect.midX - scaledWidth / 2
let y = rect.midY - scaledHeight / 2 + offsetY * scale
let newRect = CGRect(x: x, y: y, width: scaledWidth, height: scaledHeight)
return RoundedRectangle(cornerRadius: cornerRadius * scale).path(in: newRect)
}
}
struct BarbellView: View {
let horizontalOffset = -53.0
var body: some View {
Group {
// Bar
RoundedRectangle(cornerRadius: 10)
.frame(width: 375, height: 38)
.offset(x: horizontalOffset + 120)
// Smaller plate
RoundedRectangle(cornerRadius: 10)
.frame(width: 44, height: 90)
.offset(x: horizontalOffset)
// Medium plate
RoundedRectangle(cornerRadius: 10)
.frame(width: 44, height: 165)
.offset(x: horizontalOffset + 53)
// Bigger plate
RoundedRectangle(cornerRadius: 10)
.frame(width: 44, height: 240)
.offset(x: horizontalOffset + 106)
}
//.foregroundStyle(.black)
.offset(y: -10)
}
}
struct ColoredBarbellView: View {
var previewMode: Bool = false
let horizontalOffset = -53.0
@State private var smallPlateHeight: CGFloat = 0
@State private var mediumPlateHeight: CGFloat = 0
@State private var bigPlateHeight: CGFloat = 0
@State private var barOpacity: CGFloat = 0 // Add this for bar animation
init(previewMode: Bool = false) {
self.previewMode = previewMode
_ = previewMode ? 1.0 : 0.0
_smallPlateHeight = State(initialValue: previewMode ? 90 : 0)
_mediumPlateHeight = State(initialValue: previewMode ? 165 : 0)
_bigPlateHeight = State(initialValue: previewMode ? 240 : 0)
}
var body: some View {
ZStack {
Group {
RoundedRectangle(cornerRadius: 10)
.fill(Color.white.gradient) // This actually fills the shape
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.black.opacity(0.5), lineWidth: 0.5) // Keeps the stroke on top
)
.frame(width: 44, height: smallPlateHeight)
.offset(x: horizontalOffset)
RoundedRectangle(cornerRadius: 10)
.fill(Color.white.gradient) // This actually fills the shape
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.black.opacity(0.5), lineWidth: 0.5) // Keeps the stroke on top
)
.frame(width: 44, height: mediumPlateHeight)
.offset(x: horizontalOffset + 53)
RoundedRectangle(cornerRadius: 10)
.fill(Color.white.gradient)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.black.opacity(0.5), lineWidth: 0.5) // Keeps the stroke on top
)
.frame(width: 44, height: bigPlateHeight)
.offset(x: horizontalOffset + 106)
}
}
.offset(y: -10)
.onAppear {
// Animate plates one by one
HapticsManager.shared.impact(type: .light)
withAnimation(.bouncy(duration: 0.5)) {
smallPlateHeight = 90
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
HapticsManager.shared.impact(type: .medium)
withAnimation(.bouncy(duration: 0.5)) {
mediumPlateHeight = 165
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
HapticsManager.shared.impact(type: .heavy)
withAnimation(.bouncy(duration: 0.5)) {
bigPlateHeight = 240
}
}
// Animate bar last, after all plates
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
withAnimation(.snappy(duration: 0.5)) {
barOpacity = 1
}
}
}
}
}
struct SplashView: View {
var previewMode: Bool = false
@State private var squareScale: CGFloat
@State private var barbellOpacity: CGFloat
@State private var bgOpacity: CGFloat
@State private var showColoredBarbell: Bool
init(previewMode: Bool = false) {
self.previewMode = previewMode
_squareScale = State(initialValue: previewMode ? 1.0 : 50)
_barbellOpacity = State(initialValue: previewMode ? 0 : 1)
_bgOpacity = State(initialValue: previewMode ? 0 : 1)
_showColoredBarbell = State(initialValue: previewMode ? true : false)
}
var body: some View {
ZStack {
Color.almostWhite
.ignoresSafeArea()
.opacity(bgOpacity)
// Normal barbell
BarbellView()
.foregroundStyle(.indigo.opacity(0.5))
//.opacity(1)
.opacity(barbellOpacity)
// Animated square
ScaledRoundedRectangle(
cornerRadius: 30,
rectSize: CGSize(width: 200, height: 200),
offsetY: -90,
scale: squareScale
)
.fill(
LinearGradient(
gradient: Gradient(colors: [
Color.accentColor,
Color.accentColor.mix(with: .black, by: 0.6)
]),
startPoint: .top,
endPoint: .bottom
)
.opacity(1 * squareScale)
)
.stroke(Color.indigo, lineWidth: 1)
.onTapGesture {
resetAnimation()
animationStart()
}
// Calculate shadow opacity inversely to the scale
// As squareScale goes from 50 to 1, shadowOpacity goes from 0 to 0.5
.shadow(color: .accentColor.mix(with: .black, by: 0.3).opacity(max(0, min(0.3, (50 - squareScale) / 98))), radius: 16, x: 3, y: 3)
.shadow(color: .black.opacity(0.5).opacity(max(0, min(0.5, (50 - squareScale) / 98))), radius: 3, x: 3, y: 3)
// Inverted barbell, clipped to the square, must remain on top of the ScaledRoundedRectangle to give the desired effect.
BarbellView()
.foregroundStyle(.indigo.gradient)
//.colorInvert() // Invert colors
.clipShape(
ScaledRoundedRectangle(
cornerRadius: 30,
rectSize: CGSize(width: 200, height: 200),
offsetY: -90,
scale: squareScale
)
)
.opacity(barbellOpacity)
if showColoredBarbell {
ColoredBarbellView(previewMode: previewMode)
.clipShape(
ScaledRoundedRectangle(
cornerRadius: 30,
rectSize: CGSize(width: 200, height: 200),
offsetY: -90,
scale: squareScale
)
)
.opacity(squareScale)
}
}
.onAppear {
if !previewMode {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
animationStart()
}
}
}
}
private func resetAnimation() {
squareScale = 50
barbellOpacity = 1
bgOpacity = 1
showColoredBarbell = false
}
private func animationStart() {
// After a delay, animate the square appearing
withAnimation(.snappy(duration: 1.5)) {
squareScale = 1.0
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
//HapticsManager.shared.impact(type: .heavy)
showColoredBarbell = true
withAnimation(.easeInOut(duration: 2)) {
bgOpacity = 0
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
//HapticsManager.shared.impact(type: .light)
withAnimation(.easeInOut(duration: 2)) {
barbellOpacity = 0
}
}
}
}
#Preview {
SplashView(previewMode: false)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment