Skip to content

Instantly share code, notes, and snippets.

@luthviar
Last active October 14, 2025 08:28
Show Gist options
  • Select an option

  • Save luthviar/482a0d28a921c2b704f222eb6ec6d410 to your computer and use it in GitHub Desktop.

Select an option

Save luthviar/482a0d28a921c2b704f222eb6ec6d410 to your computer and use it in GitHub Desktop.
//
// ContentView.swift
// Try14Oct2025
//
// Created by Luthfi Abdurrahim on 14/10/25.
//
import SwiftUI
struct ContentView: View {
@State private var selectedClipIndex: Int? = nil
@State private var pressedButton: String? = nil
@State private var timelineLabelPositions: [Int: CGFloat] = [:]
let clips = [
VideoClip(id: 1, time: "05:12", name: "5 Klip", duration: 42),
VideoClip(id: 2, time: "05:54", name: "3 Klip", duration: 34),
VideoClip(id: 3, time: "06:28", name: "6 Klip", duration: 33),
VideoClip(id: 4, time: "07:01", name: "6 Klip", duration: 33)
]
var body: some View {
ZStack {
Color(hex: "F5F5F5")
.ignoresSafeArea()
VStack(spacing: 0) {
cameraControlsBar()
mainContentArea()
}
}
}
func cameraControlsBar() -> some View {
// Top toolbar
HStack(spacing: 20) {
Button(action: {}) {
Image(systemName: "video.fill")
.font(.system(size: 24))
.foregroundColor(.white)
}
.buttonStyle(ScaleButtonStyle())
Spacer()
Button(action: {}) {
Image(systemName: "camera.fill")
.font(.system(size: 24))
.foregroundColor(.white)
}
.buttonStyle(ScaleButtonStyle())
Spacer()
Button(action: {}) {
Image(systemName: "speaker.slash.fill")
.font(.system(size: 24))
.foregroundColor(.white)
}
.buttonStyle(ScaleButtonStyle())
Spacer()
Button(action: {}) {
Image(systemName: "arrow.up.left.and.arrow.down.right")
.font(.system(size: 22))
.foregroundColor(.white)
}
.buttonStyle(ScaleButtonStyle())
}
.padding(.horizontal, 20)
.padding(.vertical, 16)
.background(Color(hex: "2C2C2E"))
}
func mainContentArea() -> some View {
ScrollView {
VStack(spacing: 0) {
// Top controls
HStack(spacing: 12) {
Button(action: {}) {
Image(systemName: "trash")
.resizable()
.scaledToFit()
.frame(width: 18)
.foregroundColor(Color(hex: "3C3C3E"))
}
.buttonStyle(ScaleButtonStyle())
.padding(.leading, 20)
Spacer()
CustomSegmentedControl()
.frame(width: 100)
Spacer()
HStack(spacing: 8) {
Image(systemName: "calendar")
.font(.system(size: 16))
.foregroundColor(Color(hex: "3C3C3E"))
Text("10 October 2025")
.font(.system(size: 14, weight: .medium))
.foregroundColor(Color(hex: "1C1C1E"))
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(
Capsule()
.stroke(Color(hex: "D1D1D6"), lineWidth: 1.5)
.background(Color.white)
.clipShape(Capsule())
)
.padding(.trailing, 20)
}
.padding(.top, 28)
.padding(.bottom, 36)
// Timeline with clips
ZStack(alignment: .topLeading) {
let timelineLeftPadding: CGFloat = 80
let cardWidth: CGFloat = 200
let cardHeight: CGFloat = 130
let indicatorOffset = selectedClipIndex != nil ? (timelineLabelPositions[selectedClipIndex!] ?? 10) : 10
// Timeline vertical line
Rectangle()
.fill(Color(hex: "3C3C3E"))
.frame(width: 4)
.padding(.leading, timelineLeftPadding)
VStack(spacing: 0) {
ForEach(Array(clips.enumerated()), id: \.element.id) { index, clip in
HStack(alignment: .top, spacing: 0) {
// Time label - aligned to top
Text(clip.time)
.font(.system(size: 18, weight: .regular))
.foregroundColor(Color(hex: "8E8E93"))
.frame(width: 60, alignment: .trailing)
.padding(.leading, 10)
.padding(.top, 4)
.padding(.trailing, 30)
.background(
GeometryReader { geo in
Color.clear
.preference(key: TimelineLabelPositionKey.self,
value: [index: geo.frame(in: .named("timelineContainer")).minY])
}
)
// Video clip card
Button(action: {
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
selectedClipIndex = selectedClipIndex == index ? nil : index
}
}) {
ZStack(alignment: .center) {
Image(.icClipMulti)
.resizable()
.scaledToFit()
.frame(width: cardWidth)
Text(clip.name)
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white.opacity(0.95))
}
.frame(width: cardWidth, height: cardHeight)
.scaleEffect(selectedClipIndex == index ? 0.96 : 1.0)
.opacity(selectedClipIndex == index ? 0.9 : 1.0)
.padding(.bottom, -20)
.padding(.top, -20)
.padding(.leading, -30)
}
.buttonStyle(PlainButtonStyle())
Spacer()
}
.padding(.bottom, index < clips.count - 1 ? 28 : 0)
}
}
// Red timeline indicator (horizontal) - positioned at selected clip time label
HStack(spacing: 0) {
Spacer()
.frame(width: timelineLeftPadding)
Rectangle()
.fill(Color(hex: "FF3B30"))
.frame(height: 4)
.padding(.trailing, 20)
}
.padding(.top, indicatorOffset)
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: selectedClipIndex)
}
.coordinateSpace(name: "timelineContainer")
.onPreferenceChange(TimelineLabelPositionKey.self) { positions in
timelineLabelPositions = positions
}
.padding(.bottom, 40)
}
}
}
}
struct VideoClip: Identifiable {
let id: Int
let time: String
let name: String
let duration: Int
}
// Button style for scale effect on press
struct ScaleButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 0.92 : 1.0)
.opacity(configuration.isPressed ? 0.7 : 1.0)
.animation(.easeInOut(duration: 0.15), value: configuration.isPressed)
}
}
// Color extension for hex colors
extension Color {
init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 3: // RGB (12-bit)
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (1, 1, 1, 0)
}
self.init(
.sRGB,
red: Double(r) / 255,
green: Double(g) / 255,
blue: Double(b) / 255,
opacity: Double(a) / 255
)
}
}
#Preview {
ContentView()
}
struct CustomSegmentedControl: View {
@State private var selectedOption: PlaybackSegmentOption = .cloud
enum PlaybackSegmentOption {
case cloud
case storage
}
var body: some View {
HStack(spacing: 0) {
// Cloud option
Button(action: {
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
selectedOption = .cloud
}
}) {
ZStack {
// Background
RoundedRectangle(cornerRadius: 25)
.fill(selectedOption == .cloud ? Color.white : Color.clear)
.shadow(color: selectedOption == .cloud ? Color.black.opacity(0.12) : Color.clear,
radius: 4, x: 0, y: 1)
.scaleEffect(0.8)
// Cloud with play icon
ZStack {
Image(systemName: "cloud.fill")
.resizable()
.scaledToFit()
.frame(width: 20)
.foregroundColor(Color(red: 0.3, green: 0.35, blue: 0.4))
Image(systemName: "play.fill")
.resizable()
.scaledToFit()
.frame(width: 5)
.foregroundColor(.white)
.offset(x: 0.5, y: 0)
}
.padding(.vertical, 2)
.padding(.horizontal, 3)
}
}
.buttonStyle(ScaleButtonStyleV2())
// Storage option
Button(action: {
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
selectedOption = .storage
}
}) {
ZStack {
// Background
RoundedRectangle(cornerRadius: 25)
.fill(selectedOption == .storage ? Color.white : Color.clear)
.shadow(color: selectedOption == .storage ? Color.black.opacity(0.12) : Color.clear,
radius: 4, x: 0, y: 1)
.scaleEffect(0.8)
// SD Card icon
ZStack {
Image(systemName: "sdcard.fill")
.resizable()
.scaledToFit()
.frame(width: 13)
.foregroundColor(Color(red: 0.3, green: 0.35, blue: 0.4))
}
.padding(.vertical, 2)
.padding(.horizontal, 3)
}
}
.buttonStyle(ScaleButtonStyleV2())
}
.frame(height: 40)
.background(
RoundedRectangle(cornerRadius: 25)
.fill(Color(UIColor.systemGray4).opacity(0.6))
.shadow(color: Color.black.opacity(0.08), radius: 6, x: 0, y: 2)
)
}
}
// Custom button style for subtle press effect
struct ScaleButtonStyleV2: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 0.96 : 1.0)
.opacity(configuration.isPressed ? 0.9 : 1.0)
.animation(.easeInOut(duration: 0.15), value: configuration.isPressed)
}
}
// PreferenceKey for tracking timeline label positions
struct TimelineLabelPositionKey: PreferenceKey {
static var defaultValue: [Int: CGFloat] = [:]
static func reduce(value: inout [Int: CGFloat], nextValue: () -> [Int: CGFloat]) {
value.merge(nextValue()) { $1 }
}
}
@luthviar
Copy link
Author

Screenshot 2025-10-14 at 14 43 42

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment