diff --git a/Projects/App/Sources/Application/DI/AppComponent.swift b/Projects/App/Sources/Application/DI/AppComponent.swift index 2653499b..3ee45609 100644 --- a/Projects/App/Sources/Application/DI/AppComponent.swift +++ b/Projects/App/Sources/Application/DI/AppComponent.swift @@ -36,6 +36,9 @@ public extension AppComponent { var signupEmailAuthCodeVerifyComponent: SignupEmailAuthCodeVerifyComponent { SignupEmailAuthCodeVerifyComponent(parent: self) } + var signupProfileImageComponent: SignupProfileImageComponent { + SignupProfileImageComponent(parent: self) + } } // MARK: - Main diff --git a/Projects/App/Sources/Application/NeedleGenerated.swift b/Projects/App/Sources/Application/NeedleGenerated.swift index 6f394c9c..02dba95e 100644 --- a/Projects/App/Sources/Application/NeedleGenerated.swift +++ b/Projects/App/Sources/Application/NeedleGenerated.swift @@ -70,6 +70,17 @@ private class SignupEmailVerifyDependencyf9d372ac752ee19b78caProvider: SignupEma private func factory3b1904c76335d70151ebf47b58f8f304c97af4d5(_ component: NeedleFoundation.Scope) -> AnyObject { return SignupEmailVerifyDependencyf9d372ac752ee19b78caProvider(appComponent: parent1(component) as! AppComponent) } +private class SignupProfileImageDependency4203088ab57581d9f871Provider: SignupProfileImageDependency { + + + init() { + + } +} +/// ^->AppComponent->SignupProfileImageComponent +private func factory6792674212c15df7e9cfe3b0c44298fc1c149afb(_ component: NeedleFoundation.Scope) -> AnyObject { + return SignupProfileImageDependency4203088ab57581d9f871Provider() +} private class MainTabDependency2826cdb310ed0b17a725Provider: MainTabDependency { var homeComponent: HomeComponent { return appComponent.homeComponent @@ -226,6 +237,7 @@ private func register1() { registerProviderFactory("^->AppComponent->SchoolCodeComponent", factoryb65c1efbf06b87162473f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->SignupEmailAuthCodeVerifyComponent", factoryb06be35aa893adde971bf47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->SignupEmailVerifyComponent", factory3b1904c76335d70151ebf47b58f8f304c97af4d5) + registerProviderFactory("^->AppComponent->SignupProfileImageComponent", factory6792674212c15df7e9cfe3b0c44298fc1c149afb) registerProviderFactory("^->AppComponent->MainTabComponent", factory1ab5a747ddf21e1393f9f47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->SigninComponent", factory2882a056d84a613debccf47b58f8f304c97af4d5) registerProviderFactory("^->AppComponent->HomeComponent", factory67229cdf0f755562b2b1f47b58f8f304c97af4d5) diff --git a/Projects/App/Support/Info.plist b/Projects/App/Support/Info.plist index 434881ab..a23b8dbb 100644 --- a/Projects/App/Support/Info.plist +++ b/Projects/App/Support/Info.plist @@ -2,6 +2,8 @@ + NSCameraUsageDescription + 사진을 촬영하기 위해서는 권한이 필요합니다! CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName diff --git a/Projects/Features/SignupFeature/Sources/SignupProfileImage/SignupProfileImageComponent.swift b/Projects/Features/SignupFeature/Sources/SignupProfileImage/SignupProfileImageComponent.swift new file mode 100644 index 00000000..afd1bcfc --- /dev/null +++ b/Projects/Features/SignupFeature/Sources/SignupProfileImage/SignupProfileImageComponent.swift @@ -0,0 +1,14 @@ +import SwiftUI +import NeedleFoundation + +public protocol SignupProfileImageDependency: Dependency {} + +public final class SignupProfileImageComponent: Component { + public func makeView(signupProfileImageParam: SignupProfileImageParam) -> some View { + SignupProfileImageView( + viewModel: .init( + signupProfileImageParam: signupProfileImageParam + ) + ) + } +} diff --git a/Projects/Features/SignupFeature/Sources/SignupProfileImage/SignupProfileImageParam.swift b/Projects/Features/SignupFeature/Sources/SignupProfileImage/SignupProfileImageParam.swift new file mode 100644 index 00000000..24307e79 --- /dev/null +++ b/Projects/Features/SignupFeature/Sources/SignupProfileImage/SignupProfileImageParam.swift @@ -0,0 +1,32 @@ +import Foundation + +public struct SignupProfileImageParam: Equatable { + public init( + schoolCode: String, + schoolAnswer: String, + email: String, + grade: Int, + classRoom: Int, + number: Int, + accountID: String, + password: String + ) { + self.schoolCode = schoolCode + self.schoolAnswer = schoolAnswer + self.email = email + self.grade = grade + self.classRoom = classRoom + self.number = number + self.accountID = accountID + self.password = password + } + + public let schoolCode: String + public let schoolAnswer: String + public let email: String + public let grade: Int + public let classRoom: Int + public let number: Int + public let accountID: String + public let password: String +} diff --git a/Projects/Features/SignupFeature/Sources/SignupProfileImage/SignupProfileImageView.swift b/Projects/Features/SignupFeature/Sources/SignupProfileImage/SignupProfileImageView.swift new file mode 100644 index 00000000..e05f1369 --- /dev/null +++ b/Projects/Features/SignupFeature/Sources/SignupProfileImage/SignupProfileImageView.swift @@ -0,0 +1,92 @@ +import DesignSystem +import SwiftUI + +struct SignupProfileImageView: View { + @StateObject var viewModel: SignupProfileImageViewModel + @State var isShowingImagePicker = false + @State var isShowingCameraPicker = false + @State var isPresentedImageActionSheet = false + + public init(viewModel: SignupProfileImageViewModel) { + _viewModel = StateObject(wrappedValue: viewModel) + } + + var body: some View { + VStack { + HStack { + VStack(alignment: .leading, spacing: 8) { + Text("DMS") + .dmsFont(.title(.extraLarge), color: .PrimaryVariant.primary) + + Text("프로필 사진") + .dmsFont(.text(.medium), color: .GrayScale.gray6) + } + + Spacer() + } + .padding(.top, 24) + + Button { + isPresentedImageActionSheet.toggle() + } label: { + ZStack(alignment: .bottomTrailing) { + Group { + if let image = viewModel.selectedImage { + Image(uiImage: image) + .resizable() + } else { + Image(systemName: "person.crop.circle.fill") + .resizable() + } + } + .foregroundColor(.GrayScale.gray4) + .frame(width: 150, height: 150) + .clipShape(Circle()) + + Image(systemName: "plus.circle.fill") + .resizable() + .frame(width: 46, height: 46) + .clipShape(Circle()) + .foregroundColor(.PrimaryVariant.primary) + } + .padding(.top, 80) + } + + Spacer() + + DMSButton(text: "다음에 설정하기", style: .underline, color: .GrayScale.gray6) { + viewModel.skipButtonDidTap() + } + + DMSWideButton(text: "다음", color: .PrimaryVariant.primary) { + viewModel.nextButtonDidTap() + } + .disabled(viewModel.selectedImage == nil) + .padding(.top, 32) + .padding(.bottom, 40) + } + .imagePicker(isShow: $isShowingImagePicker, uiImage: $viewModel.selectedImage) + .cameraPicker(isShow: $isShowingCameraPicker, uiImage: $viewModel.selectedImage) + .padding(.horizontal, 24) + .dmsActionSheet(isPresented: $isPresentedImageActionSheet) { + Button(role: nil) { + isPresentedImageActionSheet = false + isShowingCameraPicker.toggle() + } label: { + Label("사진 촬영", systemImage: "camera.fill") + } + .foregroundColor(.GrayScale.gray6) + + Divider() + .foregroundColor(.GrayScale.gray4) + + Button(role: nil) { + isPresentedImageActionSheet = false + isShowingImagePicker.toggle() + } label: { + Label("사진 선택", systemImage: "photo.tv") + } + .foregroundColor(.GrayScale.gray6) + } + } +} diff --git a/Projects/Features/SignupFeature/Sources/SignupProfileImage/SignupProfileImageViewModel.swift b/Projects/Features/SignupFeature/Sources/SignupProfileImage/SignupProfileImageViewModel.swift new file mode 100644 index 00000000..24cba650 --- /dev/null +++ b/Projects/Features/SignupFeature/Sources/SignupProfileImage/SignupProfileImageViewModel.swift @@ -0,0 +1,20 @@ +import BaseFeature +import Combine +import UIKit + +final class SignupProfileImageViewModel: BaseViewModel { + @Published var selectedImage: UIImage? + let signupProfileImageParam: SignupProfileImageParam + + init(signupProfileImageParam: SignupProfileImageParam) { + self.signupProfileImageParam = signupProfileImageParam + } + + func skipButtonDidTap() { + + } + + func nextButtonDidTap() { + + } +} diff --git a/Projects/UsertInterfaces/DesignSystem/Sources/ActionSheet/DMSActionSheet.swift b/Projects/UsertInterfaces/DesignSystem/Sources/ActionSheet/DMSActionSheet.swift new file mode 100644 index 00000000..ffb46999 --- /dev/null +++ b/Projects/UsertInterfaces/DesignSystem/Sources/ActionSheet/DMSActionSheet.swift @@ -0,0 +1,53 @@ +import SwiftUI + +public extension View { + func dmsActionSheet( + isPresented: Binding, + @ViewBuilder actions: @escaping () -> Actions + ) -> some View { + modifier(DMSActionSheet(isPresented: isPresented, actions: actions)) + } +} +struct DMSActionSheet: ViewModifier { + @Binding var isPresented: Bool + @ViewBuilder let actions: () -> Actions + + func body(content: Content) -> some View { + ZStack { + content + .frame(maxWidth: .infinity, maxHeight: .infinity) + + ZStack(alignment: .bottom) { + if isPresented { + Color.GrayScale.gray9.opacity(0.16) + .ignoresSafeArea() + .onTapGesture { + isPresented = false + } + .transition(.opacity) + } + + if isPresented { + VStack { + GroupBox { + actions() + .frame(maxWidth: .infinity, alignment: .leading) + } + + GroupBox { + Button("취소", role: .cancel) { + isPresented = false + } + .foregroundColor(.GrayScale.gray6) + .frame(maxWidth: .infinity, alignment: .center) + } + } + .font(.title3) + .padding(8) + .transition(.move(edge: .bottom)) + } + } + } + .animation(.easeInOut, value: isPresented) + } +} diff --git a/Projects/UsertInterfaces/DesignSystem/Sources/CameraPicker/CameraPicker.swift b/Projects/UsertInterfaces/DesignSystem/Sources/CameraPicker/CameraPicker.swift new file mode 100644 index 00000000..ed0e33f4 --- /dev/null +++ b/Projects/UsertInterfaces/DesignSystem/Sources/CameraPicker/CameraPicker.swift @@ -0,0 +1,55 @@ +import SwiftUI +import UIKit + +public extension View { + func cameraPicker(isShow: Binding, uiImage: Binding) -> some View { + self + .fullScreenCover(isPresented: isShow) { + CameraPicker(sourceType: .camera, image: uiImage, isPresented: isShow) + } + } +} + +struct CameraPicker: UIViewControllerRepresentable { + var sourceType: UIImagePickerController.SourceType + @Binding var image: UIImage? + @Binding var isPresented: Bool + + func makeCoordinator() -> ImagePickerViewCoordinator { + return ImagePickerViewCoordinator(image: $image, isPresented: $isPresented) + } + + func makeUIViewController(context: Context) -> UIImagePickerController { + let pickerController = UIImagePickerController() + pickerController.sourceType = sourceType + pickerController.delegate = context.coordinator + return pickerController + } + + func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {} + +} + +final class ImagePickerViewCoordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { + @Binding var image: UIImage? + @Binding var isPresented: Bool + + init(image: Binding, isPresented: Binding) { + self._image = image + self._isPresented = isPresented + } + + func imagePickerController( + _ picker: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any] + ) { + if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { + self.image = image + } + self.isPresented = false + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + self.isPresented = false + } +} diff --git a/Projects/UsertInterfaces/DesignSystem/Sources/ImagePicker/ImagePicker.swift b/Projects/UsertInterfaces/DesignSystem/Sources/ImagePicker/ImagePicker.swift new file mode 100644 index 00000000..e4eea08b --- /dev/null +++ b/Projects/UsertInterfaces/DesignSystem/Sources/ImagePicker/ImagePicker.swift @@ -0,0 +1,52 @@ +import SwiftUI +import PhotosUI + +public extension View { + func imagePicker(isShow: Binding, uiImage: Binding) -> some View { + self + .fullScreenCover(isPresented: isShow) { + ImagePicker(configuration: .init(photoLibrary: .shared()), requests: uiImage) + } + } +} + +struct ImagePicker: UIViewControllerRepresentable { + let configuration: PHPickerConfiguration + @Binding var requests: UIImage? + + func makeUIViewController(context: Context) -> PHPickerViewController { + let controller = PHPickerViewController(configuration: configuration) + controller.delegate = context.coordinator + return controller + } + + func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) { } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: PHPickerViewControllerDelegate { + var parent: ImagePicker + + init(_ parent: ImagePicker) { + self.parent = parent + } + + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + for result in results { + let provider = result.itemProvider + if provider.canLoadObject(ofClass: UIImage.self) { + provider.loadObject(ofClass: UIImage.self) { [weak self] image, _ in + if let image = image as? UIImage { + DispatchQueue.main.async { + self?.parent.requests = image + } + } + } + } + } + picker.dismiss(animated: true) + } + } +}