Skip to content

Commit

Permalink
[Feature/#331] 코치 마크 구현 (#338)
Browse files Browse the repository at this point in the history
* feat: BottleFontSystem mainTitle 추가

* feat: SandBeachView 디자인 QA 반영

* feat: UserClient CoachMarkState 추가

* feat: SandBeachCoachMarkFeature 구현

* feat: SandBeachCoachMarkView 구현

* feat: SandBeachCoachMarkView 연결

* feat: PopupType CoachMark 추가

* feat: 코치마크 구현

* feat: CoachMark 3번 클릭 시 끝나도록 구현
  • Loading branch information
leemhyungyu authored Nov 4, 2024
1 parent 1f78df6 commit 0612812
Show file tree
Hide file tree
Showing 13 changed files with 304 additions and 61 deletions.
15 changes: 14 additions & 1 deletion Projects/Domain/User/Interface/Sources/UserClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import Combine
public struct UserClient {
private let _isLoggedIn: () -> Bool
private let _isAppDeleted: () -> Bool
private let _isCoachMarkViewed: () -> Bool
private let _fetchFcmToken: () -> String?
private let updateLoginState: (Bool) -> Void
private let updateDeleteState: (Bool) -> Void
private let updateCoachMarkState: (Bool) -> Void
private let updateFcmToken: (String) -> Void
private let updatePushNotificationAllowStatus: (Bool) -> Void
private let _fetchAlertState: () async throws -> [UserAlertState]
Expand All @@ -31,11 +33,13 @@ public struct UserClient {
public init(
isLoggedIn: @escaping () -> Bool,
isAppDeleted: @escaping () -> Bool,
isCoachMarkViewed: @escaping () -> Bool,
fetchFcmToken: @escaping () -> String?,
updateLoginState: @escaping (Bool) -> Void,
updateDeleteState: @escaping (Bool) -> Void,
updateFcmToken: @escaping (String) -> Void,
updatePushNotificationAllowStatus: @escaping (Bool) -> Void,
updateCoachMarkState: @escaping (Bool) -> Void,
fetchAlertState: @escaping () async throws -> [UserAlertState],
fetchPushNotificationAllowStatus: @escaping () -> Bool,
updateAlertState: @escaping (UserAlertState) async throws -> Void,
Expand All @@ -44,11 +48,13 @@ public struct UserClient {
) {
self._isLoggedIn = isLoggedIn
self._isAppDeleted = isAppDeleted
self._isCoachMarkViewed = isCoachMarkViewed
self._fetchFcmToken = fetchFcmToken
self.updateLoginState = updateLoginState
self.updateDeleteState = updateDeleteState
self.updateFcmToken = updateFcmToken
self.updatePushNotificationAllowStatus = updatePushNotificationAllowStatus
self.updateCoachMarkState = updateCoachMarkState
self._fetchAlertState = fetchAlertState
self._fetchPushNotificationAllowStatus = fetchPushNotificationAllowStatus
self.updateAlertState = updateAlertState
Expand All @@ -64,6 +70,10 @@ public struct UserClient {
_isAppDeleted()
}

public func isCoachMarkViewd() -> Bool {
_isCoachMarkViewed()
}

public func fetchFcmToken() -> String? {
_fetchFcmToken()
}
Expand All @@ -75,7 +85,10 @@ public struct UserClient {
public func updateDeleteState(isDelete: Bool) {
updateDeleteState(isDelete)
}


public func updateCoachMarkState(isViewed: Bool) {
updateCoachMarkState(isViewed)
}
public func updateFcmToken(fcmToken: String) {
updateFcmToken(fcmToken)
}
Expand Down
9 changes: 9 additions & 0 deletions Projects/Domain/User/Sources/UserClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ extension UserClient: DependencyKey {
case deleteState
case fcmToken
case alertAllowState
case coachMarkState
}

static public var liveValue: UserClient = .live()
Expand All @@ -38,6 +39,10 @@ extension UserClient: DependencyKey {
return !UserDefaults.standard.bool(forKey: UserDefaultsKeys.deleteState.rawValue)
},

isCoachMarkViewed: {
return UserDefaults.standard.bool(forKey: UserDefaultsKeys.coachMarkState.rawValue)
},

fetchFcmToken: {
return UserDefaults.standard.string(forKey: UserDefaultsKeys.fcmToken.rawValue)
},
Expand All @@ -58,6 +63,10 @@ extension UserClient: DependencyKey {
UserDefaults.standard.set(isAllow, forKey: UserDefaultsKeys.alertAllowState.rawValue)
},

updateCoachMarkState: { isViewed in
UserDefaults.standard.set(isViewed, forKey: UserDefaultsKeys.coachMarkState.rawValue)
},

fetchAlertState: {
let responseData = try await networkManager.reqeust(api: .apiType(UserAPI.fetchAlertState), dto: [AlertStateResponseDTO].self)
return responseData.map { $0.toDomain() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import Foundation
import FeatureProfileSetupInterface
import FeatureBottleArrivalInterface
import FeatureTabBarInterface

import DomainProfile
import DomainUserInterface

import ComposableArchitecture

Expand Down Expand Up @@ -38,26 +40,32 @@ public struct SandBeachRootFeature {
var introduction: String
var profileImageData: Data
var isLoading: Bool
public var isCoachMarkViewed: Bool = true

public var sandBeach: SandBeachFeature.State
public var sandBeachCoachMark: SandBeachCoachMarkFeature.State

public init(
path: StackState<Path.State> = StackState<Path.State>(),
introduction: String = "",
profileImageData: Data = .init(),
sandBeach: SandBeachFeature.State = .init(),
sandBeachCoachMark: SandBeachCoachMarkFeature.State = .init(),
isLoading: Bool = false
) {
self.path = path
self.introduction = introduction
self.profileImageData = profileImageData
self.sandBeach = sandBeach
self.sandBeachCoachMark = sandBeachCoachMark
self.isLoading = isLoading
}
}

public enum Action {
case path(StackAction<Path.State, Path.Action>)
case sandBeach(SandBeachFeature.Action)
case sandBeachCoachMark(SandBeachCoachMarkFeature.Action)
case profileSetupDidCompleted
case delegate(Delegate)
case selectedTabDidChanged(selectedTab: TabType)
Expand All @@ -74,6 +82,10 @@ public struct SandBeachRootFeature {
SandBeachFeature()
}

Scope(state: \.sandBeachCoachMark, action: \.sandBeachCoachMark) {
SandBeachCoachMarkFeature()
}

reducer
.forEach(\.path, action: \.path)
}
Expand All @@ -84,7 +96,8 @@ extension SandBeachRootFeature {

let reducer = Reduce<State, Action> { state, action in
@Dependency(\.profileClient) var profileClient

@Dependency(\.userClient) var userClient

switch action {

// IntrodctionSetup Delegate
Expand Down Expand Up @@ -147,6 +160,10 @@ extension SandBeachRootFeature {
case .writeButtonDidTapped:
state.path.append(.IntroductionSetup(IntroductionSetupFeature.State()))
return .none

case .sandBeachLoadCompleted:
state.isCoachMarkViewed = userClient.isCoachMarkViewd()
return .none
}

// BottleArrivalDetail Delegate
Expand All @@ -157,6 +174,15 @@ extension SandBeachRootFeature {
return .none
}

// SandBeachCoachMark Delegate
case let .sandBeachCoachMark(.delegate(delegate)):
switch delegate {
case .coachMarkDidCompleted:
userClient.updateCoachMarkState(isViewed: true)
state.isCoachMarkViewed = true
return .none
}

case .profileSetupDidCompleted:
state.isLoading = false
state.path.removeAll()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,17 @@ public struct SandBeachRootView: View {
public var body: some View {
WithPerceptionTracking {
NavigationStack(path: $store.scope(state: \.path, action: \.path)) {
ZStack {
SandBeachView(store: store.scope(state: \.sandBeach, action: \.sandBeach))
.setTabBar(selectedTab: .sandBeach) { selectedTab in
store.send(.selectedTabDidChanged(selectedTab: selectedTab))
.setTabBar(selectedTab: .sandBeach) { selectedTab in
store.send(.selectedTabDidChanged(selectedTab: selectedTab))
}

if !store.isCoachMarkViewed && store.sandBeach.userState == .noIntroduction {
SandBeachCoachMarkView(
store: store.scope(state: \.sandBeachCoachMark, action: \.sandBeachCoachMark))
}
}
} destination: { store in
WithPerceptionTracking {
switch store.state {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public struct SandBeachFeature {
case writeButtonDidTapped
case newBottleIslandDidTapped
case bottleStorageIslandDidTapped
case sandBeachLoadCompleted
}

case alert(Alert)
Expand Down Expand Up @@ -152,7 +153,7 @@ extension SandBeachFeature {
state.userState = userState
state.isDisableIslandBottle = isDisableButton
state.isLoading = false
return .none
return .send(.delegate(.sandBeachLoadCompleted))

case .writeButtonDidTapped:
return .send(.delegate(.writeButtonDidTapped))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,67 +20,72 @@ public struct SandBeachView: View {

public var body: some View {
WithPerceptionTracking {
GeometryReader { geo in
WithPerceptionTracking {
if store.userState == .none && store.isLoading {
LoadingIndicator()
} else {
VStack(spacing: 0) {
Spacer()
BottleImageView(type: .local(bottleImageSystem: .illustraition(.logo)))
.frame(width: 78.06, height: 20)
.padding(.top, geo.safeAreaInsets.top + 14)
.padding(.bottom, 38)

WantedSansStyleText(
store.userState.title, style: .title1, color: .secondary)
.frame(height: 62)
.multilineTextAlignment(.center)
.padding(.bottom, 24)
Spacer()

popup
.padding(.bottom, 8)

BottleImageView(type: .local(
bottleImageSystem:
store.userState.isEmptyBottle ? .illustraition(.islandEmptyBottle) : .illustraition(.islandHasBottle))
)
.frame(width: geo.size.width)
.frame(height: geo.size.width)
.asThrottleButton {
if store.userState.isHasNewBottle {
store.send(.newBottleIslandDidTapped)
} else if store.userState.isHasActiveBottle {
store.send(.bottleStorageIslandDidTapped)
} else if store.userState != .noIntroduction {
store.send(.newBottleIslandDidTapped)
}
}
.disabled(store.isDisableIslandBottle)

Spacer()
}
}
if store.userState == .none && store.isLoading {
LoadingIndicator()
} else {
VStack(spacing: 0) {
Spacer()
.frame(height: 1)
logoImage
userStateTitle
popup
islandImage
Spacer()
}
}
.bottleAlert($store.scope(state: \.destination?.alert, action: \.destination.alert))
.onAppear {
store.send(.onAppear)
}
.background {
BottleImageView(
type: .local(bottleImageSystem: .illustraition(.sandBeachBackground))
)
}
}
.edgesIgnoringSafeArea([.top, .bottom])
.bottleAlert($store.scope(state: \.destination?.alert, action: \.destination.alert))
.onAppear {
store.send(.onAppear)
}
.background {
BottleImageView(
type: .local(bottleImageSystem: .illustraition(.sandBeachBackground))
)
.edgesIgnoringSafeArea(.all)
}
}
}

// MARK: - Views

public extension SandBeachView {
var logoImage: some View {
BottleImageView(type: .local(bottleImageSystem: .illustraition(.logo)))
.frame(width: 78.06, height: 20)
.padding(.top, 14)
.padding(.bottom, 46)
}

var userStateTitle: some View {
WantedSansStyleText(
store.userState.title, style: .mainTitle, color: .secondary)
.multilineTextAlignment(.center)
.padding(.bottom, store.userState == .noIntroduction ? 32 : 64)
.lineSpacing(5)
}

var islandImage: some View {
GeometryReader { geo in
BottleImageView(type: .local(
bottleImageSystem:
store.userState.isEmptyBottle ? .illustraition(.islandEmptyBottle) : .illustraition(.islandHasBottle))
)
.frame(width: geo.size.width)
.frame(height: geo.size.width)
.asThrottleButton {
if store.userState.isHasNewBottle {
store.send(.newBottleIslandDidTapped)
} else if store.userState.isHasActiveBottle {
store.send(.bottleStorageIslandDidTapped)
} else if store.userState != .noIntroduction {
store.send(.newBottleIslandDidTapped)
}
}
.disabled(store.isDisableIslandBottle)
}
}

@ViewBuilder
var popup: some View {
let userState = store.userState
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// SandBeachCoachMarkFeature.swift
// FeatureSandBeachInterface
//
// Created by 임현규 on 10/28/24.
//

import ComposableArchitecture

extension SandBeachCoachMarkFeature {
public init() {
let reducer = Reduce<State, Action> { state, action in
switch action {
case .coachMarkDidTapped:
state.count += 1

if state.count == 3 {
return .send(.delegate(.coachMarkDidCompleted))
} else {
return .none
}

case .delegate:
return .none
}
}

self.init(reducer: reducer)
}
}
Loading

0 comments on commit 0612812

Please sign in to comment.