Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 = "<group>"; };
7110A6032AA337DD009A7E99 /* Runnect-iOSDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Runnect-iOSDebug.entitlements"; sourceTree = "<group>"; };
712F661C2A7B7BAB00D9539B /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = "<group>"; };
7136BF892AF921A900679364 /* CustomBottomSheetVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomBottomSheetVC.swift; sourceTree = "<group>"; };
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 = "<group>"; };
A3305A96296EF58C000B1A10 /* GoalRewardInfoDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalRewardInfoDto.swift; sourceTree = "<group>"; };
A3BC2F2A2962C3D500198261 /* GoalRewardInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalRewardInfoVC.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -206,7 +207,6 @@
CE102C4929DBAD3D00E23E69 /* AuthInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthInterceptor.swift; sourceTree = "<group>"; };
CE14676F296568DC00DCEA1B /* RunTrackingVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunTrackingVC.swift; sourceTree = "<group>"; };
CE14677729658C7200DCEA1B /* Stopwatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stopwatch.swift; sourceTree = "<group>"; };
CE1467792965A80700DCEA1B /* CustomBottomSheetVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomBottomSheetVC.swift; sourceTree = "<group>"; };
CE14677B2965C1B100DCEA1B /* RunningRecordVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunningRecordVC.swift; sourceTree = "<group>"; };
CE15F5A3296C932E0023827C /* RunningModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunningModel.swift; sourceTree = "<group>"; };
CE17F02C2961BBA100E1DED0 /* ColorLiterals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorLiterals.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
280 changes: 273 additions & 7 deletions Runnect-iOS/Runnect-iOS/Global/UIComponents/CustomBottomSheetVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Void> {
completeButton.publisher(for: .touchUpInside)
.map { _ in }
.asDriver()
}
// 바텀 시트 높이
let bottomHeight: CGFloat = 206

// MARK: - UI Components

Expand All @@ -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()
}
Comment on lines +94 to +97
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

질문 ! 두 함수를 textField 타입일때만 추가해준 이유가 있나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.image 같은 경우 아직 사용한 적이 없는데,
원래 사용하려던 목적은
러닝 완료 후 -> 러닝 종료 화면이 뜨면 완료 버튼 말고는 못 누르는 로직이었던 것 같은데,, 예전에?

일단은 저희가 BottomSheet만 사용하기 때문에 따로 빼서 관리를 해주었습니다!

}

}

// MARK: - Methods
Expand All @@ -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)

Expand All @@ -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..<index]
self.bottomSheetTextField.text = String(newString)
self.showToast(message: "20자가 넘어갑니다")
}
}

// UITapGestureRecognizer 연결 함수 부분
@objc private func dimmedViewTapped(_ tapRecognizer: UITapGestureRecognizer) {
hideBottomSheetAndGoBack()
}

// UISwipeGestureRecognizer 연결 함수 부분
@objc func panGesture(_ recognizer: UISwipeGestureRecognizer) {
if recognizer.state == .ended {
switch recognizer.direction {
case .down:
hideBottomSheetAndGoBack()
default:
break
}
}
}

}

// MARK: - UITextFieldDelegate

extension CustomBottomSheetVC: UITextFieldDelegate {

func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}

private func changeTextFieldLayerColor(_ isEditing: Bool) {
bottomSheetTextField.layer.borderColor = isEditing ? UIColor.m1.cgColor : UIColor.g3.cgColor
}
}