diff --git a/WithSuhyeon-iOS/WithSuhyeon-iOS/Core/DesignSystem/Component/WithSuhyeonLocationSelect.swift b/WithSuhyeon-iOS/WithSuhyeon-iOS/Core/DesignSystem/Component/WithSuhyeonLocationSelect.swift new file mode 100644 index 0000000..e032f0e --- /dev/null +++ b/WithSuhyeon-iOS/WithSuhyeon-iOS/Core/DesignSystem/Component/WithSuhyeonLocationSelect.swift @@ -0,0 +1,94 @@ +// +// WithSuhyeonLocationSelect.swift +// WithSuhyeon-iOS +// +// Created by 우상욱 on 1/15/25. +// + +import SwiftUI + +struct WithSuhyeonLocationSelect: View { + let withSuhyeonLocation: [WithSuhyeonLocation] + let selectedMainLocationIndex: Int + let selectedSubLocationIndex: Int + let onTabSelected: (Int, Int) -> Void + + var body: some View { + HStack(spacing: 0) { + ScrollView { + VStack(spacing: 0) { + ForEach(withSuhyeonLocation.indices) { index in + ZStack { + Text(withSuhyeonLocation[index].location) + .font(.body02SB) + .foregroundColor(selectedMainLocationIndex == index ? Color.white : Color.black) + .padding(.vertical, 9) + .padding(.leading, 12) + .padding(.trailing, 42) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(selectedMainLocationIndex == index ? Color.primary500 : Color.clear) + ) + } + .frame(height: 50) + .contentShape(Rectangle()) + .onTapGesture { + if(selectedMainLocationIndex != index) { + onTabSelected(index, 0) + } + } + } + }.padding(.leading, 16) + .padding(.trailing, 8) + } + Divider() + .foregroundColor(.gray100) + ScrollView { + VStack(alignment: .leading, spacing: 0) { + ForEach(withSuhyeonLocation[selectedMainLocationIndex].subLocation.indices) { index in + ZStack(alignment: .leading) { + HStack { + Text(withSuhyeonLocation[selectedMainLocationIndex].subLocation[index]) + .font(.body03SB) + .foregroundColor(.gray500) + Spacer() + } + .padding(.leading, 12) + .padding(.vertical, 10) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(selectedSubLocationIndex == index ? Color.primary50 : Color.clear) + ) + .frame(width: .infinity) + .contentShape(Rectangle()) + .onTapGesture { + onTabSelected(selectedMainLocationIndex, index) + } + } + .frame(height: 50) + .padding(.leading, 8) + .padding(.trailing, 16) + } + } + } + }.frame(height: .infinity) + } +} + +struct locationPreview: View { + @State var selectedMainLocationIndex: Int = 0 + @State var selectedSubLocationIndex: Int = 0 + + var body: some View { + VStack { + WithSuhyeonLocationSelect(withSuhyeonLocation: WithSuhyeonLocation.location, selectedMainLocationIndex: selectedMainLocationIndex, selectedSubLocationIndex: selectedSubLocationIndex, onTabSelected: { num1, num2 in + selectedMainLocationIndex = num1 + selectedSubLocationIndex = num2 + }) + }.frame(height: 400) + } +} + +#Preview { + locationPreview() +} diff --git a/WithSuhyeon-iOS/WithSuhyeon-iOS/Core/DesignSystem/Foundation/WithSuhyeonLocation.swift b/WithSuhyeon-iOS/WithSuhyeon-iOS/Core/DesignSystem/Foundation/WithSuhyeonLocation.swift new file mode 100644 index 0000000..4966094 --- /dev/null +++ b/WithSuhyeon-iOS/WithSuhyeon-iOS/Core/DesignSystem/Foundation/WithSuhyeonLocation.swift @@ -0,0 +1,18 @@ +// +// WithSuhyeonLocation.swift +// WithSuhyeon-iOS +// +// Created by 우상욱 on 1/15/25. +// + +import Foundation + +struct WithSuhyeonLocation: Identifiable { + let id: UUID = UUID() + let location: String + let subLocation: [String] +} + +extension WithSuhyeonLocation { + static let location = [WithSuhyeonLocation(location: "서울", subLocation: ["서울1","서울2","서울3","서울4","서울5","서울6","서울7","서울8","서울9","서울10","서울11"]),WithSuhyeonLocation(location: "부산", subLocation: ["부산1","부산2","부산3","부산4","부산5","부산6","부산7","부산8","부산9","부산10","부산11",]),WithSuhyeonLocation(location: "서울", subLocation: ["서울","서울","서울","서울","서울","서울","서울","서울","서울","서울","서울",]),WithSuhyeonLocation(location: "서울", subLocation: ["서울","서울","서울","서울","서울","서울","서울","서울","서울","서울","서울",]),WithSuhyeonLocation(location: "서울", subLocation: ["서울","서울","서울","서울","서울","서울","서울","서울","서울","서울","서울",]),WithSuhyeonLocation(location: "서울", subLocation: ["서울","서울","서울","서울","서울","서울","서울","서울","서울","서울","서울",]),WithSuhyeonLocation(location: "서울", subLocation: ["서울","서울","서울","서울","서울","서울","서울","서울","서울","서울","서울",]),WithSuhyeonLocation(location: "서울", subLocation: ["서울","서울","서울","서울","서울","서울","서울","서울","서울","서울","서울",]),] +} diff --git a/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/.gitkeep b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/SignUpContent.swift b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/SignUpContent.swift new file mode 100644 index 0000000..dc484ab --- /dev/null +++ b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/SignUpContent.swift @@ -0,0 +1,39 @@ +// +// SignUpContent.swift +// WithSuhyeon-iOS +// +// Created by 김예지 on 1/15/25. +// + +import SwiftUI + +struct SignUpContent: View { + @Binding var selectedTab: SignUpContentCase + init(selectedTab: Binding) { + self._selectedTab = selectedTab + UITabBar.appearance().isHidden = true + } + + var body: some View { + TabView(selection: $selectedTab) { + TermsOfServiceView() + .tag(SignUpContentCase.termsOfServiceView) + PhoneAuthenticationView() + .tag(SignUpContentCase.authenticationView) + WriteNickNameView() + .tag(SignUpContentCase.nickNameView) + SelectBirthYearView() + .tag(SignUpContentCase.birthYearView) + SelectGenderView() + .tag(SignUpContentCase.genderView) + ProfileImageView() + .tag(SignUpContentCase.profileImageView) + ActiveAreaView() + .tag(SignUpContentCase.activeAreaView) + } + .tabViewStyle(PageTabViewStyle()) + .onAppear { + UIScrollView.appearance().isScrollEnabled = false + } + } +} diff --git a/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/SignUpContentCase.swift b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/SignUpContentCase.swift new file mode 100644 index 0000000..0973575 --- /dev/null +++ b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/SignUpContentCase.swift @@ -0,0 +1,37 @@ +// +// SignUpContentCase.swift +// WithSuhyeon-iOS +// +// Created by 김예지 on 1/15/25. +// + +import Foundation + +public enum SignUpContentCase: CaseIterable { + case termsOfServiceView + case authenticationView + case nickNameView + case birthYearView + case genderView + case profileImageView + case activeAreaView + + var title: String { + switch self { + case .termsOfServiceView: + return "수현이랑\n서비스 이용약관" + case .authenticationView: + return "본인인증을 위한\n휴대폰 번호 인증이 필요해요" + case .nickNameView: + return "수현이랑에서 사용할\n닉네임을 입력해주세요" + case .birthYearView: + return "태어난 년도를\n선택해주세요" + case .genderView: + return "성별을\n선택해주세요" + case .profileImageView: + return "프로필 이미지를\n등록해주세요" + case .activeAreaView: + return "자주 활동하는\n지역을 선택해주세요" + } + } +} diff --git a/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/SignUpFeature.swift b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/SignUpFeature.swift new file mode 100644 index 0000000..e83a418 --- /dev/null +++ b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/SignUpFeature.swift @@ -0,0 +1,118 @@ +// +// SignUpFeature.swift +// WithSuhyeon-iOS +// +// Created by 김예지 on 1/15/25. +// + +import Foundation +import Combine + +class SignUpFeature: Feature { + struct State { + var progress: Double = 0.0 + var isAgree: Bool = false + var buttonState: WithSuhyeonButtonState = .disabled + } + + enum Intent { + case tapButton + case tapBackButton + } + + enum SideEffect { + + } + + @Published private(set) var state = State() + @Published var currentContent: SignUpContentCase = .termsOfServiceView + private var cancellables = Set() + private let intentSubject = PassthroughSubject() + let sideEffectSubject = PassthroughSubject() + + init() { + bindIntents() + updateProgress() + receiveState() + } + + private func bindIntents() { + intentSubject.sink { [weak self] intent in + self?.handleIntent(intent) + }.store(in: &cancellables) + } + + private func receiveState() { + $state.sink { [weak self] _ in + DispatchQueue.main.async { + self?.updateButtonState() + } + }.store(in: &cancellables) + + $currentContent.sink { [weak self] _ in + DispatchQueue.main.async { + self?.updateButtonState() + } + }.store(in: &cancellables) + } + + func send(_ intent: Intent) { + intentSubject.send(intent) + } + + func handleIntent(_ intent: Intent) { + switch intent { + case .tapButton: + moveToNextStep() + case .tapBackButton: + moveToPreviousStep() + } + } + + private func moveToNextStep() { + if let currentIndex = SignUpContentCase.allCases.firstIndex(of: currentContent), + currentIndex < SignUpContentCase.allCases.count - 1 { + currentContent = SignUpContentCase.allCases[currentIndex + 1] + updateProgress() + } + } + + private func moveToPreviousStep() { + if let currentIndex = SignUpContentCase.allCases.firstIndex(of: currentContent), + currentIndex > 0 { + currentContent = SignUpContentCase.allCases[currentIndex - 1] + updateProgress() + } + } + + private func updateProgress() { + if let currentIndex = SignUpContentCase.allCases.firstIndex(of: currentContent) { + state.progress = Double(currentIndex + 1) / Double(SignUpContentCase.allCases.count) * 100 + } + } + + private func updateButtonState() { + let newButtonState: WithSuhyeonButtonState + + switch currentContent { + case .termsOfServiceView: + newButtonState = state.isAgree ? .enabled : .disabled + default: + newButtonState = .enabled + } + + guard state.buttonState != newButtonState else { + return + } + + state.buttonState = newButtonState + } + + func changeSelectedContent(signUpContentCase: SignUpContentCase) { + currentContent = signUpContentCase + } + + func updateIsAgree(_ newValue: Bool) { + state.isAgree = newValue + } +} diff --git a/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/SignUpView.swift b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/SignUpView.swift new file mode 100644 index 0000000..387fae5 --- /dev/null +++ b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/SignUpView.swift @@ -0,0 +1,48 @@ +// +// SignUpView.swift +// WithSuhyeon-iOS +// +// Created by 김예지 on 1/15/25. +// + +import SwiftUI + +struct SignUpView: View { + @EnvironmentObject private var router: RouterRegistry + @StateObject private var signUpFeature = SignUpFeature() + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + WithSuhyeonTopNavigationBar( + title: "", + onTapLeft: { + signUpFeature.send( + .tapBackButton + ) + }) + + WithSuhyeonProgressBar(progress: signUpFeature.state.progress) + + Text(signUpFeature.currentContent.title) + .font(.title02B) + .padding(.leading, 16) + .padding(.vertical, 20) + + SignUpContent(selectedTab: $signUpFeature.currentContent) + + WithSuhyeonButton( + title: "다음", + buttonState: signUpFeature.state.buttonState, + clickable: signUpFeature.state.buttonState == .enabled, + onTapButton: { + signUpFeature.send(.tapButton) + } + ) + .padding(.horizontal, 16) + }.environmentObject(signUpFeature) + } +} + +#Preview { + SignUpView() +} diff --git a/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/View/ActiveAreaView.swift b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/View/ActiveAreaView.swift new file mode 100644 index 0000000..9900e11 --- /dev/null +++ b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/View/ActiveAreaView.swift @@ -0,0 +1,18 @@ +// +// ActiveAreaView.swift +// WithSuhyeon-iOS +// +// Created by 김예지 on 1/15/25. +// + +import SwiftUI + +struct ActiveAreaView: View { + var body: some View { + Text("활동 지역 선택") + } +} + +#Preview { + ActiveAreaView() +} diff --git a/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/View/PhoneAuthenticationView.swift b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/View/PhoneAuthenticationView.swift new file mode 100644 index 0000000..baa74b7 --- /dev/null +++ b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/View/PhoneAuthenticationView.swift @@ -0,0 +1,18 @@ +// +// PhoneAuthenticationView.swift +// WithSuhyeon-iOS +// +// Created by 김예지 on 1/15/25. +// + +import SwiftUI + +struct PhoneAuthenticationView: View { + var body: some View { + Text("핸드폰 인증") + } +} + +#Preview { + PhoneAuthenticationView() +} diff --git a/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/View/ProfileImageView.swift b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/View/ProfileImageView.swift new file mode 100644 index 0000000..5640a1b --- /dev/null +++ b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/View/ProfileImageView.swift @@ -0,0 +1,18 @@ +// +// ProfileImageView.swift +// WithSuhyeon-iOS +// +// Created by 김예지 on 1/15/25. +// + +import SwiftUI + +struct ProfileImageView: View { + var body: some View { + Text("프로필 이미지 선택") + } +} + +#Preview { + ProfileImageView() +} diff --git a/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/View/SelectBirthYearView.swift b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/View/SelectBirthYearView.swift new file mode 100644 index 0000000..86f46ab --- /dev/null +++ b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/View/SelectBirthYearView.swift @@ -0,0 +1,18 @@ +// +// SelectBirthYearView.swift +// WithSuhyeon-iOS +// +// Created by 김예지 on 1/15/25. +// + +import SwiftUI + +struct SelectBirthYearView: View { + var body: some View { + Text("태어난 년도 선택") + } +} + +#Preview { + SelectBirthYearView() +} diff --git a/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/View/SelectGenderView.swift b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/View/SelectGenderView.swift new file mode 100644 index 0000000..86c688d --- /dev/null +++ b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/View/SelectGenderView.swift @@ -0,0 +1,19 @@ +// +// SelectGenderView.swift +// WithSuhyeon-iOS +// +// Created by 김예지 on 1/15/25. +// + +import SwiftUI + +struct SelectGenderView: View { + var body: some View { + Text("성별 선택 뷰") + .navigationBarBackButtonHidden(true) + } +} + +#Preview { + SelectGenderView() +} diff --git a/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/View/TermsOfServiceView.swift b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/View/TermsOfServiceView.swift new file mode 100644 index 0000000..630a299 --- /dev/null +++ b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/View/TermsOfServiceView.swift @@ -0,0 +1,77 @@ +// +// TermsOfServiceView.swift +// WithSuhyeon-iOS +// +// Created by 김예지 on 1/15/25. +// + +import SwiftUI + +struct TermsOfServiceView: View { + @EnvironmentObject var signUpFeature: SignUpFeature + @State private var agreeStatus: [Bool] = [false, false, false] + + var body: some View { + VStack(alignment: .leading) { + WithSuhyeonCheckbox( + state: agreeStatus.allSatisfy { $0 } ? .checked : .unchecked, + placeholder: "모두 동의", + hasBackground: true + ) { + toggleAllChecks() + } + .padding(.bottom, 16) + + VStack(alignment: .leading, spacing: 16) { + ForEach(agreeStatus.indices, id: \.self) { index in + HStack(alignment: .top, spacing: 12) { + WithSuhyeonCheckbox( + state: agreeStatus[index] ? .checked : .unchecked, + placeholder: getPlaceholder(for: index), + hasBackground: false + ) { + toggleAgreeCheck(at: index) + } + WithSuhyeonUnderlineButton(title: "보기") {} + } + } + } + .padding(.horizontal, 20) + .padding(.vertical, 24) + .frame(maxWidth: .infinity, alignment: .topLeading) + .cornerRadius(20) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(Color.gray200, lineWidth: 1) + ) + Spacer() + } + .padding(.horizontal, 16) + .onChange(of: agreeStatus) { newStatus in + let allSelected = newStatus.allSatisfy { $0 } + signUpFeature.updateIsAgree(allSelected) + } + } + + private func toggleAllChecks() { + let newState = !agreeStatus.allSatisfy { $0 } + agreeStatus = Array(repeating: newState, count: agreeStatus.count) + } + + private func toggleAgreeCheck(at index: Int) { + agreeStatus[index].toggle() + } + + private func getPlaceholder(for index: Int) -> String { + switch index { + case 0: return "[필수] 만 18세 이상" + case 1: return "[필수] 이용약관 동의" + case 2: return "[필수] 개인정보 처리방침 동의" + default: return "" + } + } +} + +#Preview { + TermsOfServiceView() +} diff --git a/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/View/WriteNickNameView.swift b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/View/WriteNickNameView.swift new file mode 100644 index 0000000..23100e6 --- /dev/null +++ b/WithSuhyeon-iOS/WithSuhyeon-iOS/Presentation/SignUp/View/WriteNickNameView.swift @@ -0,0 +1,18 @@ +// +// WriteNickNameView.swift +// WithSuhyeon-iOS +// +// Created by 김예지 on 1/15/25. +// + +import SwiftUI + +struct WriteNickNameView: View { + var body: some View { + Text("닉네임 입력") + } +} + +#Preview { + WriteNickNameView() +}