refactor(navigation): 전역 경로 기반 단일 내비게이션 흐름으로 전환한다

This commit is contained in:
Yu Sung
2026-03-06 16:34:44 +09:00
parent f145de87aa
commit 42ce09d927
34 changed files with 1181 additions and 873 deletions

View File

@@ -7,13 +7,23 @@
import Foundation
struct AppRoute: Hashable {
let id = UUID()
}
class AppState: ObservableObject {
static let shared = AppState()
private var appStepBackStack = [AppStep]()
private var routeStepMap: [AppRoute: AppStep] = [:]
@Published var alreadyUpdatedMarketingInfo = false
@Published private(set) var appStep: AppStep = .splash
@Published private(set) var rootStep: AppStep = .splash
@Published var navigationPath: [AppRoute] = [] {
didSet {
syncStepWithNavigationPath()
}
}
@Published var isShowPlayer = false {
didSet {
@@ -53,28 +63,52 @@ class AppState: ObservableObject {
@Published var isShowErrorPopup = false
@Published var errorMessage = ""
func setAppStep(step: AppStep) {
switch step {
case .splash, .main:
appStepBackStack.removeAll()
default:
appStepBackStack.append(appStep)
}
private func syncStepWithNavigationPath() {
let validRoutes = Set(navigationPath)
routeStepMap = routeStepMap.filter { validRoutes.contains($0.key) }
if let route = navigationPath.last,
let step = routeStepMap[route] {
appStep = step
} else {
appStep = rootStep
}
}
func appStep(for route: AppRoute) -> AppStep? {
routeStepMap[route]
}
func setAppStep(step: AppStep) {
DispatchQueue.main.async {
self.appStep = step
switch step {
case .splash, .main:
self.rootStep = step
self.routeStepMap.removeAll()
self.navigationPath.removeAll()
self.appStep = step
default:
let route = AppRoute()
self.routeStepMap[route] = step
self.navigationPath.append(route)
self.appStep = step
}
}
}
func back() {
if let step = appStepBackStack.popLast() {
self.appStep = step
} else {
self.appStep = .main
DispatchQueue.main.async {
if self.navigationPath.isEmpty {
self.rootStep = .main
self.appStep = .main
return
}
_ = self.navigationPath.popLast()
}
}
// ( -> ) UI
func softRestart() {
isRestartApp = true

View File

@@ -14,105 +14,100 @@ struct NewCharacterListView: View {
private let gridSpacing: CGFloat = 12
var body: some View {
NavigationStack {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 8) {
// Toolbar
DetailNavigationBar(title: String(localized: "신규 캐릭터 전체보기"))
Group { BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 8) {
// Toolbar
DetailNavigationBar(title: String(localized: "신규 캐릭터 전체보기"))
VStack(alignment: .leading, spacing: 12) {
// n
HStack(spacing: 0) {
Text("전체")
.appFont(size: 12, weight: .regular)
.foregroundColor(Color(hex: "e2e2e2"))
Text(" \(viewModel.totalCount)")
.appFont(size: 12, weight: .regular)
.foregroundColor(Color(hex: "ff5c49"))
Text("")
.appFont(size: 12, weight: .regular)
.foregroundColor(Color(hex: "e2e2e2"))
Spacer()
}
.padding(.horizontal, 24)
VStack(alignment: .leading, spacing: 12) {
// n
HStack(spacing: 0) {
Text("전체")
.appFont(size: 12, weight: .regular)
.foregroundColor(Color(hex: "e2e2e2"))
Text(" \(viewModel.totalCount)")
.appFont(size: 12, weight: .regular)
.foregroundColor(Color(hex: "ff5c49"))
Text("")
.appFont(size: 12, weight: .regular)
.foregroundColor(Color(hex: "e2e2e2"))
Spacer()
}
.padding(.horizontal, 24)
// Grid 3
GeometryReader { geo in
let width = (geo.size.width - (horizontalPadding * 2) - gridSpacing) / 2
// Grid 3
GeometryReader { geo in
let width = (geo.size.width - (horizontalPadding * 2) - gridSpacing) / 2
ScrollView(.vertical, showsIndicators: false) {
LazyVGrid(
columns: Array(
repeating: GridItem(
.flexible(),
spacing: gridSpacing,
alignment: .topLeading
),
count: 2
ScrollView(.vertical, showsIndicators: false) {
LazyVGrid(
columns: Array(
repeating: GridItem(
.flexible(),
spacing: gridSpacing,
alignment: .topLeading
),
alignment: .leading,
spacing: gridSpacing
) {
ForEach(viewModel.items.indices, id: \.self) { idx in
let item = viewModel.items[idx]
NavigationLink(value: item.characterId) {
CharacterItemView(
character: item,
size: width,
rank: 0,
isShowRank: false
)
.onAppear { viewModel.loadMoreIfNeeded(currentIndex: idx) }
}
}
count: 2
),
alignment: .leading,
spacing: gridSpacing
) {
ForEach(viewModel.items.indices, id: \.self) { idx in
let item = viewModel.items[idx]
CharacterItemView(
character: item,
size: width,
rank: 0,
isShowRank: false
)
.onAppear { viewModel.loadMoreIfNeeded(currentIndex: idx) }
.onTapGesture { AppState.shared.setAppStep(step: .characterDetail(characterId: item.characterId)) }
}
.padding(.horizontal, horizontalPadding)
if viewModel.isLoadingMore {
HStack {
Spacer()
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.padding(.vertical, 16)
Spacer()
}
}
.padding(.horizontal, horizontalPadding)
if viewModel.isLoadingMore {
HStack {
Spacer()
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.padding(.vertical, 16)
Spacer()
}
}
}
.frame(minHeight: 0, maxHeight: .infinity)
}
.padding(.vertical, 12)
.onAppear {
// 1
if viewModel.items.isEmpty {
viewModel.fetch()
}
}
.frame(minHeight: 0, maxHeight: .infinity)
}
.background(Color.black)
}
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
GeometryReader { geo in
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: geo.size.width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
.padding(.vertical, 12)
.onAppear {
// 1
if viewModel.items.isEmpty {
viewModel.fetch()
}
}
}
.navigationDestination(for: Int.self) { characterId in
CharacterDetailView(characterId: characterId)
.background(Color.black)
}
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
GeometryReader { geo in
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: geo.size.width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
}
}
}

View File

@@ -15,91 +15,87 @@ struct OriginalWorkDetailView: View {
let originalId: Int
var body: some View {
NavigationStack {
BaseView(isLoading: $viewModel.isLoading) {
ZStack(alignment: .top) {
if let imageUrl = viewModel.response?.imageUrl {
KFImage(URL(string: imageUrl))
.cancelOnDisappear(true)
Group { BaseView(isLoading: $viewModel.isLoading) {
ZStack(alignment: .top) {
if let imageUrl = viewModel.response?.imageUrl {
KFImage(URL(string: imageUrl))
.cancelOnDisappear(true)
.resizable()
.scaledToFill()
.frame(width: screenSize().width, height: (168 * 288 / 306) + 56)
.clipped()
.blur(radius: 25)
}
Color.black.opacity(0.5).ignoresSafeArea()
VStack(spacing: 0) {
HStack(spacing: 0) {
Image("ic_back")
.resizable()
.scaledToFill()
.frame(width: screenSize().width, height: (168 * 288 / 306) + 56)
.clipped()
.blur(radius: 25)
}
Color.black.opacity(0.5).ignoresSafeArea()
VStack(spacing: 0) {
HStack(spacing: 0) {
Image("ic_back")
.resizable()
.frame(width: 24, height: 24)
.onTapGesture {
AppState.shared.back()
}
Spacer()
}
.padding(.horizontal, 24)
.frame(height: 56)
.frame(width: 24, height: 24)
.onTapGesture {
AppState.shared.back()
}
if let response = viewModel.response {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 0) {
OriginalWorkDetailHeaderView(item: response)
.padding(.horizontal, 24)
.padding(.bottom, 24)
HStack(spacing: 0) {
SeriesDetailTabView(
title: I18n.Tab.character,
width: screenSize().width / 2,
isSelected: viewModel.currentTab == .character
) {
if viewModel.currentTab != .character {
viewModel.currentTab = .character
}
}
SeriesDetailTabView(
title: I18n.Tab.workInfo,
width: screenSize().width / 2,
isSelected: viewModel.currentTab == .info
) {
if viewModel.currentTab != .info {
viewModel.currentTab = .info
}
Spacer()
}
.padding(.horizontal, 24)
.frame(height: 56)
if let response = viewModel.response {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 0) {
OriginalWorkDetailHeaderView(item: response)
.padding(.horizontal, 24)
.padding(.bottom, 24)
HStack(spacing: 0) {
SeriesDetailTabView(
title: I18n.Tab.character,
width: screenSize().width / 2,
isSelected: viewModel.currentTab == .character
) {
if viewModel.currentTab != .character {
viewModel.currentTab = .character
}
}
.background(Color.black)
Rectangle()
.foregroundColor(Color.gray90.opacity(0.5))
.frame(height: 1)
.frame(maxWidth: .infinity)
switch(viewModel.currentTab) {
case .info:
OriginalWorkInfoView(response: response)
default:
OriginalWorkCharacterView(characters: viewModel.characters)
SeriesDetailTabView(
title: I18n.Tab.workInfo,
width: screenSize().width / 2,
isSelected: viewModel.currentTab == .info
) {
if viewModel.currentTab != .info {
viewModel.currentTab = .info
}
}
}
.background(Color.black)
Rectangle()
.foregroundColor(Color.gray90.opacity(0.5))
.frame(height: 1)
.frame(maxWidth: .infinity)
switch(viewModel.currentTab) {
case .info:
OriginalWorkInfoView(response: response)
default:
OriginalWorkCharacterView(characters: viewModel.characters)
}
}
}
}
}
}
.onAppear {
if viewModel.response == nil {
viewModel.originalId = originalId
}
}
.navigationDestination(for: Int.self) { characterId in
CharacterDetailView(characterId: characterId)
}
.onAppear {
if viewModel.response == nil {
viewModel.originalId = originalId
}
}
}
}
}
@@ -129,14 +125,13 @@ struct OriginalWorkCharacterView: View {
ForEach(characters.indices, id: \.self) { idx in
let item = characters[idx]
NavigationLink(value: item.characterId) {
CharacterItemView(
character: item,
size: width,
rank: 0,
isShowRank: false
)
}
CharacterItemView(
character: item,
size: width,
rank: 0,
isShowRank: false
)
.onTapGesture { AppState.shared.setAppStep(step: .characterDetail(characterId: item.characterId)) }
}
}
.padding(.horizontal, horizontalPadding)

View File

@@ -9,11 +9,12 @@ import SwiftUI
struct ContentAllByThemeView: View {
@StateObject var viewModel = ContentAllByThemeViewModel()
@State private var isInitialized = false
let themeId: Int
var body: some View {
NavigationView {
Group {
BaseView(isLoading: $viewModel.isLoading) {
VStack(alignment: .leading, spacing: 0) {
DetailNavigationBar(title: viewModel.theme)
@@ -111,8 +112,11 @@ struct ContentAllByThemeView: View {
}
}
.onAppear {
viewModel.themeId = themeId
viewModel.getContentList()
if !isInitialized || viewModel.themeId != themeId {
viewModel.themeId = themeId
viewModel.getContentList()
isInitialized = true
}
}
}
}

View File

@@ -10,12 +10,13 @@ import SwiftUI
struct ContentAllView: View {
@StateObject var viewModel = ContentAllViewModel()
@State private var isInitialized = false
var isFree: Bool = false
var isPointAvailableOnly: Bool = false
var body: some View {
NavigationView {
Group {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
DetailNavigationBar(title: isFree ? String(localized: "무료 콘텐츠 전체") : isPointAvailableOnly ? String(localized: "포인트 대여 전체") : String(localized: "콘텐츠 전체"))
@@ -78,63 +79,63 @@ struct ContentAllView: View {
ForEach(viewModel.contentList.indices, id: \.self) { idx in
let item = viewModel.contentList[idx]
NavigationLink {
ContentDetailView(contentId: item.contentId)
} label: {
VStack(alignment: .leading, spacing: 0) {
ZStack(alignment: .top) {
DownsampledKFImage(
url: URL(string: item.coverImageUrl),
size: CGSize(width: itemSize, height: itemSize)
)
.cornerRadius(16)
VStack(alignment: .leading, spacing: 0) {
ZStack(alignment: .top) {
DownsampledKFImage(
url: URL(string: item.coverImageUrl),
size: CGSize(width: itemSize, height: itemSize)
)
.cornerRadius(16)
HStack(alignment: .top, spacing: 0) {
Spacer()
HStack(alignment: .top, spacing: 0) {
Spacer()
if item.isPointAvailable {
Image("ic_point")
.padding(.top, 6)
.padding(.trailing, 6)
}
if item.isPointAvailable {
Image("ic_point")
.padding(.top, 6)
.padding(.trailing, 6)
}
}
Text(item.title)
.appFont(size: 18, weight: .regular)
.foregroundColor(.white)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(1)
.padding(.horizontal, 6)
.padding(.top, 8)
Text(item.creatorNickname)
.appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "78909C"))
.lineLimit(1)
.padding(.horizontal, 6)
.padding(.top, 4)
}
.frame(width: itemSize)
.contentShape(Rectangle())
.onAppear {
if idx == viewModel.contentList.count - 1 {
viewModel.fetchData()
}
Text(item.title)
.appFont(size: 18, weight: .regular)
.foregroundColor(.white)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(1)
.padding(.horizontal, 6)
.padding(.top, 8)
Text(item.creatorNickname)
.appFont(size: 14, weight: .regular)
.foregroundColor(Color(hex: "78909C"))
.lineLimit(1)
.padding(.horizontal, 6)
.padding(.top, 4)
}
.frame(width: itemSize)
.contentShape(Rectangle())
.onAppear {
if idx == viewModel.contentList.count - 1 {
viewModel.fetchData()
}
}
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: item.contentId)) }
}
}
.padding(horizontalPadding)
}
}
.onAppear {
viewModel.isFree = isFree
viewModel.isPointAvailableOnly = isPointAvailableOnly
viewModel.getThemeList()
viewModel.fetchData()
if !isInitialized || viewModel.isFree != isFree || viewModel.isPointAvailableOnly != isPointAvailableOnly {
viewModel.isFree = isFree
viewModel.isPointAvailableOnly = isPointAvailableOnly
viewModel.getThemeList()
viewModel.fetchData()
isInitialized = true
}
}
}
}

View File

@@ -14,95 +14,92 @@ struct ContentNewAllItemView: View {
let item: GetAudioContentMainItem
var body: some View {
NavigationLink {
ContentDetailView(contentId: item.contentId)
} label: {
VStack(alignment: .leading, spacing: 8) {
ZStack(alignment: .bottom) {
KFImage(URL(string: item.coverImageUrl))
.cancelOnDisappear(true)
.downsampling(
size: CGSize(
width: width,
height: width
)
VStack(alignment: .leading, spacing: 8) {
ZStack(alignment: .bottom) {
KFImage(URL(string: item.coverImageUrl))
.cancelOnDisappear(true)
.downsampling(
size: CGSize(
width: width,
height: width
)
.resizable()
.scaledToFill()
.frame(width: width, height: width, alignment: .top)
.cornerRadius(2.7)
)
.resizable()
.scaledToFill()
.frame(width: width, height: width, alignment: .top)
.cornerRadius(2.7)
VStack(spacing: 0) {
Spacer()
VStack(spacing: 0) {
Spacer()
HStack(spacing: 0) {
HStack(spacing: 2) {
if item.price > 0 {
Image("ic_card_can_gray")
Text("\(item.price)")
.appFont(size: 8.5, weight: .medium)
.foregroundColor(Color.white)
} else {
Text("무료")
.appFont(size: 8.5, weight: .medium)
.foregroundColor(Color.white)
}
}
.padding(3)
.background(Color(hex: "333333").opacity(0.7))
.cornerRadius(10)
.padding(.leading, 2.7)
.padding(.bottom, 2.7)
Spacer()
HStack(spacing: 2) {
Text(item.duration)
HStack(spacing: 0) {
HStack(spacing: 2) {
if item.price > 0 {
Image("ic_card_can_gray")
Text("\(item.price)")
.appFont(size: 8.5, weight: .medium)
.foregroundColor(Color.white)
} else {
Text("무료")
.appFont(size: 8.5, weight: .medium)
.foregroundColor(Color.white)
}
.padding(3)
.background(Color(hex: "333333").opacity(0.7))
.cornerRadius(10)
.padding(.trailing, 2.7)
.padding(.bottom, 2.7)
}
.padding(3)
.background(Color(hex: "333333").opacity(0.7))
.cornerRadius(10)
.padding(.leading, 2.7)
.padding(.bottom, 2.7)
Spacer()
HStack(spacing: 2) {
Text(item.duration)
.appFont(size: 8.5, weight: .medium)
.foregroundColor(Color.white)
}
.padding(3)
.background(Color(hex: "333333").opacity(0.7))
.cornerRadius(10)
.padding(.trailing, 2.7)
.padding(.bottom, 2.7)
}
}
.frame(width: width, height: width)
Text(item.title)
.appFont(size: 13.3, weight: .medium)
.foregroundColor(Color(hex: "d2d2d2"))
.frame(width: width, alignment: .leading)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(2)
HStack(spacing: 5.3) {
KFImage(URL(string: item.creatorProfileImageUrl))
.cancelOnDisappear(true)
.downsampling(
size: CGSize(
width: 21.3,
height: 21.3
)
)
.resizable()
.scaledToFill()
.frame(width: 21.3, height: 21.3)
.clipShape(Circle())
.onTapGesture { AppState.shared.setAppStep(step: .creatorDetail(userId: item.creatorId)) }
Text(item.creatorNickname)
.appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "777777"))
.lineLimit(1)
}
.padding(.bottom, 10)
}
.frame(width: width)
.frame(width: width, height: width)
Text(item.title)
.appFont(size: 13.3, weight: .medium)
.foregroundColor(Color(hex: "d2d2d2"))
.frame(width: width, alignment: .leading)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
.lineLimit(2)
HStack(spacing: 5.3) {
KFImage(URL(string: item.creatorProfileImageUrl))
.cancelOnDisappear(true)
.downsampling(
size: CGSize(
width: 21.3,
height: 21.3
)
)
.resizable()
.scaledToFill()
.frame(width: 21.3, height: 21.3)
.clipShape(Circle())
.onTapGesture { AppState.shared.setAppStep(step: .creatorDetail(userId: item.creatorId)) }
Text(item.creatorNickname)
.appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "777777"))
.lineLimit(1)
}
.padding(.bottom, 10)
}
.frame(width: width)
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: item.contentId)) }
}
}

View File

@@ -10,11 +10,12 @@ import SwiftUI
struct ContentNewAllView: View {
@StateObject var viewModel = ContentNewAllViewModel()
@State private var isInitialized = false
let isFree: Bool
var body: some View {
NavigationView {
Group {
BaseView(isLoading: $viewModel.isLoading) {
VStack(alignment: .leading, spacing: 13.3) {
DetailNavigationBar(title: isFree ? "최신 무료 콘텐츠" : "최신 콘텐츠")
@@ -82,9 +83,12 @@ struct ContentNewAllView: View {
}
}
.onAppear {
viewModel.isFree = isFree
viewModel.getThemeList()
viewModel.getNewContentList()
if !isInitialized || viewModel.isFree != isFree {
viewModel.isFree = isFree
viewModel.getThemeList()
viewModel.getNewContentList()
isInitialized = true
}
}
}
.navigationBarHidden(true)

View File

@@ -11,9 +11,10 @@ import Kingfisher
struct ContentRankingAllView: View {
@StateObject var viewModel = ContentRankingAllViewModel()
@State private var isInitialized = false
var body: some View {
NavigationView {
Group {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
DetailNavigationBar(title: "인기 콘텐츠")
@@ -44,97 +45,94 @@ struct ContentRankingAllView: View {
LazyVStack(spacing: 20) {
ForEach(0..<viewModel.contentRankingItemList.count, id: \.self) { index in
let item = viewModel.contentRankingItemList[index]
NavigationLink {
ContentDetailView(contentId: item.contentId)
} label: {
HStack(spacing: 0) {
KFImage(URL(string: item.coverImageUrl))
.cancelOnDisappear(true)
.downsampling(
size: CGSize(
width: 66.7,
height: 66.7
)
HStack(spacing: 0) {
KFImage(URL(string: item.coverImageUrl))
.cancelOnDisappear(true)
.downsampling(
size: CGSize(
width: 66.7,
height: 66.7
)
.resizable()
.scaledToFill()
.frame(width: 66.7, height: 66.7, alignment: .top)
.clipped()
.cornerRadius(5.3)
Text("\(index + 1)")
.appFont(size: 16.7, weight: .bold)
.foregroundColor(Color(hex: "3bb9f1"))
.padding(.horizontal, 12)
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 8) {
Text(item.themeStr)
.appFont(size: 8, weight: .medium)
.foregroundColor(Color(hex: "3bac6a"))
.padding(2.6)
.background(Color(hex: "28312b"))
.cornerRadius(2.6)
Text(item.duration)
.appFont(size: 8, weight: .medium)
.foregroundColor(Color(hex: "777777"))
.padding(2.6)
.background(Color(hex: "222222"))
.cornerRadius(2.6)
if item.isPointAvailable {
Text("포인트")
.appFont(size: 8, weight: .medium)
.foregroundColor(.white)
.padding(2.6)
.background(Color(hex: "7849bc"))
.cornerRadius(2.6)
}
}
Text(item.creatorNickname)
.appFont(size: 10.7, weight: .medium)
.foregroundColor(Color(hex: "777777"))
.padding(.vertical, 8)
Text(item.title)
.appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "d2d2d2"))
.lineLimit(2)
.padding(.top, 2.7)
}
Spacer()
if item.price > 0 {
HStack(spacing: 8) {
Image("ic_can")
.resizable()
.frame(width: 17, height: 17)
Text("\(item.price)")
.appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "909090"))
}
} else {
Text("무료")
.appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "ffffff"))
.padding(.horizontal, 5.3)
.padding(.vertical, 2.7)
.background(Color(hex: "cf5c37"))
)
.resizable()
.scaledToFill()
.frame(width: 66.7, height: 66.7, alignment: .top)
.clipped()
.cornerRadius(5.3)
Text("\(index + 1)")
.appFont(size: 16.7, weight: .bold)
.foregroundColor(Color(hex: "3bb9f1"))
.padding(.horizontal, 12)
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 8) {
Text(item.themeStr)
.appFont(size: 8, weight: .medium)
.foregroundColor(Color(hex: "3bac6a"))
.padding(2.6)
.background(Color(hex: "28312b"))
.cornerRadius(2.6)
Text(item.duration)
.appFont(size: 8, weight: .medium)
.foregroundColor(Color(hex: "777777"))
.padding(2.6)
.background(Color(hex: "222222"))
.cornerRadius(2.6)
if item.isPointAvailable {
Text("포인트")
.appFont(size: 8, weight: .medium)
.foregroundColor(.white)
.padding(2.6)
.background(Color(hex: "7849bc"))
.cornerRadius(2.6)
}
}
Text(item.creatorNickname)
.appFont(size: 10.7, weight: .medium)
.foregroundColor(Color(hex: "777777"))
.padding(.vertical, 8)
Text(item.title)
.appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "d2d2d2"))
.lineLimit(2)
.padding(.top, 2.7)
}
.frame(height: 66.7)
.contentShape(Rectangle())
.onAppear {
if index == viewModel.contentRankingItemList.count - 1 {
viewModel.getContentRanking()
Spacer()
if item.price > 0 {
HStack(spacing: 8) {
Image("ic_can")
.resizable()
.frame(width: 17, height: 17)
Text("\(item.price)")
.appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "909090"))
}
} else {
Text("무료")
.appFont(size: 12, weight: .medium)
.foregroundColor(Color(hex: "ffffff"))
.padding(.horizontal, 5.3)
.padding(.vertical, 2.7)
.background(Color(hex: "cf5c37"))
.cornerRadius(2.6)
}
}
.frame(height: 66.7)
.contentShape(Rectangle())
.onAppear {
if index == viewModel.contentRankingItemList.count - 1 {
viewModel.getContentRanking()
}
}
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: item.contentId)) }
}
}
}
@@ -165,8 +163,11 @@ struct ContentRankingAllView: View {
}
}
.onAppear {
viewModel.getContentRankingSortType()
viewModel.getContentRanking()
if !isInitialized {
viewModel.getContentRankingSortType()
viewModel.getContentRanking()
isInitialized = true
}
}
}
}

View File

@@ -21,7 +21,7 @@ struct ContentBoxView: View {
var body: some View {
ZStack {
NavigationView {
Group {
VStack(spacing: 13.3) {
DetailNavigationBar(title: I18n.ContentBox.title)

View File

@@ -11,9 +11,10 @@ struct ContentListView: View {
let userId: Int
@StateObject var viewModel = ContentListViewModel()
@State private var isInitialized = false
var body: some View {
NavigationView {
Group {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
HStack(spacing: 0) {
@@ -128,17 +129,14 @@ struct ContentListView: View {
ForEach(0..<viewModel.audioContentList.count, id: \.self) { index in
let audioContent = viewModel.audioContentList[index]
NavigationLink {
ContentDetailView(contentId: audioContent.contentId)
} label: {
ContentListItemView(item: audioContent)
.contentShape(Rectangle())
.onAppear {
if index == viewModel.audioContentList.count - 1 {
viewModel.getAudioContentList()
}
ContentListItemView(item: audioContent)
.contentShape(Rectangle())
.onAppear {
if index == viewModel.audioContentList.count - 1 {
viewModel.getAudioContentList()
}
}
}
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: audioContent.contentId)) }
}
}
.padding(.horizontal, 13.3)
@@ -147,9 +145,12 @@ struct ContentListView: View {
.padding(.top, 13.3)
}
.onAppear {
viewModel.userId = userId
viewModel.getCategoryList()
viewModel.getAudioContentList()
if !isInitialized || viewModel.userId != userId {
viewModel.userId = userId
viewModel.getCategoryList()
viewModel.getAudioContentList()
isInitialized = true
}
}
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
HStack {

View File

@@ -10,6 +10,7 @@ import SwiftUI
struct ContentCurationView: View {
@StateObject var viewModel = ContentCurationViewModel()
@State private var isInitialized = false
let title: String
let curationId: Int
@@ -21,7 +22,7 @@ struct ContentCurationView: View {
]
var body: some View {
NavigationView {
Group {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
DetailNavigationBar(title: title)
@@ -119,8 +120,11 @@ struct ContentCurationView: View {
}
}
.onAppear {
viewModel.curationId = curationId
viewModel.getContentList()
if !isInitialized || viewModel.curationId != curationId {
viewModel.curationId = curationId
viewModel.getContentList()
isInitialized = true
}
}
}
}

View File

@@ -25,7 +25,7 @@ struct AudioContentCommentListView: View {
@State private var isShowMemberProfilePopup: Bool = false
var body: some View {
NavigationView {
Group {
ZStack {
VStack(spacing: 0) {
HStack(spacing: 0) {

View File

@@ -422,12 +422,15 @@ struct ContentDetailView: View {
.sheet(
isPresented: $isShowCommentListView,
content: {
AudioContentCommentListView(
isPresented: $isShowCommentListView,
creatorId: viewModel.audioContent!.creator.creatorId,
audioContentId: viewModel.audioContent!.contentId,
isShowSecret: viewModel.audioContent!.existOrdered
)
NavigationStack {
AudioContentCommentListView(
isPresented: $isShowCommentListView,
creatorId: viewModel.audioContent!.creator.creatorId,
audioContentId: viewModel.audioContent!.contentId,
isShowSecret: viewModel.audioContent!.existOrdered
)
}
.toolbar(.hidden, for: .navigationBar)
}
)
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {

View File

@@ -10,9 +10,10 @@ import SwiftUI
struct ContentMainAlarmAllView: View {
@StateObject var viewModel = ContentMainAlarmAllViewModel()
@State private var isInitialized = false
var body: some View {
NavigationView {
Group {
BaseView(isLoading: $viewModel.isLoading) {
VStack(alignment: .leading, spacing: 13.3) {
DetailNavigationBar(title: "새로운 알람")
@@ -81,7 +82,10 @@ struct ContentMainAlarmAllView: View {
}
}
.onAppear {
viewModel.getContentMainAlarmAll()
if !isInitialized {
viewModel.getContentMainAlarmAll()
isInitialized = true
}
}
}
.navigationBarHidden(true)

View File

@@ -10,9 +10,10 @@ import SwiftUI
struct ContentMainAsmrAllView: View {
@StateObject var viewModel = ContentNewAllViewModel()
@State private var isInitialized = false
var body: some View {
NavigationView {
Group {
BaseView(isLoading: $viewModel.isLoading) {
VStack(alignment: .leading, spacing: 13.3) {
DetailNavigationBar(title: "새로운 ASMR")
@@ -72,7 +73,14 @@ struct ContentMainAsmrAllView: View {
}
}
.onAppear {
viewModel.selectedTheme = "ASMR"
if !isInitialized {
if viewModel.selectedTheme != "ASMR" {
viewModel.selectedTheme = "ASMR"
} else if viewModel.newContentList.isEmpty {
viewModel.getNewContentList()
}
isInitialized = true
}
}
}
.navigationBarHidden(true)

View File

@@ -46,7 +46,7 @@ struct ContentMainViewV2: View {
}
var body: some View {
NavigationView {
Group {
ZStack {
Color.black.ignoresSafeArea()

View File

@@ -10,9 +10,10 @@ import SwiftUI
struct ContentMainIntroduceCreatorAllView: View {
@StateObject var viewModel = ContentMainIntroduceCreatorAllViewModel()
@State private var isInitialized = false
var body: some View {
NavigationView {
Group {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 13.3) {
DetailNavigationBar(title: "크리에이터 소개")
@@ -48,7 +49,10 @@ struct ContentMainIntroduceCreatorAllView: View {
}
}
.onAppear {
viewModel.getIntroduceCreatorList()
if !isInitialized {
viewModel.getIntroduceCreatorList()
isInitialized = true
}
}
}
.navigationBarHidden(true)

View File

@@ -10,9 +10,10 @@ import SwiftUI
struct ContentMainReplayAllView: View {
@StateObject var viewModel = ContentNewAllViewModel()
@State private var isInitialized = false
var body: some View {
NavigationView {
Group {
BaseView(isLoading: $viewModel.isLoading) {
VStack(alignment: .leading, spacing: 13.3) {
DetailNavigationBar(title: "새로운 라이브 다시듣기")
@@ -72,7 +73,14 @@ struct ContentMainReplayAllView: View {
}
}
.onAppear {
viewModel.selectedTheme = "다시듣기"
if !isInitialized {
if viewModel.selectedTheme != "다시듣기" {
viewModel.selectedTheme = "다시듣기"
} else if viewModel.newContentList.isEmpty {
viewModel.getNewContentList()
}
isInitialized = true
}
}
}
.navigationBarHidden(true)

View File

@@ -10,6 +10,7 @@ import SwiftUI
struct SeriesMainByGenreView: View {
@StateObject var viewModel = SeriesMainByGenreViewModel()
@State private var isInitialized = false
var body: some View {
ZStack {
@@ -41,17 +42,16 @@ struct SeriesMainByGenreView: View {
) {
ForEach(viewModel.seriesList.indices, id: \.self) { index in
let item = viewModel.seriesList[index]
NavigationLink {
SeriesDetailView(seriesId: item.seriesId)
} label: {
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
.contentShape(Rectangle())
.onAppear {
if index == viewModel.seriesList.count - 1 {
viewModel.getSeriesListByGenre()
}
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
.contentShape(Rectangle())
.onAppear {
if index == viewModel.seriesList.count - 1 {
viewModel.getSeriesListByGenre()
}
}
}
.onTapGesture {
AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
}
}
}
.padding(.horizontal, horizontalPadding)
@@ -73,7 +73,10 @@ struct SeriesMainByGenreView: View {
}
}
.onAppear {
viewModel.getGenreList()
if !isInitialized {
viewModel.getGenreList()
isInitialized = true
}
}
if viewModel.isLoading {

View File

@@ -75,17 +75,14 @@ struct SeriesMainDayOfWeekView: View {
) {
ForEach(viewModel.seriesList.indices, id: \.self) { index in
let item = viewModel.seriesList[index]
NavigationLink {
SeriesDetailView(seriesId: item.seriesId)
} label: {
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
.contentShape(Rectangle())
.onAppear {
if index == viewModel.seriesList.count - 1 {
viewModel.getDayOfWeekSeriesList(dayOfWeek: dayOfWeek)
}
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
.contentShape(Rectangle())
.onAppear {
if index == viewModel.seriesList.count - 1 {
viewModel.getDayOfWeekSeriesList(dayOfWeek: dayOfWeek)
}
}
}
.onTapGesture { AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId)) }
}
}
.padding(.horizontal, horizontalPadding)

View File

@@ -22,11 +22,8 @@ struct SeriesMainHomeBannerView: View {
ForEach(0..<bannerList.count, id: \.self) { index in
let item = bannerList[index]
NavigationLink {
SeriesDetailView(seriesId: item.seriesId)
} label: {
SeriesMainHomeBannerImageView(url: item.imagePath, width: width, height: height)
}
SeriesMainHomeBannerImageView(url: item.imagePath, width: width, height: height)
.onTapGesture { AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId)) }
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))

View File

@@ -10,6 +10,7 @@ import SwiftUI
struct SeriesMainHomeView: View {
@StateObject var viewModel = SeriesMainHomeViewModel()
@State private var isInitialized = false
var body: some View {
ZStack {
@@ -43,11 +44,10 @@ struct SeriesMainHomeView: View {
LazyHStack(spacing: 16) {
ForEach(0..<viewModel.completedSeriesList.count, id: \.self) {
let item = viewModel.completedSeriesList[$0]
NavigationLink {
SeriesDetailView(seriesId: item.seriesId)
} label: {
SeriesMainItemView(item: item)
}
SeriesMainItemView(item: item)
.onTapGesture {
AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
}
}
}
.padding(.horizontal, 24)
@@ -89,11 +89,10 @@ struct SeriesMainHomeView: View {
) {
ForEach(viewModel.recommendSeriesList.indices, id: \.self) {
let item = viewModel.recommendSeriesList[$0]
NavigationLink {
SeriesDetailView(seriesId: item.seriesId)
} label: {
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
}
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
.onTapGesture {
AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
}
}
}
.padding(.horizontal, horizontalPadding)
@@ -117,7 +116,10 @@ struct SeriesMainHomeView: View {
}
}
.onAppear {
viewModel.fetchHome()
if !isInitialized {
viewModel.fetchHome()
isInitialized = true
}
}
if viewModel.isLoading {

View File

@@ -25,7 +25,7 @@ struct SeriesMainView: View {
@State private var selectedTab: InnerTab = .home
var body: some View {
NavigationView {
Group {
BaseView {
VStack(spacing: 0) {
DetailNavigationBar(title: "시리즈 전체보기")

View File

@@ -9,7 +9,8 @@ import SwiftUI
struct SeriesListAllView: View {
@ObservedObject var viewModel = SeriesListAllViewModel()
@StateObject var viewModel = SeriesListAllViewModel()
@State private var isInitialized = false
var creatorId: Int? = nil
var creatorNickname: String? = nil
@@ -18,7 +19,7 @@ struct SeriesListAllView: View {
var isCompleted = false
var body: some View {
NavigationView {
Group {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
if isCompleted {
@@ -48,17 +49,16 @@ struct SeriesListAllView: View {
) {
ForEach(0..<viewModel.seriesList.count, id: \.self) { index in
let item = viewModel.seriesList[index]
NavigationLink {
SeriesDetailView(seriesId: item.seriesId)
} label: {
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
.contentShape(Rectangle())
.onAppear {
if index == viewModel.seriesList.count - 1 {
viewModel.getSeriesList()
}
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
.contentShape(Rectangle())
.onAppear {
if index == viewModel.seriesList.count - 1 {
viewModel.getSeriesList()
}
}
}
.onTapGesture {
AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
}
}
}
.padding(horizontalPadding)
@@ -67,10 +67,24 @@ struct SeriesListAllView: View {
}
}
.onAppear {
viewModel.creatorId = creatorId
viewModel.isOriginal = isOriginal
viewModel.isCompleted = isCompleted
viewModel.getSeriesList()
let hasFilterChanged =
viewModel.creatorId != creatorId ||
viewModel.isOriginal != isOriginal ||
viewModel.isCompleted != isCompleted
if !isInitialized || hasFilterChanged {
if hasFilterChanged {
viewModel.page = 1
viewModel.isLast = false
viewModel.seriesList.removeAll()
}
viewModel.creatorId = creatorId
viewModel.isOriginal = isOriginal
viewModel.isCompleted = isCompleted
viewModel.getSeriesList()
isInitialized = true
}
}
}
}

View File

@@ -15,303 +15,327 @@ struct ContentView: View {
@State private var message = ""
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
if appState.isRestartApp {
EmptyView()
} else {
HomeView()
}
switch appState.appStep {
case .splash:
SplashView()
case .login:
LoginView()
case .signUp:
SignUpView()
case .findPassword:
FindPasswordView()
case .textMessageDetail(let messageItem, let messageBox, let refresh):
TextMessageDetailView(messageItem: messageItem, messageBox: messageBox, refresh: refresh)
case .writeTextMessage(let userId, let nickname):
TextMessageWriteView(replySenderId: userId, replySenderNickname: nickname)
case .writeVoiceMessage(let userId, let nickname, let onRefresh):
VoiceMessageWriteView(replySenderId: userId, replySenderNickname: nickname, onRefresh: onRefresh)
case .settings:
SettingsView()
case .languageSettings:
LanguageSettingsView()
case .notices:
NoticeListView()
case .noticeDetail(let notice):
NoticeDetailView(notice: notice)
case .events:
EventListView()
case .eventDetail(let event):
EventDetailView(event: event)
case .terms:
TermsView(isPrivacyPolicy: false)
case .privacy:
TermsView(isPrivacyPolicy: true)
case .notificationSettings:
NotificationSettingsView()
case .contentViewSettings:
ContentSettingsView()
case .signOut:
SignOutView()
case .canStatus(let refresh):
CanStatusView(refresh: refresh)
case .canCharge(let refresh, let afterCompletionToGoBack):
CanChargeView(refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
case .canPayment(let canProduct, let refresh, let afterCompletionToGoBack):
CanPaymentView(canProduct: canProduct, refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
case .canPgPayment(let canResponse, let refresh, let afterCompletionToGoBack):
CanPgPaymentView(canResponse: canResponse, refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
.environmentObject(canPgPaymentViewModel)
case .liveReservation:
LiveReservationStatusView()
case .liveReservationCancel(let reservationId):
LiveReservationCancelView(reservationId: reservationId)
case .serviceCenter:
ServiceCenterView()
case .createContent:
ContentCreateView()
case .liveReservationComplete(let response):
LiveReservationCompleteView(reservationCompleteData: response)
case .creatorDetail(let userId):
UserProfileView(userId: userId)
case .followerList(let userId):
FollowerListView(userId: userId)
case .modifyContent(let contentId):
ContentModifyView(contentId: contentId)
case .contentListAll(let userId):
ContentListView(userId: userId)
case .contentDetail(let contentId):
ContentDetailView(contentId: contentId)
case .createLive(let timeSettingMode, let onSuccess):
LiveRoomCreateView(
timeSettingMode: timeSettingMode,
onSuccess: onSuccess
)
case .liveNowAll(let onClickParticipant):
LiveNowAllView(onClickParticipant: onClickParticipant)
case .liveReservationAll(let onClickReservation, let onClickStart, let onClickCancel, let onTapCreateLive):
LiveReservationAllView(
onClickReservation: onClickReservation,
onClickStart: onClickStart,
onClickCancel: onClickCancel,
onTapCreateLive: onTapCreateLive
)
case .modifyLive(let room):
LiveRoomEditView(room: room)
case .liveDetail(let roomId, let onClickParticipant, let onClickReservation, let onClickStart, let onClickCancel):
LiveDetailView(
roomId: roomId,
onClickParticipant: onClickParticipant,
onClickReservation: onClickReservation,
onClickStart: onClickStart,
onClickCancel: onClickCancel
)
case .modifyPassword:
ModifyPasswordView()
case .changeNickname:
NicknameUpdateView()
case .profileUpdate(let refresh):
ProfileUpdateView(refresh: refresh)
case .followingList:
FollowCreatorView()
case .orderListAll:
OrderListAllView()
case .userProfileDonationAll(let userId):
UserProfileDonationAllView(userId: userId)
NavigationStack(path: $appState.navigationPath) {
ZStack {
Color.black.ignoresSafeArea()
case .channelDonationAll(let creatorId):
ChannelDonationAllView(creatorId: creatorId)
if appState.isRestartApp {
EmptyView()
} else {
HomeView()
}
case .userProfileFanTalkAll(let userId):
UserProfileFanTalkAllView(userId: userId)
case .newContentAll(let isFree):
ContentNewAllView(isFree: isFree)
case .curationAll(let title, let curationId):
ContentCurationView(title: title, curationId: curationId)
case .contentRankingAll:
ContentRankingAllView()
case .creatorCommunityAll(let creatorId):
CreatorCommunityAllView(creatorId: creatorId)
case .creatorCommunityWrite(let onSuccess):
CreatorCommunityWriteView(onSuccess: onSuccess)
case .creatorCommunityModify(let postId, let onSuccess):
CreatorCommunityModifyView(postId: postId, onSuccess: onSuccess)
case .canCoupon(let refresh):
CanCouponView(refresh: refresh)
case .contentAllByTheme(let themeId):
ContentAllByThemeView(themeId: themeId)
case .seriesAll(let creatorId, let creatorNickname, let isOriginal, let isCompleted):
SeriesListAllView(creatorId: creatorId, creatorNickname: creatorNickname, isOriginal: isOriginal, isCompleted: isCompleted)
case .seriesDetail(let seriesId):
SeriesDetailView(seriesId: seriesId)
case .seriesContentAll(let seriesId, let seriesTitle):
SeriesContentAllView(seriesId: seriesId, seriesTitle: seriesTitle)
case .tempCanPayment(let orderType, let contentId, let title, let can):
CanPaymentTempView(orderType: orderType, contentId: contentId, title: title, can: can)
case .blockList:
BlockMemberListView()
case .myBox(let currentTab):
ContentBoxView(initCurrentTab: currentTab)
case .auditionDetail(let auditionId):
AuditionDetailView(auditionId: auditionId)
case .auditionRoleDetail(let roleId, let auditionTitle):
AuditionRoleDetailView(
roleId: roleId,
auditionTitle: auditionTitle
)
case .search:
SearchView()
case .contentMain(let startTab):
ContentMainViewV2(selectedTab: startTab)
case .completedSeriesAll:
CompletedSeriesView()
case .newAlarmContentAll:
ContentMainAlarmAllView()
case .newAsmrContentAll:
ContentMainAsmrAllView()
case .newReplayContentAll:
ContentMainReplayAllView()
case .introduceCreatorAll:
ContentMainIntroduceCreatorAllView()
case .message:
MessageView()
case .pointStatus(let refresh):
PointStatusView(refresh: refresh)
case .audition:
AuditionView()
case .characterDetail(let characterId):
CharacterDetailView(characterId: characterId)
case .chatRoom(let id):
ChatRoomView(roomId: id)
case .newCharacterAll:
NewCharacterListView()
case .originalWorkDetail(let originalId):
OriginalWorkDetailView(originalId: originalId)
case .contentAll(let isFree, let isPointOnly):
ContentAllView(isFree: isFree, isPointAvailableOnly: isPointOnly)
case .seriesMain:
SeriesMainView()
default:
EmptyView()
.frame(width: 0, height: 0, alignment: .topLeading)
if case .splash = appState.rootStep {
AppStepLayerView(step: .splash, canPgPaymentViewModel: canPgPaymentViewModel)
.navigationBarBackButtonHidden(true)
}
if isShowDialog {
SodaDialog(
title: I18n.Common.pointGrantTitle,
desc: message,
confirmButtonTitle: I18n.Common.confirm
) {
isShowDialog = false
message = ""
}
}
}
if isShowDialog {
SodaDialog(
title: I18n.Common.pointGrantTitle,
desc: message,
confirmButtonTitle: I18n.Common.confirm
) {
isShowDialog = false
message = ""
.onReceive(NotificationCenter.default.publisher(for: .pointGranted)) {
if let msg = $0.object as? String {
self.message = msg
self.isShowDialog = true
}
}
.popup(isPresented: $appState.isShowErrorPopup, type: .toast, position: .top, autohideIn: 1) {
GeometryReader { geo in
HStack {
Spacer()
Text(appState.errorMessage)
.padding(.vertical, 13.3)
.frame(width: geo.size.width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
.navigationDestination(for: AppRoute.self) { route in
if let step = appState.appStep(for: route) {
AppStepLayerView(step: step, canPgPaymentViewModel: canPgPaymentViewModel)
.navigationBarBackButtonHidden(true)
} else {
EmptyView()
.frame(width: 0, height: 0, alignment: .topLeading)
}
}
}
.onReceive(NotificationCenter.default.publisher(for: .pointGranted)) {
if let msg = $0.object as? String {
self.message = msg
self.isShowDialog = true
}
}
.popup(isPresented: $appState.isShowErrorPopup, type: .toast, position: .top, autohideIn: 1) {
GeometryReader { geo in
HStack {
Spacer()
Text(appState.errorMessage)
.padding(.vertical, 13.3)
.frame(width: geo.size.width - 66.7, alignment: .center)
.appFont(size: 12, weight: .medium)
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.cornerRadius(20)
.padding(.top, 66.7)
Spacer()
}
}
}
}
struct AppStepLayerView: View {
let step: AppStep
@ObservedObject var canPgPaymentViewModel: CanPgPaymentViewModel
@ViewBuilder
var body: some View {
switch step {
case .splash:
SplashView()
case .login:
LoginView()
case .signUp:
SignUpView()
case .findPassword:
FindPasswordView()
case .textMessageDetail(let messageItem, let messageBox, let refresh):
TextMessageDetailView(messageItem: messageItem, messageBox: messageBox, refresh: refresh)
case .writeTextMessage(let userId, let nickname):
TextMessageWriteView(replySenderId: userId, replySenderNickname: nickname)
case .writeVoiceMessage(let userId, let nickname, let onRefresh):
VoiceMessageWriteView(replySenderId: userId, replySenderNickname: nickname, onRefresh: onRefresh)
case .settings:
SettingsView()
case .languageSettings:
LanguageSettingsView()
case .notices:
NoticeListView()
case .noticeDetail(let notice):
NoticeDetailView(notice: notice)
case .events:
EventListView()
case .eventDetail(let event):
EventDetailView(event: event)
case .terms:
TermsView(isPrivacyPolicy: false)
case .privacy:
TermsView(isPrivacyPolicy: true)
case .notificationSettings:
NotificationSettingsView()
case .contentViewSettings:
ContentSettingsView()
case .signOut:
SignOutView()
case .canStatus(let refresh):
CanStatusView(refresh: refresh)
case .canCharge(let refresh, let afterCompletionToGoBack):
CanChargeView(refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
case .canPayment(let canProduct, let refresh, let afterCompletionToGoBack):
CanPaymentView(canProduct: canProduct, refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
case .canPgPayment(let canResponse, let refresh, let afterCompletionToGoBack):
CanPgPaymentView(canResponse: canResponse, refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
.environmentObject(canPgPaymentViewModel)
case .liveReservation:
LiveReservationStatusView()
case .liveReservationCancel(let reservationId):
LiveReservationCancelView(reservationId: reservationId)
case .serviceCenter:
ServiceCenterView()
case .createContent:
ContentCreateView()
case .liveReservationComplete(let response):
LiveReservationCompleteView(reservationCompleteData: response)
case .creatorDetail(let userId):
UserProfileView(userId: userId)
case .followerList(let userId):
FollowerListView(userId: userId)
case .modifyContent(let contentId):
ContentModifyView(contentId: contentId)
case .contentListAll(let userId):
ContentListView(userId: userId)
case .contentDetail(let contentId):
ContentDetailView(contentId: contentId)
case .createLive(let timeSettingMode, let onSuccess):
LiveRoomCreateView(
timeSettingMode: timeSettingMode,
onSuccess: onSuccess
)
case .liveNowAll(let onClickParticipant):
LiveNowAllView(onClickParticipant: onClickParticipant)
case .liveReservationAll(let onClickReservation, let onClickStart, let onClickCancel, let onTapCreateLive):
LiveReservationAllView(
onClickReservation: onClickReservation,
onClickStart: onClickStart,
onClickCancel: onClickCancel,
onTapCreateLive: onTapCreateLive
)
case .modifyLive(let room):
LiveRoomEditView(room: room)
case .liveDetail(let roomId, let onClickParticipant, let onClickReservation, let onClickStart, let onClickCancel):
LiveDetailView(
roomId: roomId,
onClickParticipant: onClickParticipant,
onClickReservation: onClickReservation,
onClickStart: onClickStart,
onClickCancel: onClickCancel
)
case .modifyPassword:
ModifyPasswordView()
case .changeNickname:
NicknameUpdateView()
case .profileUpdate(let refresh):
ProfileUpdateView(refresh: refresh)
case .followingList:
FollowCreatorView()
case .orderListAll:
OrderListAllView()
case .userProfileDonationAll(let userId):
UserProfileDonationAllView(userId: userId)
case .channelDonationAll(let creatorId):
ChannelDonationAllView(creatorId: creatorId)
case .userProfileFanTalkAll(let userId):
UserProfileFanTalkAllView(userId: userId)
case .newContentAll(let isFree):
ContentNewAllView(isFree: isFree)
case .curationAll(let title, let curationId):
ContentCurationView(title: title, curationId: curationId)
case .contentRankingAll:
ContentRankingAllView()
case .creatorCommunityAll(let creatorId):
CreatorCommunityAllView(creatorId: creatorId)
case .creatorCommunityWrite(let onSuccess):
CreatorCommunityWriteView(onSuccess: onSuccess)
case .creatorCommunityModify(let postId, let onSuccess):
CreatorCommunityModifyView(postId: postId, onSuccess: onSuccess)
case .canCoupon(let refresh):
CanCouponView(refresh: refresh)
case .contentAllByTheme(let themeId):
ContentAllByThemeView(themeId: themeId)
case .seriesAll(let creatorId, let creatorNickname, let isOriginal, let isCompleted):
SeriesListAllView(creatorId: creatorId, creatorNickname: creatorNickname, isOriginal: isOriginal, isCompleted: isCompleted)
case .seriesDetail(let seriesId):
SeriesDetailView(seriesId: seriesId)
case .seriesContentAll(let seriesId, let seriesTitle):
SeriesContentAllView(seriesId: seriesId, seriesTitle: seriesTitle)
case .tempCanPayment(let orderType, let contentId, let title, let can):
CanPaymentTempView(orderType: orderType, contentId: contentId, title: title, can: can)
case .blockList:
BlockMemberListView()
case .myBox(let currentTab):
ContentBoxView(initCurrentTab: currentTab)
case .auditionDetail(let auditionId):
AuditionDetailView(auditionId: auditionId)
case .auditionRoleDetail(let roleId, let auditionTitle):
AuditionRoleDetailView(
roleId: roleId,
auditionTitle: auditionTitle
)
case .search:
SearchView()
case .contentMain(let startTab):
ContentMainViewV2(selectedTab: startTab)
case .completedSeriesAll:
CompletedSeriesView()
case .newAlarmContentAll:
ContentMainAlarmAllView()
case .newAsmrContentAll:
ContentMainAsmrAllView()
case .newReplayContentAll:
ContentMainReplayAllView()
case .introduceCreatorAll:
ContentMainIntroduceCreatorAllView()
case .message:
MessageView()
case .pointStatus(let refresh):
PointStatusView(refresh: refresh)
case .audition:
AuditionView()
case .characterDetail(let characterId):
CharacterDetailView(characterId: characterId)
case .chatRoom(let id):
ChatRoomView(roomId: id)
case .newCharacterAll:
NewCharacterListView()
case .originalWorkDetail(let originalId):
OriginalWorkDetailView(originalId: originalId)
case .contentAll(let isFree, let isPointOnly):
ContentAllView(isFree: isFree, isPointAvailableOnly: isPointOnly)
case .seriesMain:
SeriesMainView()
case .main:
EmptyView()
.frame(width: 0, height: 0, alignment: .topLeading)
}
}
}

View File

@@ -25,7 +25,7 @@ struct CreatorCommunityCommentListView: View {
@StateObject var viewModel = CreatorCommunityCommentListViewModel()
var body: some View {
NavigationView {
Group {
ZStack {
VStack(spacing: 0) {
HStack(spacing: 0) {

View File

@@ -56,12 +56,15 @@ struct CreatorCommunityAllView: View {
.sheet(
isPresented: $viewModel.isShowCommentListView,
content: {
CreatorCommunityCommentListView(
isPresented: $viewModel.isShowCommentListView,
creatorId: creatorId,
postId: viewModel.postId,
isShowSecret: viewModel.isShowSecret
)
NavigationStack {
CreatorCommunityCommentListView(
isPresented: $viewModel.isShowCommentListView,
creatorId: creatorId,
postId: viewModel.postId,
isShowSecret: viewModel.isShowSecret
)
}
.toolbar(.hidden, for: .navigationBar)
}
)

View File

@@ -26,6 +26,7 @@ struct UserProfileView: View {
@State private var isShowChannelDonationDialog: Bool = false
@State private var didTriggerAutoBackOnLoadFailure: Bool = false
@State private var isViewVisible: Bool = false
@State private var loadedUserId: Int? = nil
@State private var maxCommunityPostHeight: CGFloat? = nil
@@ -639,7 +640,12 @@ struct UserProfileView: View {
.onAppear {
isViewVisible = true
didTriggerAutoBackOnLoadFailure = false
viewModel.getCreatorProfile(userId: userId)
if loadedUserId != userId || viewModel.creatorProfile == nil {
loadedUserId = userId
viewModel.getCreatorProfile(userId: userId)
}
AppState.shared.pushChannelId = 0
}
.onDisappear {

View File

@@ -9,10 +9,11 @@ import SwiftUI
struct OrderListAllInnerView: View {
@StateObject var viewModel = OrderListAllViewModel()
@State private var isInitialized = false
var body: some View {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
VStack(spacing: 0) {
HStack(spacing: 0) {
Text("")
.appFont(size: 13.3, weight: .medium)
@@ -38,19 +39,16 @@ struct OrderListAllInnerView: View {
ForEach(0..<viewModel.orderList.count, id: \.self) { index in
let item = viewModel.orderList[index]
NavigationLink {
ContentDetailView(contentId: item.contentId)
} label: {
OrderListItemView(item: item)
.contentShape(Rectangle())
.padding(.horizontal, 13.3)
.padding(.top, 6.7)
.onAppear {
if index == viewModel.orderList.count - 1 {
viewModel.getOrderList()
}
OrderListItemView(item: item)
.contentShape(Rectangle())
.padding(.horizontal, 13.3)
.padding(.top, 6.7)
.onAppear {
if index == viewModel.orderList.count - 1 {
viewModel.getOrderList()
}
}
}
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: item.contentId)) }
}
}
}
@@ -58,7 +56,10 @@ struct OrderListAllInnerView: View {
.padding(.top, 13.3)
}
.onAppear {
viewModel.getOrderList()
if !isInitialized {
viewModel.getOrderList()
isInitialized = true
}
}
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
HStack {

View File

@@ -10,7 +10,7 @@ import SwiftUI
struct OrderListAllView: View {
var body: some View {
NavigationView {
Group {
VStack(spacing: 0) {
HStack(spacing: 0) {
Button {

View File

@@ -96,26 +96,23 @@ struct SearchCreatorItemView: View {
let item: SearchResponseItem
var body: some View {
NavigationLink {
UserProfileView(userId: item.id)
} label: {
HStack(spacing: 13.3) {
KFImage(URL(string: item.imageUrl))
.cancelOnDisappear(true)
.downsampling(size: CGSize(width: 60, height: 60))
.resizable()
.frame(width: 60, height: 60)
.clipShape(Circle())
Text(item.nickname)
.appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.grayee)
Spacer()
}
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
HStack(spacing: 13.3) {
KFImage(URL(string: item.imageUrl))
.cancelOnDisappear(true)
.downsampling(size: CGSize(width: 60, height: 60))
.resizable()
.frame(width: 60, height: 60)
.clipShape(Circle())
Text(item.nickname)
.appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.grayee)
Spacer()
}
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
.onTapGesture { AppState.shared.setAppStep(step: .creatorDetail(userId: item.id)) }
}
}
@@ -123,34 +120,31 @@ struct SearchContentItemView: View {
let item: SearchResponseItem
var body: some View {
NavigationLink {
ContentDetailView(contentId: item.id)
} label: {
HStack(spacing: 13.3) {
KFImage(URL(string: item.imageUrl))
.cancelOnDisappear(true)
.downsampling(size: CGSize(width: 60, height: 60))
.resizable()
.frame(width: 60, height: 60)
.cornerRadius(5.3)
HStack(spacing: 13.3) {
KFImage(URL(string: item.imageUrl))
.cancelOnDisappear(true)
.downsampling(size: CGSize(width: 60, height: 60))
.resizable()
.frame(width: 60, height: 60)
.cornerRadius(5.3)
VStack(alignment: .leading, spacing: 6.7) {
Text(item.title)
.appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.grayee)
.multilineTextAlignment(.leading)
.lineLimit(2)
VStack(alignment: .leading, spacing: 6.7) {
Text(item.title)
.appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.grayee)
.multilineTextAlignment(.leading)
.lineLimit(2)
Text(item.nickname)
.appFont(size: 10, weight: .medium)
.foregroundColor(Color.gray77)
}
Spacer()
Text(item.nickname)
.appFont(size: 10, weight: .medium)
.foregroundColor(Color.gray77)
}
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
Spacer()
}
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: item.id)) }
}
}
@@ -158,36 +152,33 @@ struct SearchSeriesItemView: View {
let item: SearchResponseItem
var body: some View {
NavigationLink {
SeriesDetailView(seriesId: item.id)
} label: {
HStack(spacing: 13.3) {
KFImage(URL(string: item.imageUrl))
.cancelOnDisappear(true)
.downsampling(size: CGSize(width: 60, height: 85))
.resizable()
.scaledToFill()
.frame(width: 60, height: 85)
.clipped()
.cornerRadius(5.3)
HStack(spacing: 13.3) {
KFImage(URL(string: item.imageUrl))
.cancelOnDisappear(true)
.downsampling(size: CGSize(width: 60, height: 85))
.resizable()
.scaledToFill()
.frame(width: 60, height: 85)
.clipped()
.cornerRadius(5.3)
VStack(alignment: .leading, spacing: 6.7) {
Text(item.title)
.appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.grayee)
.multilineTextAlignment(.leading)
.lineLimit(2)
VStack(alignment: .leading, spacing: 6.7) {
Text(item.title)
.appFont(size: 13.3, weight: .medium)
.foregroundColor(Color.grayee)
.multilineTextAlignment(.leading)
.lineLimit(2)
Text(item.nickname)
.appFont(size: 10, weight: .medium)
.foregroundColor(Color.gray77)
}
Spacer()
Text(item.nickname)
.appFont(size: 10, weight: .medium)
.foregroundColor(Color.gray77)
}
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
Spacer()
}
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
.onTapGesture { AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.id)) }
}
}

View File

@@ -26,7 +26,7 @@ struct SearchView: View {
]
var body: some View {
NavigationView {
Group {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
HStack(spacing: 0) {

View File

@@ -76,6 +76,7 @@ struct SplashView: View {
)
}
}
.toolbar(.hidden, for: .navigationBar)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
AppState.shared.isRestartApp = false