Files
sodalive-ios/SodaLive/Sources/V2/Main/Home/Following/Components/MainHomeFollowingScheduleSection.swift

275 lines
9.0 KiB
Swift

import Foundation
import SwiftUI
struct MainHomeFollowingScheduleSection: View {
let monthlySchedules: [FollowingScheduleResponse]
let onTapSchedule: (CreatorActivityType, Int) -> Void
init(
monthlySchedules: [FollowingScheduleResponse],
onTapSchedule: @escaping (CreatorActivityType, Int) -> Void = { _, _ in }
) {
self.monthlySchedules = monthlySchedules
self.onTapSchedule = onTapSchedule
}
var body: some View {
if !monthlySchedules.isEmpty {
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
SectionTitle(title: I18n.HomeFollowing.monthlyScheduleTitle)
VStack(alignment: .leading, spacing: SodaSpacing.s4) {
ForEach(monthlySchedules) { schedule in
MainHomeFollowingScheduleItemView(schedule: schedule) {
onTapSchedule(schedule.type, schedule.targetId)
}
}
}
.background(alignment: .leading) {
ScheduleTimelineLine()
.padding(.leading, 3)
.padding(.vertical, 52)
.opacity(monthlySchedules.count > 1 ? 1 : 0)
}
.padding(.horizontal, SodaSpacing.s14)
}
}
}
}
private struct MainHomeFollowingScheduleItemView: View {
let schedule: FollowingScheduleResponse
let action: () -> Void
var body: some View {
HStack(alignment: .center, spacing: SodaSpacing.s14) {
TimelineDotView(isHighlighted: schedule.isToday || schedule.isOnAir)
Button(action: action) {
HStack(alignment: .center, spacing: SodaSpacing.s12) {
DateBoxView(schedule: schedule)
VStack(alignment: .leading, spacing: SodaSpacing.s8) {
creatorInfo
HStack(alignment: .center, spacing: SodaSpacing.s8) {
Text(schedule.title)
.appFont(size: 16, weight: .medium)
.foregroundColor(.white)
.lineLimit(1)
.truncationMode(.tail)
Spacer(minLength: 0)
}
HStack(alignment: .center, spacing: SodaSpacing.s8) {
ActivityTypeTagView(type: schedule.type)
Spacer(minLength: 0)
Text(schedule.statusText)
.appFont(size: 16, weight: .regular)
.foregroundColor(schedule.isOnAir ? Color.soda400 : Color.gray500)
.lineLimit(1)
.multilineTextAlignment(.trailing)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(SodaSpacing.s14)
.padding(.leading, schedule.isOnAir ? SodaSpacing.s8 : 0)
.background(Color.gray900)
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
.overlay(alignment: .leading) {
if schedule.isOnAir {
Color.soda400
.frame(width: SodaSpacing.s4)
}
}
}
.buttonStyle(.plain)
}
}
private var creatorInfo: some View {
HStack(alignment: .center, spacing: SodaSpacing.s6) {
DownsampledKFImage(url: URL(string: schedule.creatorProfileImageUrl), size: CGSize(width: 20, height: 20))
.clipShape(Circle())
.frame(width: 20, height: 20)
Text(schedule.creatorNickname)
.appFont(size: 12, weight: .regular)
.foregroundColor(.white)
.lineLimit(1)
.truncationMode(.tail)
}
}
}
private struct ScheduleTimelineLine: View {
var body: some View {
Rectangle()
.fill(Color.gray700)
.frame(width: 2)
.frame(maxHeight: .infinity)
}
}
private struct TimelineDotView: View {
let isHighlighted: Bool
var body: some View {
Circle()
.fill(isHighlighted ? Color.soda400 : Color.gray500)
.frame(width: SodaSpacing.s8, height: SodaSpacing.s8)
.frame(width: SodaSpacing.s8)
}
}
private struct DateBoxView: View {
let schedule: FollowingScheduleResponse
var body: some View {
VStack(alignment: .center, spacing: SodaSpacing.s4) {
if schedule.isToday {
Text("오늘")
.appFont(size: 22, weight: .bold)
.foregroundColor(.white)
.lineLimit(1)
} else {
Text(schedule.dayText)
.appFont(size: 22, weight: .bold)
.foregroundColor(.white)
.lineLimit(1)
Text(schedule.weekdayText)
.appFont(size: 12, weight: .medium)
.foregroundColor(Color.gray500)
.lineLimit(1)
}
}
.frame(width: 53, height: 53)
}
}
private struct ActivityTypeTagView: View {
let type: CreatorActivityType
var body: some View {
Text(type.scheduleTagTitle)
.appFont(size: 14, weight: .regular)
.foregroundColor(Color.gray100)
.padding(.horizontal, SodaSpacing.s4)
.padding(.vertical, 2)
.background(Color.gray700)
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s4, style: .continuous))
.lineLimit(1)
}
}
private extension FollowingScheduleResponse {
var scheduledDate: Date? {
DateParser.parse(scheduledAtUtc)
}
var isToday: Bool {
guard let scheduledDate else { return false }
return Calendar.current.isDateInToday(scheduledDate)
}
var dayText: String {
guard let scheduledDate else { return scheduledAtUtc }
return ScheduleDateFormatter.day.string(from: scheduledDate)
}
var weekdayText: String {
guard let scheduledDate else { return "" }
return ScheduleDateFormatter.weekday.string(from: scheduledDate)
}
var statusText: String {
if isOnAir {
return "On Air"
}
guard let scheduledDate else { return scheduledAtUtc }
return ScheduleDateFormatter.time.string(from: scheduledDate)
}
}
private extension CreatorActivityType {
var scheduleTagTitle: String {
switch self {
case .live:
return "라이브"
case .liveReplay:
return "다시보기"
case .audio:
return "오디오"
case .community:
return "커뮤니티"
case .unknown(let rawValue):
return rawValue
}
}
}
private enum ScheduleDateFormatter {
static let day: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "ko_KR")
formatter.dateFormat = "d"
return formatter
}()
static let weekday: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "ko_KR")
formatter.dateFormat = "E"
return formatter
}()
static let time: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "ko_KR")
formatter.dateFormat = "a hh:mm"
return formatter
}()
}
struct MainHomeFollowingScheduleSection_Previews: PreviewProvider {
private static let previewImageUrl = "https://picsum.photos/500/500"
static var previews: some View {
MainHomeFollowingScheduleSection(
monthlySchedules: [
FollowingScheduleResponse(
scheduleId: "today-on-air",
creatorId: 1,
creatorProfileImageUrl: previewImageUrl,
creatorNickname: "크리에이터",
title: "오늘 밤 같이 이야기해요",
type: .live,
targetId: 100,
scheduledAtUtc: ISO8601DateFormatter().string(from: Date()),
isOnAir: false
),
FollowingScheduleResponse(
scheduleId: "normal-schedule",
creatorId: 2,
creatorProfileImageUrl: previewImageUrl,
creatorNickname: "소다",
title: "신규 오디오 콘텐츠 공개",
type: .audio,
targetId: 200,
scheduledAtUtc: "2026-07-12T12:30:00Z",
isOnAir: false
)
]
)
.padding(.vertical, SodaSpacing.s20)
.background(Color.black)
.previewLayout(.sizeThatFits)
}
}