diff --git a/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj b/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj index 47b9d4dc..6360f34f 100644 --- a/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj +++ b/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 0AEBD608F3973389E8E1C6D6 /* Pods_Runnect_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 015778D02D5CDE0838284CD7 /* Pods_Runnect_iOS.framework */; }; 712F661D2A7B7BAB00D9539B /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 712F661C2A7B7BAB00D9539B /* Config.swift */; }; + 7136BF8A2AF921A900679364 /* CustomBottomSheetVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7136BF892AF921A900679364 /* CustomBottomSheetVC.swift */; }; 71DBF23E2ABB255A0013415B /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 71DBF23D2ABB255A0013415B /* GoogleService-Info.plist */; }; A3305A97296EF58C000B1A10 /* GoalRewardInfoDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3305A96296EF58C000B1A10 /* GoalRewardInfoDto.swift */; }; A3BC2F2B2962C3D500198261 /* GoalRewardInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BC2F2A2962C3D500198261 /* GoalRewardInfoVC.swift */; }; @@ -43,7 +44,6 @@ CE102C4A29DBAD3D00E23E69 /* AuthInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE102C4929DBAD3D00E23E69 /* AuthInterceptor.swift */; }; CE146770296568DC00DCEA1B /* RunTrackingVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE14676F296568DC00DCEA1B /* RunTrackingVC.swift */; }; CE14677829658C7200DCEA1B /* Stopwatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE14677729658C7200DCEA1B /* Stopwatch.swift */; }; - CE14677A2965A80700DCEA1B /* CustomBottomSheetVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1467792965A80700DCEA1B /* CustomBottomSheetVC.swift */; }; CE14677C2965C1B100DCEA1B /* RunningRecordVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE14677B2965C1B100DCEA1B /* RunningRecordVC.swift */; }; CE15F5A4296C932E0023827C /* RunningModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE15F5A3296C932E0023827C /* RunningModel.swift */; }; CE17F02D2961BBA100E1DED0 /* ColorLiterals.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE17F02C2961BBA100E1DED0 /* ColorLiterals.swift */; }; @@ -166,6 +166,7 @@ 3C3033C911343B5C57EB68E7 /* Pods-Runnect-iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runnect-iOS.debug.xcconfig"; path = "Target Support Files/Pods-Runnect-iOS/Pods-Runnect-iOS.debug.xcconfig"; sourceTree = ""; }; 7110A6032AA337DD009A7E99 /* Runnect-iOSDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Runnect-iOSDebug.entitlements"; sourceTree = ""; }; 712F661C2A7B7BAB00D9539B /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; + 7136BF892AF921A900679364 /* CustomBottomSheetVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomBottomSheetVC.swift; sourceTree = ""; }; 71DBF23D2ABB255A0013415B /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../../../../Runnect-iOS/Runnect-iOS/Runnect-iOS/GoogleService-Info.plist"; sourceTree = ""; }; A3305A96296EF58C000B1A10 /* GoalRewardInfoDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalRewardInfoDto.swift; sourceTree = ""; }; A3BC2F2A2962C3D500198261 /* GoalRewardInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalRewardInfoVC.swift; sourceTree = ""; }; @@ -206,7 +207,6 @@ CE102C4929DBAD3D00E23E69 /* AuthInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthInterceptor.swift; sourceTree = ""; }; CE14676F296568DC00DCEA1B /* RunTrackingVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunTrackingVC.swift; sourceTree = ""; }; CE14677729658C7200DCEA1B /* Stopwatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stopwatch.swift; sourceTree = ""; }; - CE1467792965A80700DCEA1B /* CustomBottomSheetVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomBottomSheetVC.swift; sourceTree = ""; }; CE14677B2965C1B100DCEA1B /* RunningRecordVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunningRecordVC.swift; sourceTree = ""; }; CE15F5A3296C932E0023827C /* RunningModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunningModel.swift; sourceTree = ""; }; CE17F02C2961BBA100E1DED0 /* ColorLiterals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorLiterals.swift; sourceTree = ""; }; @@ -1042,8 +1042,8 @@ CEC2A6882962ADB900160BF7 /* MapView */, CEEC6B4A2961D89700D00E1E /* CustomNavigationBar.swift */, CEC2A6842961F92C00160BF7 /* CustomButton.swift */, + 7136BF892AF921A900679364 /* CustomBottomSheetVC.swift */, CE0D9FD229648DA300CEB5CD /* CustomAlertVC.swift */, - CE1467792965A80700DCEA1B /* CustomBottomSheetVC.swift */, CE9291242965C9FB0010959C /* CourseDetailInfoView.swift */, CE9291262965D0ED0010959C /* StatsInfoView.swift */, CE6B63D729673450003F900F /* ListEmptyView.swift */, @@ -1378,6 +1378,7 @@ CE14677C2965C1B100DCEA1B /* RunningRecordVC.swift in Sources */, CE6B63D829673450003F900F /* ListEmptyView.swift in Sources */, CE6655F6295D90B600C64E12 /* addToolBar.swift in Sources */, + 7136BF8A2AF921A900679364 /* CustomBottomSheetVC.swift in Sources */, A3C2CAD529E4F85400EC525B /* PersonalInfoVC.swift in Sources */, DAD5A3DA296C6DA500C8166B /* TitleCollectionViewCell.swift in Sources */, CEC2A68A2962ADCD00160BF7 /* RNMapView.swift in Sources */, @@ -1446,7 +1447,6 @@ A3BC2F4129667A0D00198261 /* NicknameEditorVC.swift in Sources */, CE0C23742966D62A00B45063 /* PagedView.swift in Sources */, CE3A53C5296C6017003D518C /* KeychainManager.swift in Sources */, - CE14677A2965A80700DCEA1B /* CustomBottomSheetVC.swift in Sources */, CEEC6B4B2961D89700D00E1E /* CustomNavigationBar.swift in Sources */, CE09037D296E9ED900BEA710 /* ScrapCourseResponseDto.swift in Sources */, CED791B32A2626AF001BFCFB /* ShadowView.swift in Sources */, diff --git a/Runnect-iOS/Runnect-iOS/Global/UIComponents/CustomBottomSheetVC.swift b/Runnect-iOS/Runnect-iOS/Global/UIComponents/CustomBottomSheetVC.swift index ed67043c..42069943 100644 --- a/Runnect-iOS/Runnect-iOS/Global/UIComponents/CustomBottomSheetVC.swift +++ b/Runnect-iOS/Runnect-iOS/Global/UIComponents/CustomBottomSheetVC.swift @@ -2,21 +2,37 @@ // CustomBottomSheetVC.swift // Runnect-iOS // -// Created by sejin on 2023/01/04. +// Created by 이명진 on 11/6/23. // import UIKit import Combine +@frozen +enum SheetType { + case Image // 가운에 이미지가 있는 시트 + case TextField // 가운데 텍스트필드가 있는 시트 +} + final class CustomBottomSheetVC: UIViewController { // MARK: - Properties + private let backgroundView = UIView().then { + $0.backgroundColor = .black.withAlphaComponent(0.65) + } + private let titleNameMaxLength = 20 + private var BottomsheetType: SheetType! + + var backgroundTapAction: (() -> Void)? + var completeButtonTapped: Driver { completeButton.publisher(for: .touchUpInside) .map { _ in } .asDriver() } + // 바텀 시트 높이 + let bottomHeight: CGFloat = 206 // MARK: - UI Components @@ -27,24 +43,60 @@ final class CustomBottomSheetVC: UIViewController { } private let contentsLabel = UILabel().then { - $0.text = "수고하셨습니다! 러닝을 완료했어요!" + $0.text = "코스 이름" $0.font = .h5 - $0.textColor = .g2 + $0.textColor = .g1 + } + + private let dismissIndicatorView = UIView().then { + $0.backgroundColor = .g3 + $0.layer.cornerRadius = 3 } + private let completeButton = CustomButton(title: "완료").setColor(bgColor: .m1, disableColor: .g3).setEnabled(false) + private let mainImageView = UIImageView().then { $0.image = ImageLiterals.imgSpaceship } - private let completeButton = CustomButton(title: "기록 보러 가기") + private lazy var bottomSheetTextField = UITextField().then { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .center + $0.attributedPlaceholder = NSAttributedString(string: "코스의 이름을 입력해 주세요", attributes: [.font: UIFont.h5, .foregroundColor: UIColor.g3, .paragraphStyle: paragraphStyle]) + $0.font = .h5 + $0.textColor = .g1 + $0.textAlignment = .center + $0.layer.cornerRadius = 10 + $0.layer.borderWidth = 1 + $0.layer.borderColor = UIColor.g3.cgColor + $0.addTarget(self, action: #selector(textFieldTextDidChange), for: .editingChanged) + } + + // MARK: - initializtion + init(type: SheetType) { + super.init(nibName: nil, bundle: nil) + self.BottomsheetType = type + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } // MARK: - View Life Cycle override func viewDidLoad() { super.viewDidLoad() self.setUI() - self.setLayout() + self.setLayout(BottomsheetType) + self.setDelegate() + self.setTapGesture() + self.setAddTarget() + if BottomsheetType == .TextField { + showBottomSheet() + setupGestureRecognizer() + } } + } // MARK: - Methods @@ -64,16 +116,68 @@ extension CustomBottomSheetVC { self.completeButton.changeTitle(attributedString: title) return self } + + /// 이미지 교체 + @discardableResult + public func setImage(_ image: UIImage) -> Self { + self.mainImageView.image = image + return self + } + + private func setDelegate() { + bottomSheetTextField.delegate = self + } + + private func dismissBottomSheet() { + UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut, animations: { + self.view.layoutIfNeeded() + }, completion: nil) + } + + // 중복 작업 통일 필요 (1. 배경화면 누를시, 2.스와이프 할시) + private func setTapGesture() { + let tap = UITapGestureRecognizer(target: view, action: #selector(UIView.endEditing)) + tap.cancelsTouchesInView = false + view.addGestureRecognizer(tap) + + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackgroundTap)) + backgroundView.addGestureRecognizer(tapGesture) + } + + private func setAddTarget() { + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardWillShow), + name: UIResponder.keyboardWillShowNotification, + object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardWillHide), + name: UIResponder.keyboardWillHideNotification, + object: nil) + } } // MARK: - UI & Layout extension CustomBottomSheetVC { private func setUI() { - view.backgroundColor = .black.withAlphaComponent(0.8) + view.addSubview(backgroundView) + backgroundView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } } - private func setLayout() { + private func setLayout(_ type: SheetType) { + switch type { + case .TextField: + setTextFieldLayout() + case .Image: + setImageLayout() + } + } + + private func setImageLayout() { view.addSubviews(bottomSheetView) bottomSheetView.addSubviews(contentsLabel, mainImageView, completeButton) @@ -100,4 +204,166 @@ extension CustomBottomSheetVC { make.leading.trailing.equalToSuperview().inset(16) } } + + private func setTextFieldLayout() { + view.addSubviews(bottomSheetView) + + let topConst = view.safeAreaInsets.bottom + view.safeAreaLayoutGuide.layoutFrame.height + + bottomSheetView.addSubviews(contentsLabel, bottomSheetTextField, dismissIndicatorView, completeButton) + + bottomSheetView.snp.makeConstraints { make in + make.leading.bottom.trailing.equalToSuperview() + make.top.equalTo(view.snp.top).offset(topConst) + make.height.equalTo(bottomHeight) + } + + dismissIndicatorView.snp.makeConstraints { make in + make.width.equalTo(102) + make.height.equalTo(7) + make.top.equalTo(bottomSheetView.snp.top).inset(12) + make.centerX.equalToSuperview() + } + + contentsLabel.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.top.equalToSuperview().inset(34) + } + + bottomSheetTextField.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.top.equalTo(contentsLabel.snp.bottom).offset(19) + make.leading.trailing.equalToSuperview().inset(16) + make.height.equalTo(44) + } + + completeButton.snp.makeConstraints { make in + make.top.equalTo(bottomSheetTextField.snp.bottom).offset(10) + make.height.equalTo(44) + make.leading.trailing.equalToSuperview().inset(16) + } + } + + private func showBottomSheet() { + + let safeAreaHeight: CGFloat = view.safeAreaLayoutGuide.layoutFrame.height + let bottomPadding: CGFloat = view.safeAreaInsets.bottom + + let topConst = (safeAreaHeight + bottomPadding) - bottomHeight + + bottomSheetView.snp.remakeConstraints { make in + make.leading.bottom.trailing.equalToSuperview() + make.top.equalTo(view.snp.top).offset(topConst) + make.height.equalTo(bottomHeight) + } + + UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseIn, animations: { + self.backgroundView.alpha = 0.65 + self.view.layoutIfNeeded() + }, completion: nil) + + } + + // 바텀 시트 사라지는 애니메이션 + private func hideBottomSheetAndGoBack() { + let safeAreaHeight = view.safeAreaLayoutGuide.layoutFrame.height + let bottomPadding = view.safeAreaInsets.bottom + + let topConst = (safeAreaHeight + bottomPadding) + + bottomSheetView.snp.remakeConstraints { make in + make.leading.bottom.trailing.equalToSuperview() + make.top.equalTo(view.snp.top).offset(topConst) + make.height.equalTo(bottomHeight) + } + + UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseIn, animations: { + self.backgroundView.alpha = 0.0 + self.view.layoutIfNeeded() + }) { _ in + if self.presentingViewController != nil { + self.dismiss(animated: false, completion: nil) + } + } + } + + // GestureRecognizer 세팅 작업 + private func setupGestureRecognizer() { + // 흐린 부분 탭할 때, 바텀시트를 내리는 TapGesture + let dimmedTap = UITapGestureRecognizer(target: self, action: #selector(dimmedViewTapped(_:))) + backgroundView.addGestureRecognizer(dimmedTap) + backgroundView.isUserInteractionEnabled = true + + // 스와이프 했을 때, 바텀시트를 내리는 swipeGesture + let swipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(panGesture)) + swipeGesture.direction = .down + view.addGestureRecognizer(swipeGesture) + } +} + +// MARK: - @objc Function + +extension CustomBottomSheetVC { + + @objc private func keyboardWillShow(_ sender: Notification) { + self.view.frame.origin.y = -341 + } + + @objc private func keyboardWillHide(_ sender: Notification) { + self.view.frame.origin.y = 0 + } + + @objc private func endEditing() { /// return 누를시 키보드 종료 + bottomSheetTextField.resignFirstResponder() + } + + @objc private func handleBackgroundTap() { + dismissBottomSheet() + } + + @objc private func textFieldTextDidChange() { + guard let text = bottomSheetTextField.text else { return } + + completeButton.isEnabled = !text.isEmpty + changeTextFieldLayerColor(!text.isEmpty) + + if text.count > titleNameMaxLength { + let index = text.index(text.startIndex, offsetBy: titleNameMaxLength) + let newString = text[text.startIndex.. Bool { + textField.resignFirstResponder() + return true + } + + private func changeTextFieldLayerColor(_ isEditing: Bool) { + bottomSheetTextField.layer.borderColor = isEditing ? UIColor.m1.cgColor : UIColor.g3.cgColor + } }