Skip to content

Instantly share code, notes, and snippets.

@indyfromoz
Forked from tarrouye/AMAlbumDetailsView.swift
Created October 28, 2024 20:12
Show Gist options
  • Save indyfromoz/df6e9897894c331bbb33db34d9cf0cd2 to your computer and use it in GitHub Desktop.
Save indyfromoz/df6e9897894c331bbb33db34d9cf0cd2 to your computer and use it in GitHub Desktop.
Sample code replicating the album details / track list screen from Apple Music
struct AMAlbumDetailsView: View {
let albumArt: Image
let title: String
let artist: String
let genre: String
let releaseYear: String
let quality: String
let tracks: [String]
@State private var playingTrack: String?
@State private var visibleBottomSpace: Bool = false
var body: some View {
List {
Section {
headerView
}
Section {
trackList
}
Section {
bottomDetailsStack
}
.listSectionSeparator(.hidden)
Section {
moreContentSection
.padding(.bottom)
.onGeometryChange(for: Bool.self) { geo in
geo.frame(in: .global).maxY < UIScreen.main.bounds.height
} action: { newValue in
visibleBottomSpace = newValue
}
}
.listSectionSeparator(.hidden)
.listRowBackground(Color(uiColor: .secondarySystemBackground))
}
.listStyle(.plain)
.background {
Color(uiColor: visibleBottomSpace ? .secondarySystemBackground : .systemBackground)
}
.ignoresSafeArea(edges: .bottom)
}
private var headerView: some View {
VStack {
albumArt
.resizable()
.scaledToFill()
.clipShape(RoundedRectangle(cornerRadius: 10))
.shadow(radius: 3, y: 3)
.padding(.horizontal, 40)
topDetailsStack
actionButtonsStack
}
.alignmentGuide(.listRowSeparatorLeading) { _ in
return 0
}
}
private var topDetailsStack: some View {
VStack(spacing: 3) {
Text(title)
.font(.title2.bold())
Text(artist)
.font(.title2)
.foregroundStyle(.pink)
Text("\(genre) • \(releaseYear) • \(quality)")
.font(.caption.bold())
.foregroundStyle(.gray)
}
}
private var actionButtonsStack: some View {
HStack(spacing: 15) {
actionButton(title: "Play", symbolName: "play.fill") {
print("Play album")
playingTrack = tracks.first
}
actionButton(title: "Shuffle", symbolName: "shuffle") {
print("Shuffle album")
playingTrack = tracks.randomElement()
}
}
.padding(.vertical)
}
private func actionButton(
title: LocalizedStringKey,
symbolName: String,
completion: @escaping () -> Void
) -> some View {
Button(action: completion) {
HStack(spacing: 4) {
Image(systemName: symbolName)
Text(title)
}
.font(.headline)
.foregroundStyle(.pink)
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.background(Color(uiColor: .secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
.buttonStyle(.plain)
}
private var trackList: some View {
ForEach(Array(tracks.enumerated()), id: \.element) { (index, trackName) in
Button {
// Play the song
print("Play: \(trackName)")
playingTrack = trackName
} label: {
HStack {
Text(trackName)
Spacer()
Image(systemName: "ellipsis")
}
.padding(.leading, 25)
.overlay(alignment: .leading) {
if playingTrack == trackName {
HStack(spacing: 2) {
ForEach(0...3, id: \.self) { _ in
Capsule()
.frame(width: 2, height: CGFloat.random(in: 8...20))
.foregroundStyle(.gray)
}
}
} else {
Text("\(index + 1)")
.foregroundStyle(.gray)
}
}
.alignmentGuide(.listRowSeparatorLeading) { _ in
return (index == tracks.count - 1) ? 0 : 25
}
}
.contextMenu {
Button {
// Show credits
print("Show credits: \(trackName)")
} label: {
Text("Show credits")
}
} preview: {
TrackContextMenuPreview(
albumArt: albumArt,
title: trackName,
artist: artist,
album: title,
releaseYear: releaseYear
)
}
}
}
private var bottomDetailsStack: some View {
VStack(alignment: .leading) {
Text("April 20, 2021")
Text("\(tracks.count) tracks, 32 minutes")
Text("© 2021 P NATION, under license to Dreamus")
}
.font(.subheadline)
.foregroundStyle(.gray)
}
private var moreContentSection: some View {
VStack(spacing: 20) {
additionalContentSection(title: "More titles from \(artist)")
additionalContentSection(title: "You might also like")
}
}
private func additionalContentSection(title: String) -> some View {
VStack(alignment: .leading) {
HStack {
Text(title)
.font(.title2.bold())
Image(systemName: "chevron.right")
.font(.body.bold())
}
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 10) {
ForEach(0...10, id: \.self) { _ in
contentCard(image: albumArt, title: "Example", year: "2024")
}
}
}
.scrollClipDisabled()
}
}
private func contentCard(image: Image, title: String, year: String) -> some View {
VStack(alignment: .leading) {
image
.resizable()
.scaledToFill()
.frame(width: 150, height: 150)
.clipShape(RoundedRectangle(cornerRadius: 5))
Text(title)
.font(.callout)
.lineLimit(1)
Text(year)
.font(.callout)
.foregroundStyle(.gray)
}
}
}
struct TrackContextMenuPreview: View {
let albumArt: Image
let title: String
let artist: String
let album: String
let releaseYear: String
var body: some View {
HStack(spacing: 20) {
albumArt
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipShape(RoundedRectangle(cornerRadius: 10))
VStack(alignment: .leading) {
Text(title)
.font(.headline)
.lineLimit(1)
Text(artist)
.font(.caption)
.foregroundStyle(.gray)
.lineLimit(1)
Text("\(album) • \(releaseYear)")
.font(.caption)
.foregroundStyle(.gray)
.lineLimit(1)
}
.frame(minWidth: 150, alignment: .leading)
}
.padding(15)
}
}
#Preview {
AMAlbumDetailsView(
albumArt: Image("albumImage"),
title: "Dry Flower",
artist: "PENOMECO",
genre: "Hip-hop/Rap",
releaseYear: "2021",
quality: "Lossless audio",
tracks: [
"Rain Drop",
"You Up",
"JAJA",
"Actually, Pt. 2",
"Interlude",
"Remember Me (Hoody)",
"Better (feat. Kid Milli)",
"Change (feat. sogumm)",
"Hotel Lobby (feat. Verbal Jint)",
"Insomnia"
]
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment