Created
March 12, 2025 19:57
-
-
Save raproenca/fde28b0c162f3f97714b9db5cb79dad6 to your computer and use it in GitHub Desktop.
SplashScreen and Icon created using SwiftUI for my app Steady Workout Tracker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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