diff --git a/Projects/Feature/Feature/Sources/AppCore.swift b/Projects/Feature/Feature/Sources/AppCore.swift index 5c46a93..60816c4 100644 --- a/Projects/Feature/Feature/Sources/AppCore.swift +++ b/Projects/Feature/Feature/Sources/AppCore.swift @@ -12,7 +12,6 @@ import SplashFeature import HomeFeature import HomeFeatureInterface import OnboardingFeature -import OnboardingFeatureInterface import PushService import ComposableArchitecture @@ -92,14 +91,14 @@ public struct AppCore { state.onboarding = OnboardingCore.State() return .none + case .splash: + return .none + case .home: return .none case .onboarding: return .none - - default: - return .none } } } diff --git a/Projects/Feature/Feature/Sources/AppView.swift b/Projects/Feature/Feature/Sources/AppView.swift index b9abd97..9b6a249 100644 --- a/Projects/Feature/Feature/Sources/AppView.swift +++ b/Projects/Feature/Feature/Sources/AppView.swift @@ -12,7 +12,6 @@ import SplashFeature import HomeFeature import HomeFeatureInterface import OnboardingFeature -import OnboardingFeatureInterface import ComposableArchitecture diff --git a/Projects/Feature/OnboardingFeature/Interface/OnboardingCoreInterface.swift b/Projects/Feature/OnboardingFeature/Interface/OnboardingCoreInterface.swift deleted file mode 100644 index 72fd03e..0000000 --- a/Projects/Feature/OnboardingFeature/Interface/OnboardingCoreInterface.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// OnboardingCoreInterface.swift -// OnboardingFeatureInterface -// -// Created by devMinseok on 7/22/24. -// Copyright © 2024 PomoNyang. All rights reserved. -// - -import Foundation - -import ComposableArchitecture - -@Reducer -public struct OnboardingCore { - private let reducer: Reduce - - public init(reducer: Reduce) { - self.reducer = reducer - } - - @ObservableState - public struct State: Equatable { - public init() {} - } - - public enum Action { - case onAppear - } - - public var body: some ReducerOf { - reducer - } -} diff --git a/Projects/Feature/OnboardingFeature/Interface/OnboardingView.swift b/Projects/Feature/OnboardingFeature/Interface/OnboardingView.swift deleted file mode 100644 index 6a32071..0000000 --- a/Projects/Feature/OnboardingFeature/Interface/OnboardingView.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// OnboardingView.swift -// OnboardingFeatureInterface -// -// Created by devMinseok on 7/22/24. -// Copyright © 2024 PomoNyang. All rights reserved. -// - -import SwiftUI - -import ComposableArchitecture - -public struct OnboardingView: View { - let store: StoreOf - - public init(store: StoreOf) { - self.store = store - } - - public var body: some View { - VStack { - Text("Onboarding") - .foregroundStyle(Color.black) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.white) - } -} diff --git a/Projects/Feature/OnboardingFeature/Project.swift b/Projects/Feature/OnboardingFeature/Project.swift index 8ab4340..57628ca 100644 --- a/Projects/Feature/OnboardingFeature/Project.swift +++ b/Projects/Feature/OnboardingFeature/Project.swift @@ -10,13 +10,12 @@ let project: Project = .makeTMABasedProject( scripts: [], targets: [ .sources, - .interface, .tests, .testing, .example ], dependencies: [ - .interface: [ + .sources: [ .dependency(rootModule: Domain.self) ] ] diff --git a/Projects/Feature/OnboardingFeature/Sources/Onboarding/Helpers/OffsetReader.swift b/Projects/Feature/OnboardingFeature/Sources/Onboarding/Helpers/OffsetReader.swift new file mode 100644 index 0000000..cf029d8 --- /dev/null +++ b/Projects/Feature/OnboardingFeature/Sources/Onboarding/Helpers/OffsetReader.swift @@ -0,0 +1,34 @@ +// +// OffsetReader.swift +// OnboardingFeature +// +// Created by 김지현 on 8/14/24. +// Copyright © 2024 PomoNyang. All rights reserved. +// + +import SwiftUI + +struct OffsetKey: PreferenceKey { + static var defaultValue: CGFloat = .zero + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = nextValue() + } +} + +extension View { + @ViewBuilder + func offsetX(_ addObserver: Bool, completion: @escaping (CGFloat) -> Void) -> some View { + self + .frame(maxWidth: .infinity) + .overlay { + if addObserver { + GeometryReader { geometry in + let minX = geometry.frame(in: .global).minX + Color.clear + .preference(key: OffsetKey.self, value: minX) + .onPreferenceChange(OffsetKey.self, perform: completion) + } + } + } + } +} diff --git a/Projects/Feature/OnboardingFeature/Sources/Onboarding/OnboardingCore.swift b/Projects/Feature/OnboardingFeature/Sources/Onboarding/OnboardingCore.swift new file mode 100644 index 0000000..d71336e --- /dev/null +++ b/Projects/Feature/OnboardingFeature/Sources/Onboarding/OnboardingCore.swift @@ -0,0 +1,129 @@ +// +// OnboardingCore.swift +// OnboardingFeature +// +// Created by devMinseok on 7/22/24. +// Copyright © 2024 PomoNyang. All rights reserved. +// + +import Foundation + +import ComposableArchitecture + +@Reducer +public struct OnboardingCore { + @ObservableState + public struct State: Equatable { + public init() { } + var data: [OnboardingItem] = OnboardingItemsData + var fakedData: [OnboardingItem] = [] + var width: CGFloat = .zero + var currentIdx: Int = 0 + var currentItemID: String = "" + } + + public enum Action: BindableAction { + case onApear + case calculateOffset(CGFloat, OnboardingItem) + case dragStart + case _timerStart + case _timerEnd + case _timerTicked + case _nextPage(Int) + case _resetTofront + case binding(BindingAction) + } + + public init() { } + + @Dependency(\.continuousClock) var clock + @Dependency(\.mainQueue) var mainQueue + + public var body: some ReducerOf { + BindingReducer() + Reduce(self.core) + } + + private func core(_ state: inout State, _ action: Action) -> EffectOf { + enum CancelID { case timer, timerDebounce } + + switch action { + case .onApear: + state.currentItemID = state.data.first!.id.uuidString + state.fakedData = state.data + guard var first = state.data.first, + var last = state.data.last else { return .none } + first.id = .init() + last.id = .init() + state.fakedData.append(first) + state.fakedData.insert(last, at: 0) + return .run { send in + await send(._timerStart) + } + + case .calculateOffset(let minX, let item): + let fakeIndex = state.fakedData.firstIndex(of: item) ?? 0 + state.currentIdx = state.data.firstIndex { item in + item.id.uuidString == state.currentItemID + } ?? 0 + let pageOffset = minX - state.width * CGFloat(fakeIndex) + + let pageProgress: CGFloat = pageOffset / state.width + if -pageProgress < 1.0 { + if state.fakedData.indices.contains(state.fakedData.count - 1) { + state.currentItemID = state.fakedData[state.fakedData.count - 1].id.uuidString + } + } + if -pageProgress > CGFloat(state.fakedData.count - 1) { + if state.fakedData.indices.contains(1) { + state.currentItemID = state.fakedData[1].id.uuidString + } + } + return .none + + case .dragStart: + let timerEndAction: Effect = .run { send in await send(._timerEnd) } + let timerStartAction: Effect = .run { send in await send(._timerStart) } + .debounce(id: CancelID.timerDebounce, for: .seconds(2), scheduler: self.mainQueue) + return .merge( + timerEndAction, + timerStartAction + ) + + case ._timerStart: + return .run { send in + for await _ in self.clock.timer(interval: .seconds(3)) { + await send(._timerTicked) + } + } + .cancellable(id: CancelID.timer) + + case ._timerEnd: + return .cancel(id: CancelID.timer) + + case ._timerTicked: + let index = state.fakedData.firstIndex { item in + item.id.uuidString == state.currentItemID + } ?? 0 + return .run { [index = index] send in + if index == 4 { + await send(._resetTofront) + await send(._nextPage(2), animation: .easeInOut(duration: 0.3)) + } else { + await send(._nextPage(index + 1), animation: .easeInOut(duration: 0.3)) + } + } + + case ._nextPage(let index): + state.currentItemID = state.fakedData[index].id.uuidString + return .none + + case ._resetTofront: + state.currentItemID = state.fakedData[1].id.uuidString + return .none + + default: + return .none + } + } +} diff --git a/Projects/Feature/OnboardingFeature/Sources/Onboarding/OnboardingItem.swift b/Projects/Feature/OnboardingFeature/Sources/Onboarding/OnboardingItem.swift new file mode 100644 index 0000000..e38a710 --- /dev/null +++ b/Projects/Feature/OnboardingFeature/Sources/Onboarding/OnboardingItem.swift @@ -0,0 +1,34 @@ +// +// OnboardingItem.swift +// OnboardingFeature +// +// Created by 김지현 on 8/9/24. +// Copyright © 2024 PomoNyang. All rights reserved. +// + +import SwiftUI + +import DesignSystem + +public struct OnboardingItem: Equatable, Identifiable { + public var id: UUID = .init() + let image: Image + var title: String + let subTitle: String +} + +let OnboardingItemsData: [OnboardingItem] = [ + OnboardingItem( + image: Image(systemName: "star"), + title: "모하냥과 함께 집중시간을 늘려보세요", + subTitle: "고양이 종에 따라 성격이 달라요.\n취향에 맞는 고양이를 선택해 몰입해 보세요."), + OnboardingItem( + image: Image(systemName: "star.fill"), + title: "다른 앱을 실행하면 방해 알림을 보내요", + subTitle: "뽀모도로를 실행한 후, 다른 앱을 사용하면\n설정한 주기로 방해 알림을 보내드려요."), + OnboardingItem( + image: Image(systemName: "house"), + title: "집중과 휴식 반복을 통해 몰입을 관리해요", + subTitle: "일정 시간 집중과 휴식을 반복해 번아웃을 방지하고\n짧은 시간의 몰입을 경험해보세요.") +] + diff --git a/Projects/Feature/OnboardingFeature/Sources/Onboarding/OnboardingView.swift b/Projects/Feature/OnboardingFeature/Sources/Onboarding/OnboardingView.swift new file mode 100644 index 0000000..65e52b6 --- /dev/null +++ b/Projects/Feature/OnboardingFeature/Sources/Onboarding/OnboardingView.swift @@ -0,0 +1,105 @@ +// +// OnboardingView.swift +// OnboardingFeature +// +// Created by 김지현 on 8/9/24. +// Copyright © 2024 PomoNyang. All rights reserved. +// + +import SwiftUI +import DesignSystem + +import ComposableArchitecture + +public struct OnboardingView: View { + @Bindable var store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + ZStack { + Alias.Color.Background.primary + .ignoresSafeArea() + VStack { + Spacer() + VStack(spacing: 0) { + TabView(selection: $store.currentItemID) { + ForEach(store.fakedData) { item in + OnboardingCarouselContentView(width: $store.width, item: item) + .tag(item.id.uuidString) + .offsetX(store.currentItemID == item.id.uuidString) { minX in + store.send(.calculateOffset(minX, item)) + } + } + } + .tabViewStyle(.page(indexDisplayMode: .never)) + .gesture( + DragGesture() + .onChanged { _ in store.send(.dragStart) } + ) + .frame(width: store.width, height: 350) + + HStack { + ForEach(0..<3, id: \.self) { idx in + Circle() + .frame(width: 8, height: 8) + .foregroundStyle( + idx == store.currentIdx ? + Alias.Color.Background.tertiary : Alias.Color.Background.secondary + ) + } + } + .padding(.vertical, 32) + + Button(title: "시작하기") { + // 다음 뷰 이동 + } + .buttonStyle(.box(level: .primary, size: .large, width: .medium)) + .padding(.top, 16) + } + Spacer() + } + } + .onAppear { store.send(.onApear) } + .background { + GeometryReader { geometry in + Color.clear + .onAppear { store.width = geometry.size.width } + } + } + } +} + +struct OnboardingCarouselContentView: View { + @Binding var width: CGFloat + var item: OnboardingItem + var body: some View { + VStack(spacing: Alias.Spacing.xxxLarge) { + ZStack { + Rectangle() + .foregroundStyle(Alias.Color.Background.secondary) + item.image + } + .frame(width: 240, height: 240) + + VStack(spacing: Alias.Spacing.small) { + Text(item.title) + .font(Typography.header4) + .foregroundStyle(Alias.Color.Text.primary) + Text(item.subTitle) + .font(Typography.bodyR) + .foregroundStyle(Alias.Color.Text.secondary) + } + .multilineTextAlignment(.center) + } + .frame(width: width, height: 350) + } +} + +struct OnboardingView_Previews: PreviewProvider { + static var previews: some View { + OnboardingView(store: Store(initialState: OnboardingCore.State()) { OnboardingCore() }) + } +} diff --git a/Projects/Feature/OnboardingFeature/Sources/OnboardingCore.swift b/Projects/Feature/OnboardingFeature/Sources/OnboardingCore.swift deleted file mode 100644 index 9328ed0..0000000 --- a/Projects/Feature/OnboardingFeature/Sources/OnboardingCore.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// OnboardingCore.swift -// OnboardingFeature -// -// Created by devMinseok on 7/22/24. -// Copyright © 2024 PomoNyang. All rights reserved. -// - -import OnboardingFeatureInterface - -import ComposableArchitecture - -extension OnboardingCore { - public init() { - let reducer = Reduce { _, action in - switch action { - case .onAppear: - return .none - } - } - self.init(reducer: reducer) - } -} diff --git a/Projects/Shared/DesignSystem/Sources/Component/Button/Detail/ButtonHuggingPriorityHorizontal.swift b/Projects/Shared/DesignSystem/Sources/Component/Button/Detail/ButtonHuggingPriorityHorizontal.swift index 5673d14..ce86b5b 100644 --- a/Projects/Shared/DesignSystem/Sources/Component/Button/Detail/ButtonHuggingPriorityHorizontal.swift +++ b/Projects/Shared/DesignSystem/Sources/Component/Button/Detail/ButtonHuggingPriorityHorizontal.swift @@ -10,6 +10,7 @@ import Foundation public enum ButtonHuggingPriorityHorizontal { case high + case medium case low } @@ -18,6 +19,8 @@ extension ButtonHuggingPriorityHorizontal { switch self { case .high: return nil + case .medium: + return Alias.Size.ButtonWidth.fixed case .low: return .infinity }