diff --git a/Score/Score.xcodeproj/project.pbxproj b/Score/Score.xcodeproj/project.pbxproj index b4c7704..8ad1e5d 100644 --- a/Score/Score.xcodeproj/project.pbxproj +++ b/Score/Score.xcodeproj/project.pbxproj @@ -27,6 +27,15 @@ FD208FC92BB9EAEF00DD4D3B /* SCProgressCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD208FC82BB9EAEF00DD4D3B /* SCProgressCard.swift */; }; FD208FCB2BB9EDCD00DD4D3B /* SCInformationCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD208FCA2BB9EDCD00DD4D3B /* SCInformationCard.swift */; }; FD2A260A2BAC837000F7B317 /* SCRadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2A26092BAC837000F7B317 /* SCRadioButton.swift */; }; + FD2E8CDF2BD9321E001957F3 /* MyPageMainFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2E8CDE2BD9321E001957F3 /* MyPageMainFeature.swift */; }; + FD2E8CE12BD93238001957F3 /* MyPageMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2E8CE02BD93238001957F3 /* MyPageMainView.swift */; }; + FD2E8CE32BD94064001957F3 /* MyPageTabRouteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2E8CE22BD94064001957F3 /* MyPageTabRouteView.swift */; }; + FD2E8CE52BD940C2001957F3 /* SCLineTabBarFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2E8CE42BD940C2001957F3 /* SCLineTabBarFeature.swift */; }; + FD2E8CE72BD94F37001957F3 /* SCIconButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2E8CE62BD94F37001957F3 /* SCIconButton.swift */; }; + FD2E8CE92BDA6763001957F3 /* FeedGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2E8CE82BDA6763001957F3 /* FeedGridView.swift */; }; + FD3428F52BDC04360021E542 /* Image+.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3428F42BDC04360021E542 /* Image+.swift */; }; + FD3428F72BDC13A60021E542 /* FeedMainFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3428F62BDC13A60021E542 /* FeedMainFeature.swift */; }; + FD3428F92BDC24AE0021E542 /* CGFloat+.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3428F82BDC24AE0021E542 /* CGFloat+.swift */; }; FD38E98C2BD527AB00D3BDB7 /* PolicyFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD38E98B2BD527AB00D3BDB7 /* PolicyFeature.swift */; }; FD38E98E2BD5297E00D3BDB7 /* PolicyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD38E98D2BD5297E00D3BDB7 /* PolicyView.swift */; }; FD38E9902BD530AC00D3BDB7 /* SettingNavigationRowViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD38E98F2BD530AC00D3BDB7 /* SettingNavigationRowViewModifier.swift */; }; @@ -111,6 +120,15 @@ FD208FC82BB9EAEF00DD4D3B /* SCProgressCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCProgressCard.swift; sourceTree = ""; }; FD208FCA2BB9EDCD00DD4D3B /* SCInformationCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCInformationCard.swift; sourceTree = ""; }; FD2A26092BAC837000F7B317 /* SCRadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCRadioButton.swift; sourceTree = ""; }; + FD2E8CDE2BD9321E001957F3 /* MyPageMainFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageMainFeature.swift; sourceTree = ""; }; + FD2E8CE02BD93238001957F3 /* MyPageMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageMainView.swift; sourceTree = ""; }; + FD2E8CE22BD94064001957F3 /* MyPageTabRouteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageTabRouteView.swift; sourceTree = ""; }; + FD2E8CE42BD940C2001957F3 /* SCLineTabBarFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCLineTabBarFeature.swift; sourceTree = ""; }; + FD2E8CE62BD94F37001957F3 /* SCIconButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCIconButton.swift; sourceTree = ""; }; + FD2E8CE82BDA6763001957F3 /* FeedGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedGridView.swift; sourceTree = ""; }; + FD3428F42BDC04360021E542 /* Image+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Image+.swift"; sourceTree = ""; }; + FD3428F62BDC13A60021E542 /* FeedMainFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedMainFeature.swift; sourceTree = ""; }; + FD3428F82BDC24AE0021E542 /* CGFloat+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGFloat+.swift"; sourceTree = ""; }; FD38E98B2BD527AB00D3BDB7 /* PolicyFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PolicyFeature.swift; sourceTree = ""; }; FD38E98D2BD5297E00D3BDB7 /* PolicyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PolicyView.swift; sourceTree = ""; }; FD38E98F2BD530AC00D3BDB7 /* SettingNavigationRowViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingNavigationRowViewModifier.swift; sourceTree = ""; }; @@ -250,10 +268,12 @@ FD0C397B2BA9E47400CC05AA /* Extension */ = { isa = PBXGroup; children = ( + FD3428F42BDC04360021E542 /* Image+.swift */, FD0C39792BA9DA4A00CC05AA /* Font+.swift */, FD0C397C2BA9E48400CC05AA /* Color+.swift */, FD208FBD2BB6F70200DD4D3B /* View+.swift */, FD8FC3B12BC826F600B80B3D /* Date+.swift */, + FD3428F82BDC24AE0021E542 /* CGFloat+.swift */, ); path = Extension; sourceTree = ""; @@ -261,6 +281,7 @@ FD13D4962BAB56A00016F0A0 /* UIComponent */ = { isa = PBXGroup; children = ( + FD3428F32BDC04290021E542 /* Image */, FDFAA42B2BCE05720053CD9F /* Progress */, FD9E070B2BCB4E53000109FD /* Navigation */, FDE9EA202BBDCA01007AA4D7 /* PopUp */, @@ -287,6 +308,13 @@ path = Constant; sourceTree = ""; }; + FD3428F32BDC04290021E542 /* Image */ = { + isa = PBXGroup; + children = ( + ); + path = Image; + sourceTree = ""; + }; FD38E99A2BD5C43000D3BDB7 /* Policy */ = { isa = PBXGroup; children = ( @@ -400,9 +428,11 @@ FD38E9AD2BD84A1500D3BDB7 /* MyPage */ = { isa = PBXGroup; children = ( + FD2E8CDE2BD9321E001957F3 /* MyPageMainFeature.swift */, FD8FC3AF2BC8251700B80B3D /* CalendarFeature.swift */, FD8B63A42BD217EA000500BF /* Setting */, FD38E9AC2BD849F500D3BDB7 /* Profile */, + FD3428F62BDC13A60021E542 /* FeedMainFeature.swift */, ); path = MyPage; sourceTree = ""; @@ -434,6 +464,7 @@ FD4A3B442BBAF0D100D2DFFA /* SCCapsuleButton.swift */, FD4A3B422BBADCD400D2DFFA /* SCRecordButton.swift */, FD9E07102BCC99DE000109FD /* DismissButton.swift */, + FD2E8CE62BD94F37001957F3 /* SCIconButton.swift */, ); path = Button; sourceTree = ""; @@ -521,8 +552,12 @@ FD9E06FD2BC97DE1000109FD /* MyPage */ = { isa = PBXGroup; children = ( + FD2E8CE02BD93238001957F3 /* MyPageMainView.swift */, + FD2E8CE22BD94064001957F3 /* MyPageTabRouteView.swift */, + FD8FC3B32BC834E400B80B3D /* Calendar */, FDFAA4312BCE1C4D0053CD9F /* Profile */, FDFAA4302BCE1C400053CD9F /* Setting */, + FD2E8CE82BDA6763001957F3 /* FeedGridView.swift */, ); path = MyPage; sourceTree = ""; @@ -565,7 +600,6 @@ FDFD3CF62BD85B6800629B8C /* SchoolGroup */, FDFD3CF72BD85B7700629B8C /* Record */, FD9E06FD2BC97DE1000109FD /* MyPage */, - FD8FC3B32BC834E400B80B3D /* Calendar */, FD13D4962BAB56A00016F0A0 /* UIComponent */, ); path = View; @@ -608,6 +642,7 @@ isa = PBXGroup; children = ( FDFD3CF32BD8547900629B8C /* SCTabBarFeature.swift */, + FD2E8CE42BD940C2001957F3 /* SCLineTabBarFeature.swift */, ); path = Component; sourceTree = ""; @@ -794,8 +829,10 @@ FD8FC3BE2BC86EB100B80B3D /* DateComponentsWithID.swift in Sources */, FD8FC3B72BC8426800B80B3D /* CalendarGridItem.swift in Sources */, FD9E06FF2BC97DF6000109FD /* ProfileEditView.swift in Sources */, + FD3428F92BDC24AE0021E542 /* CGFloat+.swift in Sources */, FD2A260A2BAC837000F7B317 /* SCRadioButton.swift in Sources */, FD13D49A2BAB62370016F0A0 /* SCCheckBox.swift in Sources */, + FD2E8CDF2BD9321E001957F3 /* MyPageMainFeature.swift in Sources */, FDFAA43D2BCE72FB0053CD9F /* PrivacyPolicyView.swift in Sources */, FD8B639D2BD200CD000500BF /* AlarmSettingFeature.swift in Sources */, FD9E06FC2BC97D29000109FD /* User.swift in Sources */, @@ -807,11 +844,14 @@ FDFAA4372BCE23180053CD9F /* SettingNavigation.swift in Sources */, FD0C397A2BA9DA4A00CC05AA /* Font+.swift in Sources */, FD208FC92BB9EAEF00DD4D3B /* SCProgressCard.swift in Sources */, + FD3428F72BDC13A60021E542 /* FeedMainFeature.swift in Sources */, FD0C397D2BA9E48400CC05AA /* Color+.swift in Sources */, FDFD3D0B2BD85EBB00629B8C /* AppFeature.swift in Sources */, FD8B63A62BD21802000500BF /* ContactFeature.swift in Sources */, FD8FC3B52BC834F300B80B3D /* CalendarView.swift in Sources */, FD8FC3BB2BC8470700B80B3D /* Month.swift in Sources */, + FD3428F52BDC04360021E542 /* Image+.swift in Sources */, + FD2E8CE92BDA6763001957F3 /* FeedGridView.swift in Sources */, FD8FC3B02BC8251700B80B3D /* CalendarFeature.swift in Sources */, FDFD3CF92BD85B8800629B8C /* HomeMainView.swift in Sources */, FD8B639B2BD1FF1E000500BF /* SettingMainFeature.swift in Sources */, @@ -822,6 +862,7 @@ FD38E99F2BD5C4FD00D3BDB7 /* BlockUserSettingView.swift in Sources */, FDFAA4422BCE7C7D0053CD9F /* UnregisterView.swift in Sources */, FD38E9902BD530AC00D3BDB7 /* SettingNavigationRowViewModifier.swift in Sources */, + FD2E8CE52BD940C2001957F3 /* SCLineTabBarFeature.swift in Sources */, FDFD3CFB2BD85B9F00629B8C /* SchoolGroupMainView.swift in Sources */, FD3C6EBB2BBC79FE00B14FF9 /* SCSearchChip.swift in Sources */, FD38E9992BD5A1A300D3BDB7 /* BlockUserSettingFeature.swift in Sources */, @@ -842,18 +883,21 @@ FD13D4982BAB56AA0016F0A0 /* SCButton.swift in Sources */, FD8B63A12BD2016D000500BF /* ServicePolicyFeature.swift in Sources */, FDFD3D022BD85E2000629B8C /* HomeMainFeature.swift in Sources */, + FD2E8CE32BD94064001957F3 /* MyPageTabRouteView.swift in Sources */, FDFAA42F2BCE0A550053CD9F /* Line.swift in Sources */, FDFAA43B2BCE6F240053CD9F /* Contexts.swift in Sources */, FD9E07012BC97F20000109FD /* ProfileEditFeature.swift in Sources */, FD208FC32BB9B2FC00DD4D3B /* Constants.swift in Sources */, FD9E07112BCC99DE000109FD /* DismissButton.swift in Sources */, FD38E9A92BD83F0B00D3BDB7 /* EmailGuideSheet.swift in Sources */, + FD2E8CE12BD93238001957F3 /* MyPageMainView.swift in Sources */, FDFD3D082BD85E8900629B8C /* SchoolGroupMainFeature.swift in Sources */, FD38E98E2BD5297E00D3BDB7 /* PolicyView.swift in Sources */, FDE62A102BAD959A00DFDA5C /* SCLabel.swift in Sources */, FD3C6EBD2BBC7DE700B14FF9 /* SCTagChip.swift in Sources */, FD8B63A32BD201CC000500BF /* UnregisterFeature.swift in Sources */, FD3C6EB82BBC41A000B14FF9 /* SCTabBar.swift in Sources */, + FD2E8CE72BD94F37001957F3 /* SCIconButton.swift in Sources */, FD208FC52BB9C45700DD4D3B /* SCNumberIcon.swift in Sources */, FD208FCB2BB9EDCD00DD4D3B /* SCInformationCard.swift in Sources */, FD208FBA2BB634AF00DD4D3B /* SCTextField.swift in Sources */, diff --git a/Score/Score/Constant/Constants.swift b/Score/Score/Constant/Constants.swift index ada3feb..515a5cc 100644 --- a/Score/Score/Constant/Constants.swift +++ b/Score/Score/Constant/Constants.swift @@ -29,6 +29,7 @@ enum Constants { case search case user case home + case setting // Member Icons case dustMood = "dust.mood" @@ -93,6 +94,8 @@ enum Constants { enum Layout: CGFloat { case horizontal = 16 + case navigationBarHeight = 56 + case iconSize = 24 } } diff --git a/Score/Score/Extension/CGFloat+.swift b/Score/Score/Extension/CGFloat+.swift new file mode 100644 index 0000000..beb4899 --- /dev/null +++ b/Score/Score/Extension/CGFloat+.swift @@ -0,0 +1,22 @@ +// +// CGFloat+.swift +// Score +// +// Created by sole on 4/27/24. +// + +import UIKit + +//MARK: - CGFloat + +extension CGFloat { + //MARK: - device size + static let deviceWidth = UIScreen.main.bounds.width + static let deviceHeight = UIScreen.main.bounds.height + + //MARK: - calendarItemSize + static var calendarItemSize: CGFloat { + let paddingHorizontal: CGFloat = Constants.Layout.horizontal.rawValue * 2 + return (.deviceWidth - paddingHorizontal) / 7 + } +} diff --git a/Score/Score/Extension/Image+.swift b/Score/Score/Extension/Image+.swift new file mode 100644 index 0000000..60bf945 --- /dev/null +++ b/Score/Score/Extension/Image+.swift @@ -0,0 +1,59 @@ +// +// Image+.swift +// Score +// +// Created by sole on 4/27/24. +// + +import SwiftUI + +//MARK: - Image+imagePlaceHolder + +extension Image { + + //MARK: - imagePlaceHolder + + /// image 플레이스 홀더가 필요한 사진의 경우에 사용합니다. + /// 플레이스 홀더를 원형의 size*size로 생성합니다. + @ViewBuilder + func imagePlaceHolder(size: CGFloat) -> some View { + self + .resizable() + .frame(width: 130, + height: 130) + .clipShape(Circle()) + .background { + Circle() + .frame(width: size, + height: size) + .foregroundStyle( + Color.brandColor(color: .gray2) + ) + } + .frame(width: size, + height: size) + } +} + +extension Image? { + + @ViewBuilder + func imagePlaceHolder(size: CGFloat) -> some View { + let image: Image = self ?? Image("") + image + .resizable() + .frame(width: 130, + height: 130) + .clipShape(Circle()) + .background { + Circle() + .frame(width: size, + height: size) + .foregroundStyle( + Color.brandColor(color: .gray2) + ) + } + .frame(width: size, + height: size) + } +} diff --git a/Score/Score/Extension/View+.swift b/Score/Score/Extension/View+.swift index 8c99365..39831a3 100644 --- a/Score/Score/Extension/View+.swift +++ b/Score/Score/Extension/View+.swift @@ -39,8 +39,15 @@ extension View { // 양쪽에 padding이 들어가므로 2배를 곱해줍니다. let paddingHorizontal: CGFloat = Constants.Layout.horizontal.rawValue * 2 frame( - width: (UIScreen.main.bounds.width - paddingHorizontal) / 7, - height: (UIScreen.main.bounds.width - paddingHorizontal) / 7 + width: (.deviceWidth - paddingHorizontal) / 7, + height: (.deviceWidth - paddingHorizontal) / 7 ) } + + @ViewBuilder + func rectFrame(size: CGFloat) -> some View { + self + .frame(width: size, + height: size) + } } diff --git a/Score/Score/Model/User.swift b/Score/Score/Model/User.swift index c22be1e..439e070 100644 --- a/Score/Score/Model/User.swift +++ b/Score/Score/Model/User.swift @@ -14,7 +14,7 @@ struct User: Equatable, // identifier let nickName: String let profileImageName: String - let sex: Sex + let gender: Gender let height: Int let weight: Int let schoolName: String @@ -25,16 +25,20 @@ struct User: Equatable, static let defaultModel: User = .init(nickName: "왕감자", profileImageName: "", - sex: .female, + gender: .female, height: 160, weight: 50, schoolName: "감자대학교", grade: 1) } -//MARK: - Sex +//MARK: - Gender -enum Sex: String { +enum Gender: String, CaseIterable { + static let sex: [Gender] = [.male, + .female] + case male = "남" case female = "여" + case ect } diff --git a/Score/Score/Reducer/Component/SCLineTabBarFeature.swift b/Score/Score/Reducer/Component/SCLineTabBarFeature.swift new file mode 100644 index 0000000..ac292f9 --- /dev/null +++ b/Score/Score/Reducer/Component/SCLineTabBarFeature.swift @@ -0,0 +1,36 @@ +// +// SCLineTabBarFeature.swift +// Score +// +// Created by sole on 4/24/24. +// + +import ComposableArchitecture + +@Reducer +struct SCLineTabBarFeature { + struct State: Equatable { + var tabItems: [SCLineTabItem] = [] + @BindingState var selectedTab: T + } + + enum Action: BindableAction { + case binding(BindingAction) + case tabBarButtonTapped(SCLineTabItem) + } + + var body: some ReducerOf { + BindingReducer() + + Reduce { state, action in + switch action { + case .binding: + return .none + + case .tabBarButtonTapped(let tabItem): + state.selectedTab = tabItem.tab + return .none + } + } + } +} diff --git a/Score/Score/Reducer/Component/SCTabBarFeature.swift b/Score/Score/Reducer/Component/SCTabBarFeature.swift index 0b481c4..4370a55 100644 --- a/Score/Score/Reducer/Component/SCTabBarFeature.swift +++ b/Score/Score/Reducer/Component/SCTabBarFeature.swift @@ -35,9 +35,6 @@ struct SCTabBarFeature { Reduce { state, action in switch action { - case .binding: - return .none - case .viewAppearing: return .none @@ -54,11 +51,13 @@ struct SCTabBarFeature { } return .none - case .destination: + case .binding, + .destination: return .none } } - .ifLet(\.$destination, action: \.destination) { + .ifLet(\.$destination, + action: \.destination) { Destination() } } diff --git a/Score/Score/Reducer/MyPage/CalendarFeature.swift b/Score/Score/Reducer/MyPage/CalendarFeature.swift index 2ef05c9..ff19728 100644 --- a/Score/Score/Reducer/MyPage/CalendarFeature.swift +++ b/Score/Score/Reducer/MyPage/CalendarFeature.swift @@ -29,10 +29,10 @@ struct CalendarFeature: Reducer { private let maxNumberOfDayInWeek: Int = 7 struct State: Equatable { - var appearedYear: Int - var appearedMonth: Int - var appearedDay: Int - var appearedDateComponentsMatrix: [[DateComponentsWithID]] + var appearedYear: Int = Date.nowDate().year + var appearedMonth: Int = Date.nowDate().month + var appearedDay: Int = Date.nowDate().day + var appearedDateComponentsMatrix: [[DateComponentsWithID]] = [] var selectedDateComponents: DateComponents? } @@ -49,11 +49,12 @@ struct CalendarFeature: Reducer { Reduce { state, action in switch action { case .viewAppearing: - // 현재 날짜로 세팅합니다. - let nowDate = Date.nowDate() - state.appearedYear = nowDate.year - state.appearedMonth = nowDate.month - state.appearedDay = nowDate.day +// // 현재 날짜로 세팅합니다. + // 탭바로 appearing 시 이전 상황이 저장되지 않아서 삭제합니다. +// let nowDate = Date.nowDate() +// state.appearedYear = nowDate.year +// state.appearedMonth = nowDate.month +// state.appearedDay = nowDate.day return .send(.calendarUpdating) case .incrementMonthButtonTapped: diff --git a/Score/Score/Reducer/MyPage/FeedMainFeature.swift b/Score/Score/Reducer/MyPage/FeedMainFeature.swift new file mode 100644 index 0000000..a8701b4 --- /dev/null +++ b/Score/Score/Reducer/MyPage/FeedMainFeature.swift @@ -0,0 +1,33 @@ +// +// FeedMainFeature.swift +// Score +// +// Created by sole on 4/27/24. +// + +import ComposableArchitecture + +@Reducer +struct FeedMainFeature { + struct State: Equatable { + + } + + enum Action { + case viewAppearing + case feedImageButtonTapped + } + + var body: some ReducerOf { + Reduce { state, action in + switch action { + case .viewAppearing: + return .none + + case .feedImageButtonTapped: + return .none + } + } + } + +} diff --git a/Score/Score/Reducer/MyPage/MyPageMainFeature.swift b/Score/Score/Reducer/MyPage/MyPageMainFeature.swift new file mode 100644 index 0000000..1061b17 --- /dev/null +++ b/Score/Score/Reducer/MyPage/MyPageMainFeature.swift @@ -0,0 +1,135 @@ +// +// MyPageMainFeature.swift +// Score +// +// Created by sole on 4/24/24. +// + +import ComposableArchitecture + +//MARK: - MyPageMainFeature + +@Reducer +struct MyPageMainFeature { + @Dependency(\.dismiss) var dismiss + typealias Tab = MyPageLineTab + + //MARK: - MyPageLineTab + + enum MyPageLineTab: SCLineTabProtocol { + case feed + case calendar + } + + //MARK: - State + + struct State: Equatable { + @PresentationState var destination: Destination.State? + + var lineTabBar: SCLineTabBarFeature.State = .init( + tabItems: [.init(title: "피드", tab: .feed), + .init(title: "캘린더", tab: .calendar)], + selectedTab: .feed) + + var calendar: CalendarFeature.State = .init() + var feed: FeedMainFeature.State = .init() + } + + //MARK: - State + + enum Action { + case viewApearing + case tasking + + case dismissButtonTapped + case settingButtonTapped + case profileEditButtonTapped + case myFriendButtonTapped + + case destination(PresentationAction) + case lineTabBar(SCLineTabBarFeature.Action) + case calendar(CalendarFeature.Action) + case feed(FeedMainFeature.Action) + } + + var body: some ReducerOf { + Reduce { state, action in + switch action { + case .viewApearing: + return .none + + case .tasking: + return .none + + case .dismissButtonTapped: + return .run { send in + await self.dismiss() + } + + case .settingButtonTapped: + state.destination = .setting(.init()) + return .none + + case .profileEditButtonTapped: + state.destination = .profileEdit(.init()) + return .none + + case .myFriendButtonTapped: + return .none + + case .destination, + .lineTabBar, + .calendar, + .feed: + return .none + } + } + .ifLet(\.$destination, + action: \.destination) { + Destination() + } + + Scope(state: \.lineTabBar, + action: \.lineTabBar) { + SCLineTabBarFeature() + } + + Scope(state: \.calendar, + action: \.calendar) { + CalendarFeature() + } + + Scope(state: \.feed, + action: \.feed) { + FeedMainFeature() + } + } + + //MARK: - Destination + + @Reducer + struct Destination { + enum State: Equatable { + case setting(SettingMainFeature.State) + case profileEdit(ProfileEditFeature.State) + } + + enum Action { + case setting(SettingMainFeature.Action) + case profileEdit(ProfileEditFeature.Action) + } + + var body: some ReducerOf { + Scope(state: \.setting, + action: \.setting) { + SettingMainFeature() + } + + Scope(state: \.profileEdit, + action: \.profileEdit) { + ProfileEditFeature() + } + + } + } +} diff --git a/Score/Score/Reducer/MyPage/Profile/PhotoPickerFeature.swift b/Score/Score/Reducer/MyPage/Profile/PhotoPickerFeature.swift index 4412736..f67d1a3 100644 --- a/Score/Score/Reducer/MyPage/Profile/PhotoPickerFeature.swift +++ b/Score/Score/Reducer/MyPage/Profile/PhotoPickerFeature.swift @@ -27,8 +27,12 @@ struct PhotoPickerFeature { Reduce { state, action in switch action { + case .binding(\.$photoItem): + return .send(.photoSelectChanging(state.photoItem)) + case .binding(_): return .none + case .photoSelectChanging(let photosPickerItem): guard let photosPickerItem else { @@ -38,6 +42,7 @@ struct PhotoPickerFeature { let image = try await photosPickerItem.loadTransferable(type: Image.self) await send(.imageUpdating(image)) } + case .imageUpdating(let image): guard let image else { return .none } diff --git a/Score/Score/Reducer/MyPage/Profile/ProfileEditFeature.swift b/Score/Score/Reducer/MyPage/Profile/ProfileEditFeature.swift index 95c43f1..586151a 100644 --- a/Score/Score/Reducer/MyPage/Profile/ProfileEditFeature.swift +++ b/Score/Score/Reducer/MyPage/Profile/ProfileEditFeature.swift @@ -15,30 +15,36 @@ struct ProfileEditFeature { struct State: Equatable { var displayedUser: User? - + var displayedProfileImage: Image? + var photoPicker: PhotoPickerFeature.State = .init() - @BindingState var displayedNickName: String - @BindingState var displayedHeight: String - @BindingState var displayedWeight: String - @BindingState var displayedSex: Sex - @BindingState var displayedSchool: String - @BindingState var displayedGrade: Int + // - FIXME: User profile State -> User로 통합할지 서버 데이터 확인 후 결정 - @BindingState var isPresentingEditGradeSheet: Bool - @BindingState var isPresentingEditSchoolSheet: Bool + @BindingState var displayedNickName: String = "" + @BindingState var displayedHeight: String = "" + @BindingState var displayedWeight: String = "" + @BindingState var displayedSex: Gender = .ect + @BindingState var displayedSchool: String = "" + @BindingState var displayedGrade: Int = 0 + + // isPresented Sheet State + @BindingState var isPresentingEditWorkOutTime: Bool = false + @BindingState var isPresentingEditGradeSheet: Bool = false + @BindingState var isPresentingEditSchoolSheet: Bool = false } enum Action: BindableAction { case binding(BindingAction) + case workOutTimeButtonTapped case schoolEditButtonTapped case gradeEditButtonTapped - case sexSelectButtonTapped(Sex) + case sexSelectButtonTapped(Gender) case editDoneButtonTapped case dismissButtonTapped - case photoSelectChanging(PhotosPickerItem?) + case photoPicker(PhotoPickerFeature.Action) case imageUpdating(Image?) } @@ -47,49 +53,57 @@ struct ProfileEditFeature { Reduce { state, action in switch action { + case .workOutTimeButtonTapped: + state.isPresentingEditWorkOutTime = true + return .none + case .schoolEditButtonTapped: + state.isPresentingEditSchoolSheet = true return .none case .gradeEditButtonTapped: - // presenting sheet state.isPresentingEditGradeSheet = true return .none case .sexSelectButtonTapped(let sex): - state.displayedSex = sex + // - FIXME: 둘 다 선택하지 않을 경우 기타로 할당합니다. (기획 확정 필요) + if state.displayedSex == sex { + state.displayedSex = .ect + } else { + state.displayedSex = sex + } + return .none + + case .imageUpdating(let image): + guard let image + else { return .none } + state.displayedProfileImage = image return .none + case .photoPicker(.imageUpdating(let image)): + return .send(.imageUpdating(image)) + case .editDoneButtonTapped: // to update the remote database return .none + case .dismissButtonTapped: return .run { send in await self.dismiss() } - case .photoSelectChanging(let photosPickerItem): - guard let photosPickerItem - else { - return .send(.imageUpdating(nil)) - } - - return .run { send in - let image = try await photosPickerItem.loadTransferable(type: Image.self) - await send(.imageUpdating(image)) - } catch: { error, send in - await send(.imageUpdating(nil)) - } - - case .imageUpdating(let image): - guard let image - else { return .none } - state.displayedProfileImage = image - return .none - - case .binding(_): + case .binding, + .photoPicker: return .none } } + + //MARK: - Scope + + Scope(state: \.photoPicker, + action: \.photoPicker) { + PhotoPickerFeature() + } } } diff --git a/Score/Score/Reducer/MyPage/Setting/SettingMainFeature.swift b/Score/Score/Reducer/MyPage/Setting/SettingMainFeature.swift index c4d3fc3..5814c1b 100644 --- a/Score/Score/Reducer/MyPage/Setting/SettingMainFeature.swift +++ b/Score/Score/Reducer/MyPage/Setting/SettingMainFeature.swift @@ -13,7 +13,7 @@ struct SettingMainFeature { struct State: Equatable { @PresentationState var destination: Destination.State? - @BindingState var isPresentedSignOutDialog: Bool = false + @BindingState var isPresentingSignOutDialogDialog: Bool = false } enum Action: BindableAction { @@ -61,11 +61,11 @@ struct SettingMainFeature { return .none case .signOutButtonTapped: - state.isPresentedSignOutDialog = true + state.isPresentingSignOutDialogDialog = true return .none case .dialogDismissButtonTapped: - state.isPresentedSignOutDialog = false + state.isPresentingSignOutDialogDialog = false return .none case .dismissButtonTapped: diff --git a/Score/Score/View/Calendar/CalendarView.swift b/Score/Score/View/Calendar/CalendarView.swift deleted file mode 100644 index b571813..0000000 --- a/Score/Score/View/Calendar/CalendarView.swift +++ /dev/null @@ -1,136 +0,0 @@ -// -// CalendarView.swift -// Score -// -// Created by sole on 4/12/24. -// - -import ComposableArchitecture -import SwiftUI - -//MARK: - CalendarView - -struct CalendarView: View { - let store: StoreOf - - var body: some View { - WithViewStore(store, - observe: { $0 }) { viewStore in - VStack(spacing: 9) { - // Calendar Header Section - // year, month, buttons to control appearedMonth - calendarHeader(viewStore: viewStore) - - // WeekOfDay Label Section - dayOfWeekLabel(viewStore: viewStore) - - // Calendar Section - calendarGrid(viewStore: viewStore) - } - .layout() - .onAppear { - viewStore.send(.viewAppearing) - } - } - } - - //MARK: - calendarHeader - - @ViewBuilder - func calendarHeader(viewStore: - ViewStore - ) -> some View { - HStack { - Group { - Text(String( - format: "%4d", - viewStore.appearedYear)) - - Text((Month(rawValue: viewStore.appearedMonth) ?? - .october).capitalizedString()) - } - .pretendard(weight: .black, - size: .m) - - Spacer() - - /// FIXME: Asset으로 변경 - Button { - viewStore.send(.decrementMonthButtonTapped) - } label: { - Image(systemName: "chevron.left") - .foregroundStyle(Color.brandColor( - color: .sub1) - ) - .frame(width: 34, - height: 34) - } - - Button { - viewStore.send(.incrementMonthButtonTapped) - } label: { - Image(systemName: "chevron.right") - .foregroundStyle(Color.brandColor( - color: .sub1) - ) - .frame(width: 34, - height: 34) - } - } - } - - //MARK: - dayOfWeekLabel - - @ViewBuilder - func dayOfWeekLabel(viewStore: - ViewStore - ) -> some View { - HStack(spacing: 0) { - ForEach(DayOfWeek.allCases, id: \.self) { dayOfWeek in - Text(dayOfWeek.stringValue()) - .pretendard(.body3) - } - .layoutOfCalendarItem() - } - } - - //MARK: - calendarGrid - - @ViewBuilder - func calendarGrid( - viewStore: ViewStore - ) -> some View { - VStack(spacing: 0) { - ForEach(viewStore.appearedDateComponentsMatrix, - id: \.self) { dateComponentsArray in - HStack(spacing: 0) { - ForEach(dateComponentsArray, - id: \.self) { dateComponentsWithID in - CalendarGridItem(store: viewStore, - dateComponents: dateComponentsWithID.dateComponents) { - viewStore.send(.dayComponentButtonTapped(dateComponentsWithID.dateComponents)) - } - } - } - } - } - } - -} - -//MARK: - Preview - -#Preview { - CalendarView(store: .init(initialState: CalendarFeature.State( - appearedYear: 0, - appearedMonth: 0, - appearedDay: 0, - appearedDateComponentsMatrix: [], - selectedDateComponents: nil), - reducer: { - CalendarFeature().body - })) -} diff --git a/Score/Score/View/Calendar/CalendarGridItem.swift b/Score/Score/View/MyPage/Calendar/CalendarGridItem.swift similarity index 70% rename from Score/Score/View/Calendar/CalendarGridItem.swift rename to Score/Score/View/MyPage/Calendar/CalendarGridItem.swift index a19fcb1..6feed50 100644 --- a/Score/Score/View/Calendar/CalendarGridItem.swift +++ b/Score/Score/View/MyPage/Calendar/CalendarGridItem.swift @@ -10,17 +10,19 @@ import SwiftUI //MARK: - CalendarGridItem -// - FIXME: CalendarView에서 무조건 store를 받아와야 하는데, 효율적인지? /// - Parameters: /// - store: ViewStore CalendarView에서 받아옵니다. /// - dateComponents: DateComponents를 정의합니다. /// - action: 사용자가 컴포넌트를 Tap했을 때 수행할 동작을 정의합니다. struct CalendarGridItem: View { - let store: ViewStore + let store: StoreOf + @ObservedObject var viewStore: ViewStoreOf + let dateComponents: DateComponents? let action: () -> (Void) + //MARK: - style + private var style: SCNumberIconStyle { guard let dateComponents else { @@ -35,7 +37,7 @@ struct CalendarGridItem: View { } // - FIXME: 오늘 날짜가 black인지? 디자인 확인 필요 - if store.state.selectedDateComponents == dateComponents { + if viewStore.state.selectedDateComponents == dateComponents { return .gray } @@ -44,11 +46,24 @@ struct CalendarGridItem: View { return .plain } + //MARK: - init + + init(store: StoreOf, + dateComponents: DateComponents? = nil, + action: @escaping () -> Void) { + self.store = store + self.dateComponents = dateComponents + self.action = action + self.viewStore = ViewStore(store, + observe: { $0 }) + } + + //MARK: - body + var body: some View { Button(action: action) { SCNumberIcon(style: style, - number: dateComponents?.day - ?? 0) + number: dateComponents?.day ?? 0) /// 양쪽 layout padding 효과 .layoutOfCalendarItem() .background { @@ -71,7 +86,8 @@ struct CalendarGridItem: View { //MARK: - Preview #Preview { - CalendarGridItem(store: .init(.init(initialState: CalendarFeature.State(appearedYear: 0, appearedMonth: 0, appearedDay: 0, appearedDateComponentsMatrix: []), reducer: {CalendarFeature().body}), observe: {$0}), dateComponents: nil) { + CalendarGridItem(store: .init(initialState: .init(), + reducer: { CalendarFeature() })) { } } diff --git a/Score/Score/View/MyPage/Calendar/CalendarView.swift b/Score/Score/View/MyPage/Calendar/CalendarView.swift new file mode 100644 index 0000000..7718773 --- /dev/null +++ b/Score/Score/View/MyPage/Calendar/CalendarView.swift @@ -0,0 +1,132 @@ +// +// CalendarView.swift +// Score +// +// Created by sole on 4/12/24. +// + +import ComposableArchitecture +import SwiftUI + +//MARK: - CalendarView + +struct CalendarView: View { + let store: StoreOf + @ObservedObject var viewStore: ViewStoreOf + + init(store: StoreOf) { + self.store = store + self.viewStore = ViewStore(store, + observe: { $0 }) + } + + var body: some View { + VStack(spacing: 9) { + // Calendar Header Section + // year, month, buttons to control appearedMonth + calendarHeader() + + // WeekOfDay Label Section + dayOfWeekLabel() + + // Calendar Section + /// frame: calendarGrid의 높이에 따라 View가 움직이는 현상을 방지합니다. + calendarGrid() + .frame(height: .calendarItemSize * 6) + } + .layout() + .onAppear { + store.send(.viewAppearing) + } + } + + //MARK: - calendarHeader + + @ViewBuilder + func calendarHeader() -> some View { + HStack { + Group { + Text(String( + format: "%4d", + viewStore.appearedYear)) + + Text((Month(rawValue: viewStore.appearedMonth) ?? + .october).capitalizedString()) + } + .pretendard(weight: .black, + size: .m) + + Spacer() + + /// FIXME: Asset으로 변경 + Button { + store.send(.decrementMonthButtonTapped) + } label: { + Image(systemName: "chevron.left") + .foregroundStyle( + Color.brandColor( + color: .sub1 + ) + ) + .frame(width: 34, + height: 34) + } + + Button { + store.send(.incrementMonthButtonTapped) + } label: { + Image(systemName: "chevron.right") + .foregroundStyle( + Color.brandColor( + color: .sub1 + ) + ) + .frame(width: 34, + height: 34) + } + } + } + + //MARK: - dayOfWeekLabel + + @ViewBuilder + func dayOfWeekLabel() -> some View { + HStack(spacing: 0) { + ForEach(DayOfWeek.allCases, + id: \.self) { dayOfWeek in + Text(dayOfWeek.stringValue()) + .pretendard(.body3) + } + .layoutOfCalendarItem() + } + } + + //MARK: - calendarGrid + + @ViewBuilder + func calendarGrid() -> some View { + VStack(spacing: 0) { + ForEach(viewStore.appearedDateComponentsMatrix, + id: \.self) { dateComponentsArray in + HStack(spacing: 0) { + ForEach(dateComponentsArray, + id: \.self) { dateComponentsWithID in + CalendarGridItem( + store: store, + dateComponents: dateComponentsWithID.dateComponents + ) { + store.send(.dayComponentButtonTapped(dateComponentsWithID.dateComponents)) + } + } + } + } + } + } +} + +//MARK: - Preview + +#Preview { + CalendarView(store: .init(initialState: .init(), + reducer: { CalendarFeature() })) +} diff --git a/Score/Score/View/MyPage/FeedGridView.swift b/Score/Score/View/MyPage/FeedGridView.swift new file mode 100644 index 0000000..f80a266 --- /dev/null +++ b/Score/Score/View/MyPage/FeedGridView.swift @@ -0,0 +1,41 @@ +// +// FeedGridView.swift +// Score +// +// Created by sole on 4/25/24. +// + +import ComposableArchitecture +import SwiftUI + +struct FeedGridView: View { + private let columnLayout: [GridItem] = [ + .init(.flexible()), + .init(.flexible()), + .init(.flexible()) + ] + + let store: StoreOf + + var body: some View { + ScrollView { + LazyVGrid(columns: columnLayout, + spacing: 2) { + // - FIXME: Mock up view + ForEach(0..<299) { num in + Image(systemName: "photo.fill.on.rectangle.fill") + .foregroundStyle(.black) + .rectFrame(size: .deviceWidth / 3) + .background( + Color.brandColor(color: .gray2) + ) + } + } + } + } +} + +#Preview { + FeedGridView(store: .init(initialState: .init(), + reducer: { FeedMainFeature() })) +} diff --git a/Score/Score/View/MyPage/MyPageMainView.swift b/Score/Score/View/MyPage/MyPageMainView.swift new file mode 100644 index 0000000..32b2a27 --- /dev/null +++ b/Score/Score/View/MyPage/MyPageMainView.swift @@ -0,0 +1,166 @@ +// +// MyPageMainView.swift +// Score +// +// Created by sole on 4/24/24. +// + +import ComposableArchitecture +import SwiftUI + +struct MyPageMainView: View { + private let layoutConstants = Constants.Layout.self + private let imageNames = Constants.ImageName.self + let store: StoreOf + + @State var isDisableChildScrollView: Bool = false + + // - FIXME: 디자인 논의 확정 후 변경 + /// ScrollView 안에 ScrollView를 넣었을 때 안쪽에 있는 ScrollView가 화면을 모두 차지하는 경우, 위쪽의 View로 넘어가지 못하는 현상 발생 + var body: some View { + ScrollViewReader { proxy in + ScrollView { + VStack(spacing: 40) { + profileSectionBuilder() + .overlay(alignment: .bottom) { + buttonSectionBuilder() + .offset(y: 180) + } + /// 상수로 따로 빼줘야 함. + .padding(.bottom, 180) + + MyPageTabRouteView(store: store) + .frame(height: 500) + } + } + .scrollIndicators(.hidden) + .scNavigationBar(style: .vertical) { + // - FIXME: 논의 후 반영 + DismissButton(style: .chevron) { + store.send(.dismissButtonTapped) + } + + Text("마이페이지") + + Spacer() + + SCIconButton(imageName: .setting, + color: .white) { + // navigate to setting View + store.send(.settingButtonTapped) + } + } + .navigationDestination( + store: store.scope( + state: \.$destination.setting, + action: \.destination.setting + ) + ) { store in + SettingMainView(store: store) + } + .navigationDestination( + store: store.scope( + state: \.$destination.profileEdit, + action: \.destination.profileEdit + ) + ) { store in + ProfileEditView(store: store) + } + } + } + + + //MARK: - profileSectionBuilder + + /// 프로필 이미지를 수정할 수 있는 부분 뷰입니다. + @ViewBuilder + func profileSectionBuilder() -> some View { + HStack { + Image("") + .imagePlaceHolder(size: 76) + + VStack(alignment: .leading, + spacing: 12) { + Text("닉네임") + .pretendard(weight: .bold, + size: .xxl) + .foregroundStyle(.white) + + Text("감자대학교 1학년") + .pretendard(.body2) + .foregroundStyle(.white) + } + + Spacer() + } + .layout() + .frame(height: 188) + .background { + UnevenRoundedRectangle( + bottomLeadingRadius: 45, + bottomTrailingRadius: 45 + ) + .foregroundStyle(Color.brandColor(color: .main)) + } + } + + //MARK: - buttonSectionBuilder + + @ViewBuilder + func buttonSectionBuilder() -> some View { + VStack(alignment: .leading, + spacing: 16) { + SCProgressCard(image: nil, + level: 1000, + restPoint: 123, + progress: 0.5) + + HStack { + Text("운동 알림 시간") + .pretendard(.body3) + + Spacer() + + Text("매일 오전 10:00") + .pretendard(.body2) + } + .foregroundStyle(Color.brandColor(color: .text1)) + .padding(.vertical, 13) + .padding(.horizontal, 15) + .background(Color.brandColor(color: .gray2), + in: RoundedRectangle(cornerRadius: 10)) + + HStack { + SCButton(style: .gray) { + store.send(.profileEditButtonTapped) + } label: { + SCLabel(style: .button, + imageName: .pencil, + title: "프로필 수정") + .frame(maxWidth: .infinity) + } + + SCButton(style: .gray) { + store.send(.myFriendButtonTapped) + } label: { + SCLabel(style: .button, + imageName: .groupUsers, + title: "내 친구") + .frame(maxWidth: .infinity) + } + + } + + } + .layout() + } +} + +//MARK: - Preview + +#Preview { + NavigationStack { + MyPageMainView(store: .init(initialState: .init(), + reducer: { MyPageMainFeature() })) + } +} diff --git a/Score/Score/View/MyPage/MyPageTabRouteView.swift b/Score/Score/View/MyPage/MyPageTabRouteView.swift new file mode 100644 index 0000000..db4cbb2 --- /dev/null +++ b/Score/Score/View/MyPage/MyPageTabRouteView.swift @@ -0,0 +1,62 @@ +// +// MyPageTabRouteView.swift +// Score +// +// Created by sole on 4/24/24. +// + +import ComposableArchitecture +import SwiftUI + +//MARK: - MyPageTabRouteView + +struct MyPageTabRouteView: View { + typealias Tab = MyPageMainFeature.MyPageLineTab + let store: StoreOf + @ObservedObject var viewStore: ViewStoreOf> + + //MARK: - init + + init(store: StoreOf) { + self.store = store + self.viewStore = ViewStore( + store.scope(state: \.lineTabBar, + action: \.lineTabBar), + observe: { $0 }) + } + + //MARK: - body + + var body: some View { + TabView(selection: viewStore.$selectedTab + ) { + FeedGridView(store: store.scope(state: \.feed, + action: \.feed) + ) + .tag(Tab.feed) + + VStack { + CalendarView( + store: store.scope(state: \.calendar, + action: \.calendar) + ) + Spacer() + } + .padding(.top, 30) + .tag(Tab.calendar) + } + .scLineTabBar( + store: store.scope(state: \.lineTabBar, + action: \.lineTabBar)) + .tabViewStyle( + .page(indexDisplayMode: .never) + ) + } +} + +//MARK: - Preview + +#Preview { + MyPageTabRouteView(store: .init(initialState: .init(), + reducer: { MyPageMainFeature() })) +} diff --git a/Score/Score/View/MyPage/Profile/PhotoPickerView.swift b/Score/Score/View/MyPage/Profile/PhotoPickerView.swift index 60b4e6d..3b702c0 100644 --- a/Score/Score/View/MyPage/Profile/PhotoPickerView.swift +++ b/Score/Score/View/MyPage/Profile/PhotoPickerView.swift @@ -12,83 +12,81 @@ import SwiftUI //MARK: - PhotoPickerView struct PhotoPickerView: View { - @ObservedObject var viewStore: ViewStoreOf + let store: StoreOf + @ObservedObject var viewStore: ViewStoreOf - @State var selectedPhotoItem: PhotosPickerItem? + init(store: StoreOf, + @ViewBuilder content: @escaping () -> Content) { + self.store = store + self.content = content + self.viewStore = ViewStore(store, + observe: { $0 }) + } + @ViewBuilder let content: () -> Content var body: some View { - PhotosPicker(selection: $selectedPhotoItem) { + PhotosPicker(selection: viewStore.$photoItem) { content() } - /// iOS 17.0을 기점으로 분기 처리합니다. - /// iOS 17.0 이상 : scOnChangeOver17 - /// iOS 17.0 미만 : scOnChangeUnder17 로 onChange가 적용됩니다. - .scOnChangeOver17(of: selectedPhotoItem) { oldValue, newValue in - guard oldValue != newValue - else { return } - viewStore.send(.photoSelectChanging(newValue)) - } - .scOnChangeUnder17(of: selectedPhotoItem) { newValue in - viewStore.send(.photoSelectChanging(newValue)) - } +// /// iOS 17.0을 기점으로 분기 처리합니다. +// /// iOS 17.0 이상 : scOnChangeOver17 +// /// iOS 17.0 미만 : scOnChangeUnder17 로 onChange가 적용됩니다. +// .scOnChangeOver17(of: viewStore.$selectedPhotoItem) { oldValue, newValue in +// guard oldValue != newValue +// else { return } +// viewStore.send(.photoSelectChanging(newValue)) +// } +// .scOnChangeUnder17(of: viewStore.$selectedPhotoItem) { newValue in +// viewStore.send(.photoSelectChanging(newValue)) +// } } } //MARK: - View+onChange -extension View { - //MARK: - scOnChangeOver17 - - /// iOS 17.0 이상에서 사용할 수 있습니다. - @ViewBuilder - func scOnChangeOver17( - of: V, - _ action: @escaping (V, V) -> (Void) - ) -> some View { - if #available(iOS 17.0, *) { - self.onChange(of: of, action) - } else { - self - } - } - - //MARK: - scOnChangeUnder17 - - /// iOS 17.0 이상에서 사용할 수 없습니다. - /// iOS 17.0 미만을 지원합니다. - @ViewBuilder - func scOnChangeUnder17 ( - of: V, - perform: @escaping (V) -> (Void) - ) -> some View { - if #unavailable(iOS 17.0) { - self.onChange(of: of, perform: perform) - } else { - self - } - } -} +//extension View { +// //MARK: - scOnChangeOver17 +// +// /// iOS 17.0 이상에서 사용할 수 있습니다. +// @ViewBuilder +// func scOnChangeOver17( +// of: V, +// _ action: @escaping (V, V) -> (Void) +// ) -> some View { +// if #available(iOS 17.0, *) { +// self.onChange(of: of, action) +// } else { +// self +// } +// } +// +// //MARK: - scOnChangeUnder17 +// +// /// iOS 17.0 이상에서 사용할 수 없습니다. +// /// iOS 17.0 미만을 지원합니다. +// @ViewBuilder +// func scOnChangeUnder17 ( +// of: V, +// perform: @escaping (V) -> (Void) +// ) -> some View { +// if #unavailable(iOS 17.0) { +// self.onChange(of: of, perform: perform) +// } else { +// self +// } +// } +//} //MARK: - Preview #Preview { - PhotoPickerView( - viewStore: .init( - .init( - initialState: ProfileEditFeature.State( - displayedNickName: "", - displayedHeight: "", - displayedWeight: "", - displayedSex: .female, - displayedSchool: "스코어대학교", - displayedGrade: 1, - isPresentingEditGradeSheet: false, - isPresentingEditSchoolSheet: false), - reducer: { ProfileEditFeature() }), - observe: { $0 } - ) - ) { - Text("photo picker active") + PhotoPickerView(store: .init(initialState: .init(), + reducer: { PhotoPickerFeature() } + )) { + VStack { + Text("123") + Text("photo picker active") + } } } diff --git a/Score/Score/View/MyPage/Profile/ProfileEditView.swift b/Score/Score/View/MyPage/Profile/ProfileEditView.swift index f630f7b..2fc26c3 100644 --- a/Score/Score/View/MyPage/Profile/ProfileEditView.swift +++ b/Score/Score/View/MyPage/Profile/ProfileEditView.swift @@ -16,12 +16,16 @@ struct ProfileEditView: View { let constant = Contexts.MyPage.self + //MARK: - init + init(store: StoreOf) { self.store = store self.viewStore = ViewStore(store, observe: { $0 }) } + //MARK: - body + var body: some View { ScrollView { VStack(alignment: .leading, @@ -56,6 +60,15 @@ struct ProfileEditView: View { Text("마이페이지") } + .sheet(isPresented: viewStore.$isPresentingEditWorkOutTime) { + Text("운동 시간 설정 Sheet") + } + .sheet(isPresented: viewStore.$isPresentingEditSchoolSheet) { + Text("학교 설정 Sheet") + } + .sheet(isPresented: viewStore.$isPresentingEditGradeSheet) { + Text("학년 설정 Sheet") + } } //MARK: - profileImageSectionBuilder @@ -63,45 +76,22 @@ struct ProfileEditView: View { /// 프로필 이미지를 수정할 수 있는 부분 뷰입니다. @ViewBuilder func profileImageSectionBuilder() -> some View { - if let image = viewStore.displayedProfileImage { - image - .resizable() - .frame(width: 130, - height: 130) - .background(Color.brandColor(color: .gray2)) - .clipShape(Circle()) - .overlay(alignment: .bottomTrailing) { - PhotoPickerView(viewStore: viewStore) { - SCIcon( - style: .init( - size: .medium, - color: .gray3 - ), - imageName: .pencil) - } - } - .frame(maxWidth: .infinity) - } else { - // image PlaceHolder - Circle() - .frame(width: 130, - height: 130) - // - FIXME: Color 변경 - .foregroundStyle( - Color.brandColor(color: .gray1) - ) - .overlay(alignment: .bottomTrailing) { - PhotoPickerView(viewStore: viewStore) { - SCIcon( - style: .init( - size: .medium, - color: .gray3 - ), - imageName: .pencil) - } + viewStore.displayedProfileImage + .imagePlaceHolder(size: 130) + .overlay(alignment: .bottomTrailing) { + PhotoPickerView( + store: store.scope(state: \.photoPicker, + action: \.photoPicker) + ) { + SCIcon( + style: .init( + size: .medium, + color: .gray3 + ), + imageName: .pencil) } - .frame(maxWidth: .infinity) - } + } + .frame(maxWidth: .infinity) } //MARK: - nickNameSectionBuilder @@ -109,7 +99,7 @@ struct ProfileEditView: View { /// 닉네임을 수정할 수 있는 부분 뷰입니다. @ViewBuilder func nickNameSectionBuilder() -> some View { - Text("닉네임") + Text(viewStore.displayedNickName) .pretendard(.body2) .foregroundStyle( Color.brandColor(color: .text1) @@ -130,7 +120,7 @@ struct ProfileEditView: View { .foregroundStyle(Color.brandColor(color: .text1)) SCButton(style: .gray) { - + store.send(.workOutTimeButtonTapped) } label: { Text("매일 오전 00:00") .pretendard(.body1) @@ -151,7 +141,7 @@ struct ProfileEditView: View { SCButton(style: .gray) { viewStore.send(.schoolEditButtonTapped) } label: { - Text("학교") + Text(viewStore.displayedSchool) .foregroundStyle(Color.brandColor(color: .text1)) .frame(maxWidth: .infinity) } @@ -185,25 +175,25 @@ struct ProfileEditView: View { .foregroundStyle(Color.brandColor(color: .text1)) HStack(spacing: 16) { - SCButton( - style: viewStore.displayedSex == .male ? - .primary : .teritary) { - viewStore.send(.sexSelectButtonTapped(.male)) - } label: { - Text("남") - .frame(maxWidth: .infinity) - } - .clipShape(Capsule()) - - SCButton( - style: viewStore.displayedSex == .female ? - .primary : .teritary) { - viewStore.send(.sexSelectButtonTapped(.female)) - } label: { - Text("여") - .frame(maxWidth: .infinity) - } - .clipShape(Capsule()) + ForEach(Gender.sex, + id: \.self) { sex in + Button { + viewStore.send(.sexSelectButtonTapped(sex)) + } label: { + Text(sex.rawValue) + .pretendard(.body3) + .foregroundStyle(viewStore.displayedSex == sex ? + .white : + Color.brandColor(color: .text2) + ) + .frame(maxWidth: .infinity) + .padding(.vertical, 8) + .padding(.horizontal, 16) + .background(viewStore.displayedSex == sex ? Color.brandColor(color: .main) : .white) + + } +// .clipShape(Capsule()) + } } } @@ -213,6 +203,7 @@ struct ProfileEditView: View { @ViewBuilder func heightAndWeightSectionBuilder() -> some View { HStack(spacing: 32) { + // 키 VStack(alignment: .leading) { Text("키") .pretendard(.body2) @@ -225,6 +216,7 @@ struct ProfileEditView: View { .keyboardType(.numberPad) } + // 몸무게 VStack(alignment: .leading) { Text("몸무게") .pretendard(.body2) @@ -244,16 +236,7 @@ struct ProfileEditView: View { #Preview { ProfileEditView( - store: .init(initialState: ProfileEditFeature.State( - displayedNickName: "", - displayedHeight: "", - displayedWeight: "", - displayedSex: .female, - displayedSchool: "감자", - displayedGrade: 1, - isPresentingEditGradeSheet: false, - isPresentingEditSchoolSheet: false - ), + store: .init(initialState: .init(displayedSchool: "감자대학교"), reducer: { ProfileEditFeature() } ) ) diff --git a/Score/Score/View/MyPage/Setting/SettingMainView.swift b/Score/Score/View/MyPage/Setting/SettingMainView.swift index 765a131..ad0c1df 100644 --- a/Score/Score/View/MyPage/Setting/SettingMainView.swift +++ b/Score/Score/View/MyPage/Setting/SettingMainView.swift @@ -54,7 +54,7 @@ struct SettingMainView: View { Text("환경설정") } .scPopUp(style: .dialog, - isPresented: viewStore.$isPresentedSignOutDialog) { + isPresented: viewStore.$isPresentingSignOutDialogDialog) { SignOutDialog(viewStore: viewStore) } } diff --git a/Score/Score/View/UIComponent/Button/SCCapsuleButton.swift b/Score/Score/View/UIComponent/Button/SCCapsuleButton.swift index b7b20ba..de8a53d 100644 --- a/Score/Score/View/UIComponent/Button/SCCapsuleButton.swift +++ b/Score/Score/View/UIComponent/Button/SCCapsuleButton.swift @@ -13,6 +13,7 @@ enum SCCapsuleButtonStyle { case primary case secondary case gray + case line } //MARK: - SCCapsuleButton @@ -53,7 +54,6 @@ struct SCCapsuleButtonViewModifier: ViewModifier { func body(content: Content) -> some View { switch style { - case .primary: content .pretendard(weight: .semiBold, @@ -61,8 +61,11 @@ struct SCCapsuleButtonViewModifier: ViewModifier { .foregroundStyle(Color.white) .padding(.vertical, 12) .padding(.horizontal, 41) - .background(Color.brandColor(color: .main), - in: RoundedRectangle(cornerRadius: 21)) + .background( + Color.brandColor(color: .main), + in: RoundedRectangle(cornerRadius: 21) + ) + case .secondary: content .pretendard(weight: .semiBold, @@ -70,8 +73,10 @@ struct SCCapsuleButtonViewModifier: ViewModifier { .foregroundStyle(Color.brandColor(color: .main)) .padding(.vertical, 12) .padding(.horizontal, 41) - .background(Color.brandColor(color: .sub3), - in: RoundedRectangle(cornerRadius: 21)) + .background( + Color.brandColor(color: .sub3), + in: RoundedRectangle(cornerRadius: 21) + ) case .gray: content @@ -80,8 +85,24 @@ struct SCCapsuleButtonViewModifier: ViewModifier { .foregroundStyle(.white) .padding(.vertical, 4) .padding(.horizontal, 12) - .background(Color.brandColor(color: .gray1), - in: Capsule()) + .background( + Color.brandColor(color: .gray1), + in: Capsule() + ) + + case .line: + content + .pretendard(.body3) + .foregroundStyle(Color.brandColor(color: .text2)) + .padding(.vertical, 8) + .padding(.horizontal, 16) + .background { + Capsule() + .stroke( + Color.brandColor(color: .gray3), + lineWidth: 1 + ) + } } } } @@ -115,5 +136,11 @@ struct SCCapsuleButtonViewModifier: ViewModifier { } label: { Text("gray") } + + SCCapsuleButton(style: .line) { + + } label: { + Text("line") + } } } diff --git a/Score/Score/View/UIComponent/Button/SCIconButton.swift b/Score/Score/View/UIComponent/Button/SCIconButton.swift new file mode 100644 index 0000000..1f92bf4 --- /dev/null +++ b/Score/Score/View/UIComponent/Button/SCIconButton.swift @@ -0,0 +1,60 @@ +// +// SCIconButton.swift +// Score +// +// Created by sole on 4/24/24. +// + +import SwiftUI + +//MARK: - SCIconButton + +struct SCIconButton: View { + private let iconSize = Constants.Layout.iconSize.rawValue + let imageName: Constants.ImageName + let color: Color + let action: () -> (Void) + + init(imageName: Constants.ImageName, + color: Color, + action: @escaping () -> Void) { + self.imageName = imageName + self.action = action + self.color = color + } + + init(imageName: Constants.ImageName, + action: @escaping () -> Void) { + self.imageName = imageName + self.action = action + self.color = .brandColor(color: .icon) + } + + var body: some View { + Button(action: action) { + Image(imageName.rawValue) + .resizable() + .renderingMode(.template) + .foregroundStyle(color) + .frame(width: iconSize, + height: iconSize) + } + .frame(width: iconSize, + height: iconSize) + } +} + +//MARK: - Preview + +#Preview { + VStack { + SCIconButton(imageName: .arrowUp) { + + } + + SCIconButton(imageName: .arrowUp, + color: .brandColor(color: .main)) { + + } + } +} diff --git a/Score/Score/View/UIComponent/Card/SCInformationCard.swift b/Score/Score/View/UIComponent/Card/SCInformationCard.swift index 7655db5..b00bff4 100644 --- a/Score/Score/View/UIComponent/Card/SCInformationCard.swift +++ b/Score/Score/View/UIComponent/Card/SCInformationCard.swift @@ -18,16 +18,16 @@ enum Weather: String { //MARK: - imageName /// 날씨에 해당하는 이미지 이름을 반환합니다. - func imageName() -> String { + func imageName() -> Constants.ImageName { switch self { case .sun: - return Constants.ImageName.weatherSun.rawValue + return .weatherSun case .cloudy: - return Constants.ImageName.weatherCloudy.rawValue + return .weatherCloudy case .snow: - return Constants.ImageName.weatherSnow.rawValue + return .weatherSnow case .rain: - return Constants.ImageName.weatherSnow.rawValue + return .weatherSnow } } } @@ -42,14 +42,14 @@ enum Dust: String { //MARK: - imageName /// 미세먼지 농도에 해당하는 이미지 이름을 반환합니다. - func imageName() -> String { + func imageName() -> Constants.ImageName { switch self { case .bad: - return Constants.ImageName.dustSad.rawValue + return .dustSad case .regular: - return Constants.ImageName.dustMood.rawValue + return .dustMood case .good: - return Constants.ImageName.dustSmile.rawValue + return .dustSmile } } } diff --git a/Score/Score/View/UIComponent/Card/SCProgressCard.swift b/Score/Score/View/UIComponent/Card/SCProgressCard.swift index d71a207..12ec119 100644 --- a/Score/Score/View/UIComponent/Card/SCProgressCard.swift +++ b/Score/Score/View/UIComponent/Card/SCProgressCard.swift @@ -23,15 +23,9 @@ struct SCProgressCard: View { var body: some View { HStack(spacing: 12) { - image? - .resizable() - .frame(width: 53, height: 53) - .background { - Color.brandColor(color: .gray3) - } - .clipShape(Circle()) + image + .imagePlaceHolder(size: 53) - VStack(alignment: .leading, spacing: 4) { HStack { diff --git a/Score/Score/View/UIComponent/Chip/SCTagChip.swift b/Score/Score/View/UIComponent/Chip/SCTagChip.swift index 1aac25f..9e6028f 100644 --- a/Score/Score/View/UIComponent/Chip/SCTagChip.swift +++ b/Score/Score/View/UIComponent/Chip/SCTagChip.swift @@ -63,7 +63,7 @@ extension View { #Preview { VStack { SCLabel(style: .button, - imageName: Constants.ImageName.bell.rawValue, + imageName: .bell, title: "1234") .foregroundStyle(Color.red) .padding(.vertical, 10) diff --git a/Score/Score/View/UIComponent/Label/SCLabel.swift b/Score/Score/View/UIComponent/Label/SCLabel.swift index 33e8ffc..b7a2410 100644 --- a/Score/Score/View/UIComponent/Label/SCLabel.swift +++ b/Score/Score/View/UIComponent/Label/SCLabel.swift @@ -20,7 +20,7 @@ enum SCLabelStyle { struct SCLabel: View { let style: SCLabelStyle - let imageName: String + let imageName: Constants.ImageName let title: String /// - Returns: style에 따라 SCLabel에 사용되는 이미지 크기를 반환합니다. @@ -49,7 +49,7 @@ struct SCLabel: View { var body: some View { HStack(spacing: 4) { - Image(imageName) + Image(imageName.rawValue) .resizable() .renderingMode(.template) .frame(width: imageSize, @@ -69,13 +69,13 @@ struct SCLabel: View { #Preview { VStack { SCLabel(style: .card, - imageName: Constants.ImageName.check.rawValue, + imageName: .check, title: "123") SCLabel(style: .button, - imageName: Constants.ImageName.camera.rawValue, + imageName: .camera, title: "123") SCLabel(style: .chip, - imageName: Constants.ImageName.dustSad.rawValue, + imageName: .dustSad, title: "123") } } diff --git a/Score/Score/View/UIComponent/Navigation/SCNavigationBar.swift b/Score/Score/View/UIComponent/Navigation/SCNavigationBar.swift index 4d21388..6d3b712 100644 --- a/Score/Score/View/UIComponent/Navigation/SCNavigationBar.swift +++ b/Score/Score/View/UIComponent/Navigation/SCNavigationBar.swift @@ -25,8 +25,8 @@ enum SCNavigationStyle { /// - content: Navigation Bar에 표시하고 싶은 View를 정의합니다. 기본적으로 HStack으로 구현되어 있기 때문에 HStack을 선언하지 않고 필요한 Component를 나열합니다. .pretendard(.title)이 기본 설정입니다. /// - style: Navigation Bar를 display하는 방식을 정의합니다. struct SCNavigationBar: View { + private let constants = Constants.Layout.self let style: SCNavigationStyle -// @Dependency(\.dismiss) var dismiss @ViewBuilder let content: () -> Content var body: some View { @@ -36,10 +36,28 @@ struct SCNavigationBar: View { .foregroundStyle( Color.brandColor(color: .text1) ) + Spacer() } .padding(.vertical, 10) + .frame( + height: constants.navigationBarHeight.rawValue + ) .layout() } + + //MARK: - backgroundColor + + @ViewBuilder + func backgroundColor( + _ color: Color? + ) -> some View { + if color == nil { + self + } else { + self + .background(color ?? .clear) + } + } } //MARK: - View+scNavigationBar @@ -52,6 +70,7 @@ extension View { @ViewBuilder func scNavigationBar( style: SCNavigationStyle, + backgroundColor: Color? = nil, @ViewBuilder content: @escaping () -> Content ) -> some View { switch style { @@ -64,13 +83,16 @@ extension View { SCNavigationBar(style: style) { content() } + .backgroundColor(backgroundColor) } + case .vertical: VStack(alignment: .leading) { SCNavigationBar(style: style) { content() } + .backgroundColor(backgroundColor) self .toolbar(.hidden) @@ -102,7 +124,8 @@ extension View { Image(systemName: "checkmark") } .navigationTitle("123") - .scNavigationBar(style: .overlay) { + .scNavigationBar(style: .overlay, + backgroundColor: .blue) { DismissButton(style: .chevron, color: .white) { @@ -142,7 +165,8 @@ extension View { .toolbar { Image(systemName: "checkmark") } - .scNavigationBar(style: .vertical) { + .scNavigationBar(style: .vertical, + backgroundColor: .yellow) { DismissButton(style: .chevron, color: .white) { diff --git a/Score/Score/View/UIComponent/TabBar/SCLineTabBar.swift b/Score/Score/View/UIComponent/TabBar/SCLineTabBar.swift index be2b1f8..b78b24e 100644 --- a/Score/Score/View/UIComponent/TabBar/SCLineTabBar.swift +++ b/Score/Score/View/UIComponent/TabBar/SCLineTabBar.swift @@ -5,40 +5,54 @@ // Created by sole on 4/14/24. // +import ComposableArchitecture import SwiftUI +protocol SCLineTabBarItemProtocol: Hashable { + associatedtype Tab: SCLineTabProtocol + + var title: String { get set } + var tab: Tab { get set } +} + +protocol SCLineTabProtocol: Hashable, + Equatable, + CaseIterable { +} + +struct SCLineTabItem: SCLineTabBarItemProtocol { + var title: String + var tab: Tab +} + + //MARK: - SCLineTabBar -struct SCLineTabBar: View { - var items: [String] = [] - - @State var selectedValue: String +struct SCLineTabBar: View { + let store: StoreOf> + @ObservedObject var viewStore: ViewStoreOf> //MARK: - init - init(items: [String]) { - self.items = items - guard let firstElement = items.first - else { - self.selectedValue = "defaultModel" - return - } - self.selectedValue = firstElement + init(store: StoreOf>) { + self.store = store + self.viewStore = ViewStore(store, + observe: { $0 }) } var body: some View { // - FIXME: 디자인 상의 필요, animation 적용 HStack(spacing: 50) { - ForEach(items, + ForEach(viewStore.tabItems, id: \.self) { item in - Text("\(item)") - .transition(.slide) + Text("\(item.title)") .modifier( - SCLineTabBarItemViewModifier(selectedValue: $selectedValue, - item: item) + SCLineTabBarItemViewModifier( + store: store, + item: item + ) ) } - } .frame(maxWidth: .infinity) } @@ -46,20 +60,32 @@ struct SCLineTabBar: View { //MARK: - SCLineTabBarItemViewModifier -struct SCLineTabBarItemViewModifier: ViewModifier { - @Binding var selectedValue: String - let item: String +struct SCLineTabBarItemViewModifier: ViewModifier { + let store: StoreOf> + let item: SCLineTabItem + + @ObservedObject var viewStore: ViewStoreOf> + + //MARK: - init + + init(store: StoreOf>, + item: SCLineTabItem) { + self.store = store + self.viewStore = ViewStore(store, + observe: { $0 }) + self.item = item + } func body(content: Content) -> some View { Button { - withAnimation { - selectedValue = item - } + viewStore.send(.tabBarButtonTapped(item), + animation: .easeIn) +// viewStore.send(.tabBarButtonTapped(item)) } label: { content .pretendard(.body2) .foregroundStyle( - selectedValue == item ? + viewStore.selectedTab == item.tab ? Color.brandColor(color: .text1) : Color.brandColor(color: .text3) ) @@ -67,7 +93,7 @@ struct SCLineTabBarItemViewModifier: ViewModifier { .padding(.horizontal, 20) .frame(maxWidth: 120) .edgeBorder(lineWidth: 2, - edges: selectedValue == item ? + edges: viewStore.selectedTab == item.tab ? [.bottom] : []) } } @@ -82,21 +108,25 @@ extension View { /// - Parameters: /// - items: LineTabBarItem(TabBar title)을 정의합니다. @ViewBuilder - func scLineTabBar(items: [String]) -> some View { - VStack(alignment: .leading, - spacing: 2) { - SCLineTabBar(items: items) - self + func scLineTabBar( + store: StoreOf> + ) -> some View { + self + .padding(.top, 20) + .overlay(alignment: .top) { + SCLineTabBar(store: store) + .offset(y: -20) + .transition(.slide) } } } //MARK: - Preview -#Preview { - Rectangle() - .foregroundStyle(.mint) - .scLineTabBar(items: ["피드(nn개)", - "캘린더"]) - .layout() -} +//#Preview { +// Rectangle() +// .foregroundStyle(.mint) +// .scLineTabBar(store: .init(initialState: .init(), +// reducer: { SCLineTabBarFeature() })) +// .layout() +//}