diff --git a/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj b/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj index e74cadf1..9dd7465a 100644 --- a/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj +++ b/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj @@ -18,6 +18,9 @@ A3BC2F3D296468E500198261 /* UploadedCourseInfoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BC2F3C296468E500198261 /* UploadedCourseInfoModel.swift */; }; A3BC2F3F2964706100198261 /* UploadedCourseInfoCVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BC2F3E2964706100198261 /* UploadedCourseInfoCVC.swift */; }; A3BC2F4129667A0D00198261 /* NicknameEditorVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BC2F4029667A0D00198261 /* NicknameEditorVC.swift */; }; + CE0C23742966D62A00B45063 /* PagedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0C23732966D62A00B45063 /* PagedView.swift */; }; + CE0C23772966D64D00B45063 /* PageCVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0C23762966D64D00B45063 /* PageCVC.swift */; }; + CE0C23792966D6AF00B45063 /* ViewPager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0C23782966D6AF00B45063 /* ViewPager.swift */; }; CE0D9FD329648DA300CEB5CD /* CustomAlertVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0D9FD229648DA300CEB5CD /* CustomAlertVC.swift */; }; CE146770296568DC00DCEA1B /* RunTrackingVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE14676F296568DC00DCEA1B /* RunTrackingVC.swift */; }; CE14677829658C7200DCEA1B /* Stopwatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE14677729658C7200DCEA1B /* Stopwatch.swift */; }; @@ -79,6 +82,10 @@ CE665610295D92C200C64E12 /* setTextLineHeight.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE66560F295D92C200C64E12 /* setTextLineHeight.swift */; }; CE665612295D92E400C64E12 /* UserDefaultWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE665611295D92E400C64E12 /* UserDefaultWrapper.swift */; }; CE665615295D989A00C64E12 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = CE665614295D989A00C64E12 /* .swiftlint.yml */; }; + CE6B63D02967230D003F900F /* PrivateCourseListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6B63CF2967230D003F900F /* PrivateCourseListView.swift */; }; + CE6B63D3296725E6003F900F /* CourseListCVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6B63D2296725E6003F900F /* CourseListCVC.swift */; }; + CE6B63D6296731F9003F900F /* ScrapCourseListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6B63D5296731F9003F900F /* ScrapCourseListView.swift */; }; + CE6B63D829673450003F900F /* ListEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6B63D729673450003F900F /* ListEmptyView.swift */; }; CE9291252965C9FB0010959C /* CourseDetailInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9291242965C9FB0010959C /* CourseDetailInfoView.swift */; }; CE9291272965D0ED0010959C /* StatsInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9291262965D0ED0010959C /* StatsInfoView.swift */; }; CE9291292965E01D0010959C /* RNTimeFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9291282965E01D0010959C /* RNTimeFormatter.swift */; }; @@ -119,6 +126,9 @@ A3BC2F3C296468E500198261 /* UploadedCourseInfoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadedCourseInfoModel.swift; sourceTree = ""; }; A3BC2F3E2964706100198261 /* UploadedCourseInfoCVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadedCourseInfoCVC.swift; sourceTree = ""; }; A3BC2F4029667A0D00198261 /* NicknameEditorVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NicknameEditorVC.swift; sourceTree = ""; }; + CE0C23732966D62A00B45063 /* PagedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagedView.swift; sourceTree = ""; }; + CE0C23762966D64D00B45063 /* PageCVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageCVC.swift; sourceTree = ""; }; + CE0C23782966D6AF00B45063 /* ViewPager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewPager.swift; sourceTree = ""; }; CE0D9FD229648DA300CEB5CD /* CustomAlertVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlertVC.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 = ""; }; @@ -187,6 +197,10 @@ CE66560F295D92C200C64E12 /* setTextLineHeight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = setTextLineHeight.swift; sourceTree = ""; }; CE665611295D92E400C64E12 /* UserDefaultWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultWrapper.swift; sourceTree = ""; }; CE665614295D989A00C64E12 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; + CE6B63CF2967230D003F900F /* PrivateCourseListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateCourseListView.swift; sourceTree = ""; }; + CE6B63D2296725E6003F900F /* CourseListCVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseListCVC.swift; sourceTree = ""; }; + CE6B63D5296731F9003F900F /* ScrapCourseListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrapCourseListView.swift; sourceTree = ""; }; + CE6B63D729673450003F900F /* ListEmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListEmptyView.swift; sourceTree = ""; }; CE9291242965C9FB0010959C /* CourseDetailInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDetailInfoView.swift; sourceTree = ""; }; CE9291262965D0ED0010959C /* StatsInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsInfoView.swift; sourceTree = ""; }; CE9291282965E01D0010959C /* RNTimeFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNTimeFormatter.swift; sourceTree = ""; }; @@ -203,7 +217,6 @@ CEEC6B3B2961C51A00D00E1E /* CourseStorageVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseStorageVC.swift; sourceTree = ""; }; CEEC6B3D2961C53700D00E1E /* CourseDiscoveryVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDiscoveryVC.swift; sourceTree = ""; }; CEEC6B3F2961C55000D00E1E /* MyPageVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageVC.swift; sourceTree = ""; }; - CEEC6B422961C59600D00E1E /* .gitkeep */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitkeep; sourceTree = ""; }; CEEC6B432961C59F00D00E1E /* .gitkeep */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitkeep; sourceTree = ""; }; CEEC6B442961C5A800D00E1E /* .gitkeep */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitkeep; sourceTree = ""; }; CEEC6B452961C5B200D00E1E /* .gitkeep */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitkeep; sourceTree = ""; }; @@ -289,19 +302,47 @@ path = UploadedCourseInfoCollectionView; sourceTree = ""; }; - CE14676C296568C000DCEA1B /* RunTracking */ = { + CE0C23712966D5FF00B45063 /* ViewPager */ = { + isa = PBXGroup; + children = ( + CE0C23782966D6AF00B45063 /* ViewPager.swift */, + CE0C23722966D62200B45063 /* PagedView */, + ); + path = ViewPager; + sourceTree = ""; + }; + CE0C23722966D62200B45063 /* PagedView */ = { + isa = PBXGroup; + children = ( + CE0C23752966D63C00B45063 /* CVC */, + CE0C23732966D62A00B45063 /* PagedView.swift */, + ); + path = PagedView; + sourceTree = ""; + }; + CE0C23752966D63C00B45063 /* CVC */ = { + isa = PBXGroup; + children = ( + CE0C23762966D64D00B45063 /* PageCVC.swift */, + ); + path = CVC; + sourceTree = ""; + }; + CE14676C296568C000DCEA1B /* Running */ = { isa = PBXGroup; children = ( CE14676E296568CD00DCEA1B /* Views */, CE14676D296568CA00DCEA1B /* VC */, ); - path = RunTracking; + path = Running; sourceTree = ""; }; CE14676D296568CA00DCEA1B /* VC */ = { isa = PBXGroup; children = ( + CEB8416F2963360800BF8080 /* CountDownVC.swift */, CE14676F296568DC00DCEA1B /* RunTrackingVC.swift */, + CE14677B2965C1B100DCEA1B /* RunningRecordVC.swift */, ); path = VC; sourceTree = ""; @@ -384,8 +425,6 @@ CEEC6B392961C4F300D00E1E /* CourseDrawingHomeVC.swift */, CEC2A6912962BE2900160BF7 /* DepartureSearchVC.swift */, CE29D581296402B500F47542 /* CourseDrawingVC.swift */, - CEB8416F2963360800BF8080 /* CountDownVC.swift */, - CE14677B2965C1B100DCEA1B /* RunningRecordVC.swift */, ); path = VC; sourceTree = ""; @@ -409,7 +448,8 @@ CE17F0422961C3D300E1DED0 /* Views */ = { isa = PBXGroup; children = ( - CEEC6B422961C59600D00E1E /* .gitkeep */, + CE6B63D4296731D8003F900F /* CourseListView */, + CE6B63D1296725BD003F900F /* CVC */, ); path = Views; sourceTree = ""; @@ -510,7 +550,7 @@ CE17F03C2961C32C00E1DED0 /* CourseDiscovery */, CE17F03B2961C2F700E1DED0 /* MyPage */, CE17F03E2961C38100E1DED0 /* CourseDetail */, - CE14676C296568C000DCEA1B /* RunTracking */, + CE14676C296568C000DCEA1B /* Running */, ); path = Presentation; sourceTree = ""; @@ -661,6 +701,7 @@ CE6655B6295D803C00C64E12 /* UIComponents */ = { isa = PBXGroup; children = ( + CE0C23712966D5FF00B45063 /* ViewPager */, CEC2A6882962ADB900160BF7 /* MapView */, CEEC6B4A2961D89700D00E1E /* CustomNavigationBar.swift */, CEC2A6842961F92C00160BF7 /* CustomButton.swift */, @@ -668,6 +709,7 @@ CE1467792965A80700DCEA1B /* CustomBottomSheetVC.swift */, CE9291242965C9FB0010959C /* CourseDetailInfoView.swift */, CE9291262965D0ED0010959C /* StatsInfoView.swift */, + CE6B63D729673450003F900F /* ListEmptyView.swift */, ); path = UIComponents; sourceTree = ""; @@ -746,6 +788,23 @@ path = "UIKit+"; sourceTree = ""; }; + CE6B63D1296725BD003F900F /* CVC */ = { + isa = PBXGroup; + children = ( + CE6B63D2296725E6003F900F /* CourseListCVC.swift */, + ); + path = CVC; + sourceTree = ""; + }; + CE6B63D4296731D8003F900F /* CourseListView */ = { + isa = PBXGroup; + children = ( + CE6B63CF2967230D003F900F /* PrivateCourseListView.swift */, + CE6B63D5296731F9003F900F /* ScrapCourseListView.swift */, + ); + path = CourseListView; + sourceTree = ""; + }; CEC2A6882962ADB900160BF7 /* MapView */ = { isa = PBXGroup; children = ( @@ -932,6 +991,8 @@ DA20D847296697A600F1581F /* PlusDetailViewController.swift in Sources */, CE66560E295D92A500C64E12 /* setStatusBarBackgroundColor.swift in Sources */, CE9291292965E01D0010959C /* RNTimeFormatter.swift in Sources */, + CE0C23792966D6AF00B45063 /* ViewPager.swift in Sources */, + CE6B63D02967230D003F900F /* PrivateCourseListView.swift in Sources */, CE6655D7295D86F900C64E12 /* String+.swift in Sources */, CE58759E29601476005D967E /* LoadingIndicator.swift in Sources */, CE5875A2296015A2005D967E /* NetworkLoggerPlugin.swift in Sources */, @@ -943,13 +1004,16 @@ CE6655E0295D87D200C64E12 /* UIDevice+.swift in Sources */, CE17F0382961BF8B00E1DED0 /* FontLiterals.swift in Sources */, CE6655E8295D889600C64E12 /* UISwitch+.swift in Sources */, + CE0C23772966D64D00B45063 /* PageCVC.swift in Sources */, CE5875A029601500005D967E /* Toast.swift in Sources */, CE14677C2965C1B100DCEA1B /* RunningRecordVC.swift in Sources */, + CE6B63D829673450003F900F /* ListEmptyView.swift in Sources */, CE6655F6295D90B600C64E12 /* addToolBar.swift in Sources */, CEC2A68A2962ADCD00160BF7 /* RNMapView.swift in Sources */, CE6655F0295D891B00C64E12 /* UITextView+.swift in Sources */, CEC2A6922962BE2900160BF7 /* DepartureSearchVC.swift in Sources */, CE6655EE295D88E600C64E12 /* UITextField+.swift in Sources */, + CE6B63D6296731F9003F900F /* ScrapCourseListView.swift in Sources */, DA20D841296696C300F1581F /* MapCollectionViewCell.swift in Sources */, CE6655F8295D90CF00C64E12 /* adjusted+.swift in Sources */, CE4545CB295D7AF4003201E1 /* SceneDelegate.swift in Sources */, @@ -967,6 +1031,7 @@ CEEC6B3A2961C4F300D00E1E /* CourseDrawingHomeVC.swift in Sources */, CEC2A6902962B06C00160BF7 /* convertLocationObject.swift in Sources */, CEC2A6852961F92C00160BF7 /* CustomButton.swift in Sources */, + CE6B63D3296725E6003F900F /* CourseListCVC.swift in Sources */, CE29D584296416D800F47542 /* caculateStatusBarHeight.swift in Sources */, CE66560C295D928300C64E12 /* setRootViewController.swift in Sources */, CE6655D9295D871B00C64E12 /* URL+.swift in Sources */, @@ -985,6 +1050,7 @@ DA20D8432966977D00F1581F /* SearchResultViewController.swift in Sources */, CE5875A4296015D2005D967E /* Encodable+.swift in Sources */, A3BC2F4129667A0D00198261 /* NicknameEditorVC.swift in Sources */, + CE0C23742966D62A00B45063 /* PagedView.swift in Sources */, CE14677A2965A80700DCEA1B /* CustomBottomSheetVC.swift in Sources */, CEEC6B4B2961D89700D00E1E /* CustomNavigationBar.swift in Sources */, CE17F02D2961BBA100E1DED0 /* ColorLiterals.swift in Sources */, diff --git a/Runnect-iOS/Runnect-iOS/Global/UIComponents/ListEmptyView.swift b/Runnect-iOS/Runnect-iOS/Global/UIComponents/ListEmptyView.swift new file mode 100644 index 00000000..28f7544e --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/UIComponents/ListEmptyView.swift @@ -0,0 +1,96 @@ +// +// ListEmptyView.swift +// Runnect-iOS +// +// Created by sejin on 2023/01/06. +// + +import UIKit + +protocol ListEmptyViewDelegate: AnyObject { + func emptyViewButtonTapped() +} + +final class ListEmptyView: UIView { + + // MARK: - Properties + + weak var delegate: ListEmptyViewDelegate? + + // MARK: - UI Components + + private let mainImageView = UIImageView().then { + $0.image = ImageLiterals.imgStorage + $0.clipsToBounds = true + } + + private let descriptionLabel = UILabel().then { + $0.font = .b4 + $0.textColor = .g2 + $0.numberOfLines = 0 + $0.textAlignment = .center + } + + private let bottomButton = CustomButton(title: "코스 그리기") + + private lazy var containerStackView = UIStackView( + arrangedSubviews: [mainImageView, descriptionLabel, bottomButton] + ).then { + $0.axis = .vertical + $0.alignment = .center + $0.spacing = 22 + } + + // MARK: - initialization + + init(description: String, buttonTitle: String) { + super.init(frame: .zero) + self.setUI(description: description, buttonTitle: buttonTitle) + self.setLayout() + self.setAddTarget() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - Methods + +extension ListEmptyView { + private func setAddTarget() { + bottomButton.addTarget(self, action: #selector(bottomButtonDidTap), for: .touchUpInside) + } +} + +// MARK: - @objc Function + +extension ListEmptyView { + @objc private func bottomButtonDidTap() { + delegate?.emptyViewButtonTapped() + } +} + +// MARK: - UI & Layout + +extension ListEmptyView { + private func setUI(description: String, buttonTitle: String) { + self.backgroundColor = .clear + + self.descriptionLabel.text = description + self.bottomButton.titleLabel?.text = buttonTitle + } + + private func setLayout() { + self.addSubviews(containerStackView) + + bottomButton.snp.makeConstraints { make in + make.height.equalTo(40) + make.leading.trailing.equalToSuperview() + } + + containerStackView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } +} diff --git a/Runnect-iOS/Runnect-iOS/Global/UIComponents/ViewPager/PagedView/CVC/PageCVC.swift b/Runnect-iOS/Runnect-iOS/Global/UIComponents/ViewPager/PagedView/CVC/PageCVC.swift new file mode 100644 index 00000000..be961b81 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/UIComponents/ViewPager/PagedView/CVC/PageCVC.swift @@ -0,0 +1,42 @@ +// +// PageCVC.swift +// Runnect-iOS +// +// Created by sejin on 2023/01/05. +// + +import UIKit + +final class PageCVC: UICollectionViewCell { + + // MARK: - UI Components + + public var view: UIView? { + didSet { + self.setLayout() + } + } + + // MARK: - Initialization + + override init(frame: CGRect) { + super.init(frame: frame) + self.setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - UI & Layout + + private func setLayout() { + guard let view = view else { return } + + self.contentView.addSubview(view) + + view.snp.makeConstraints { make in + make.edges.equalTo(contentView) + } + } +} diff --git a/Runnect-iOS/Runnect-iOS/Global/UIComponents/ViewPager/PagedView/PagedView.swift b/Runnect-iOS/Runnect-iOS/Global/UIComponents/ViewPager/PagedView/PagedView.swift new file mode 100644 index 00000000..8a6f46a6 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/UIComponents/ViewPager/PagedView/PagedView.swift @@ -0,0 +1,116 @@ +// +// PagedView.swift +// Runnect-iOS +// +// Created by sejin on 2023/01/05. +// + +import UIKit +import Combine + +final class PagedView: UIView { + + // MARK: - Properties + + var pages: [UIView] { + didSet { + self.collectionView.reloadData() + } + } + var movedPage = PassthroughSubject() + var percent = PassthroughSubject() + + // MARK: - UI Components + + private lazy var collectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + let collectionView = UICollectionView( + frame: .zero, + collectionViewLayout: layout + ) + collectionView.isScrollEnabled = true + collectionView.showsHorizontalScrollIndicator = false + collectionView.isPagingEnabled = true + collectionView.register(PageCVC.self, forCellWithReuseIdentifier: PageCVC.className) + collectionView.delegate = self + collectionView.dataSource = self + return collectionView + }() + + // MARK: - Initialization + + init(pages: [UIView] = []) { + self.pages = pages + super.init(frame: .zero) + + self.setUI() + self.setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - UI & Layout + private func setUI() { + collectionView.backgroundColor = .white + } + + private func setLayout() { + self.addSubview(collectionView) + + collectionView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } + + // MARK: - Methods + + public func moveToPage(at index: Int) { + self.collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: true) + } +} + +// MARK: - UICollectionViewDelegateFlowLayout + +extension PagedView: UICollectionViewDelegateFlowLayout { + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + + return CGSize(width: self.collectionView.frame.width, + height: self.collectionView.frame.height) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return 0 + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + let page = Int(self.collectionView.contentOffset.x / self.collectionView.frame.size.width) + self.movedPage.send(page) + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + let offsetX = scrollView.contentOffset.x + let contentSize = scrollView.contentSize.width + + self.percent.send(offsetX / contentSize) + } +} + +// MARK: - UICollectionViewDataSource + +extension PagedView: UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return pages.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PageCVC.className, for: indexPath) as? PageCVC + else { return UICollectionViewCell() } + let page = self.pages[indexPath.item] + cell.view = page + return cell + } +} diff --git a/Runnect-iOS/Runnect-iOS/Global/UIComponents/ViewPager/ViewPager.swift b/Runnect-iOS/Runnect-iOS/Global/UIComponents/ViewPager/ViewPager.swift new file mode 100644 index 00000000..1d1a0beb --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/UIComponents/ViewPager/ViewPager.swift @@ -0,0 +1,177 @@ +// +// ViewPager.swift +// Runnect-iOS +// +// Created by sejin on 2023/01/05. +// + +import UIKit +import Combine + +class ViewPager: UIView { + + // MARK: - Properties + + @Published var selectedTabIndex = 0 + private var cancelBag = CancelBag() + + // 탭이 클릭된 건지 판단하기 위한 변수(스와이프랑 구분하기 위함) + private var tappedButton: Bool = false + + // MARK: - UI Components + + private lazy var buttonStackView = UIStackView().then { + $0.distribution = .fillEqually + } + + private let barBackgroundView = UIView().then { + $0.backgroundColor = .w1 + $0.clipsToBounds = true + } + + private let barView = UIView().then { + $0.backgroundColor = .m1 + } + + private let bottomBorderView = UIView().then { + $0.backgroundColor = .g3 + } + + public let pagedView = PagedView() + + // MARK: - Initialization + + init(pageTitles: [String]) { + super.init(frame: .zero) + self.makeTabbedView(pageTitles: pageTitles) + self.setLayout() + self.bind() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - Methods + +extension ViewPager { + + /// paging할 UIView 추가 + @discardableResult + func addPagedView(pagedView: [UIView]) -> Self { + self.pagedView.pages += pagedView + return self + } + + private func makeTabbedView(pageTitles: [String]) { + for (index, pageTitle) in pageTitles.enumerated() { + let button = UIButton() + button.setAttributedTitle(NSAttributedString(string: pageTitle, attributes: [.font: UIFont.h5, .foregroundColor: UIColor.m1]), for: .selected) + button.setAttributedTitle(NSAttributedString(string: pageTitle, attributes: [.font: UIFont.b3, .foregroundColor: UIColor.g2]), for: .normal) + button.tag = index + button.addTarget(self, action: #selector(tabButtonTapped), for: .touchUpInside) + buttonStackView.addArrangedSubview(button) + } + } + + private func moveBar(index: Int) { + var leadingConstant: CGFloat = 0 + let buttonWidth = self.buttonStackView.arrangedSubviews[0].frame.width + leadingConstant += (buttonWidth * CGFloat(index)) + + UIView.animate(withDuration: 0.4, delay: 0, options: [.curveLinear]) { + self.barView.snp.updateConstraints { make in + make.leading.equalToSuperview().offset(leadingConstant) + } + self.barBackgroundView.layoutIfNeeded() + } + + selectedTabIndex = index + tappedButton = false + } +} + +// MARK: - @objc Function + +extension ViewPager { + @objc private func tabButtonTapped(_ sender: UIButton) { + let index = sender.tag + guard index != selectedTabIndex else { return } // 이미 선택되어 있는 페이지일 경우 이동 X + + tappedButton = true + + moveBar(index: index) + pagedView.moveToPage(at: index) + } +} + +// MARK: - Bind + +extension ViewPager { + private func bind() { + pagedView.movedPage.sink { [weak self] index in + guard let self = self, !self.tappedButton else { return } + self.selectedTabIndex = index + }.store(in: cancelBag) + + pagedView.percent.sink { [weak self] percent in + guard let self = self, !self.tappedButton else { return } + let leadingContraints = self.barBackgroundView.frame.width * percent + self.barView.snp.updateConstraints { make in + make.leading.equalToSuperview().offset(leadingContraints) + } + }.store(in: cancelBag) + + $selectedTabIndex.sink { index in + for (i, tabView) in self.buttonStackView.arrangedSubviews.enumerated() { + guard let button = tabView as? UIButton else { return } + button.isSelected = (i == index) + } + }.store(in: cancelBag) + } +} + +// MARK: - UI & Layout + +extension ViewPager { + private func setUI() { + self.backgroundColor = .w1 + } + + private func setLayout() { + self.addSubviews(buttonStackView, barBackgroundView, pagedView) + barBackgroundView.addSubviews(bottomBorderView, barView) + + buttonStackView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.leading.trailing.equalToSuperview().inset(16) + make.height.equalTo(38) + } + + barBackgroundView.snp.makeConstraints { make in + make.height.equalTo(2) + make.bottom.equalTo(buttonStackView.snp.bottom) + make.leading.trailing.equalToSuperview().inset(16) + } + + bottomBorderView.snp.makeConstraints { make in + make.top.equalTo(barBackgroundView.snp.bottom).inset(1) + make.leading.trailing.equalToSuperview() + make.height.equalTo(1) + } + + guard let button = buttonStackView.arrangedSubviews.first as? UIButton else { return } + + barView.snp.makeConstraints { make in + make.leading.top.bottom.equalToSuperview() + make.width.equalTo(button.snp.width) + } + + pagedView.snp.makeConstraints { make in + make.top.equalTo(barBackgroundView.snp.bottom) + make.leading.trailing.equalToSuperview() + make.bottom.equalToSuperview() + } + } +} diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/VC/CourseStorageVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/VC/CourseStorageVC.swift index cfee89aa..a46da683 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/VC/CourseStorageVC.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/VC/CourseStorageVC.swift @@ -6,11 +6,67 @@ // import UIKit +import Combine final class CourseStorageVC: UIViewController { - + + // MARK: - Properties + + private let cancelBag = CancelBag() + + // MARK: - UI Components + + private lazy var naviBar = CustomNavigationBar(self, type: .title).setTitle("보관함") + + private let privateCourseListView = PrivateCourseListView() + + private let scrapCourseListView = ScrapCourseListView() + + private lazy var viewPager = ViewPager(pageTitles: ["내가 그린 코스", "스크랩 코스"]) + .addPagedView(pagedView: [privateCourseListView, scrapCourseListView]) + + // MARK: - View Life Cycle + override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .g2 + self.setUI() + self.setLayout() + self.bindUI() + } +} + +// MARK: - Methods + +extension CourseStorageVC { + private func bindUI() { + privateCourseListView.courseDrawButtonTapped.sink { + self.tabBarController?.selectedIndex = 0 + }.store(in: cancelBag) + + scrapCourseListView.scrapButtonTapped.sink { + self.tabBarController?.selectedIndex = 2 + }.store(in: cancelBag) + } +} + +// MARK: - UI & Layout + +extension CourseStorageVC { + private func setUI() { + view.backgroundColor = .w1 + } + + private func setLayout() { + view.addSubviews(naviBar, viewPager) + + naviBar.snp.makeConstraints { make in + make.leading.top.trailing.equalTo(view.safeAreaLayoutGuide) + make.height.equalTo(48) + } + + viewPager.snp.makeConstraints { make in + make.top.equalTo(naviBar.snp.bottom) + make.leading.bottom.trailing.equalTo(view.safeAreaLayoutGuide) + } } } diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/.gitkeep b/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/CVC/CourseListCVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/CVC/CourseListCVC.swift new file mode 100644 index 00000000..8dca7b92 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/CVC/CourseListCVC.swift @@ -0,0 +1,157 @@ +// +// CourseListCVC.swift +// Runnect-iOS +// +// Created by sejin on 2023/01/06. +// + +import UIKit + +protocol CourseListCVCDeleagte: AnyObject { + func likeButtonTapped(wantsTolike: Bool) +} + +@frozen +enum CourseListCVCType { + case title + case titleWithLocation + case all + + static func getCellHeight(type: CourseListCVCType, cellWidth: CGFloat) -> CGFloat { + let imageHeight = cellWidth * (124/174) + switch type { + case .title: + return imageHeight + 24 + case .titleWithLocation, .all: + return imageHeight + 40 + } + } +} + +final class CourseListCVC: UICollectionViewCell { + + // MARK: - Properties + + weak var delegate: CourseListCVCDeleagte? + + // MARK: - UI Components + + private let courseImageView = UIImageView().then { + $0.backgroundColor = .g3 + $0.contentMode = .scaleToFill + $0.layer.cornerRadius = 5 + } + + private let titleLabel = UILabel().then { + $0.text = "제목" + $0.font = .b4 + $0.textColor = .g1 + } + + private let locationLabel = UILabel().then { + $0.text = "위치" + $0.font = .b6 + $0.textColor = .g2 + } + + private lazy var labelStackView = UIStackView( + arrangedSubviews: [titleLabel, locationLabel] + ).then { + $0.axis = .vertical + $0.alignment = .leading + } + + private let likeButton = UIButton(type: .custom).then { + $0.setImage(ImageLiterals.icHeartFill, for: .selected) + $0.setImage(ImageLiterals.icHeart, for: .normal) + $0.isSelected = true + $0.backgroundColor = .w1 + } + + // MARK: - initialization + + override init(frame: CGRect) { + super.init(frame: frame) + self.setUI() + self.setLayout() + self.setAddTarget() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - Methods + +extension CourseListCVC { + private func setAddTarget() { + likeButton.addTarget(self, action: #selector(likeButtonDidTap), for: .touchUpInside) + } + + func setData(imageURL: String, title: String, location: String?, didLike: Bool?) { + self.courseImageView.setImage(with: imageURL) + self.titleLabel.text = title + + if let location = location { + self.locationLabel.text = location + } + + if let didLike = didLike { + self.likeButton.isSelected = didLike + } + } +} + +// MARK: - @objc Function + +extension CourseListCVC { + @objc func likeButtonDidTap(_ sender: UIButton) { + sender.isSelected.toggle() + delegate?.likeButtonTapped(wantsTolike: (sender.isSelected == true)) + } +} + +// MARK: - UI & Layout + +extension CourseListCVC { + private func setUI() { + self.contentView.backgroundColor = .w1 + } + + private func setLayout() { + self.contentView.addSubviews(courseImageView, labelStackView, likeButton) + + courseImageView.snp.makeConstraints { make in + make.leading.top.trailing.equalToSuperview() + let imageHeight = contentView.frame.width * (124/174) + make.height.equalTo(imageHeight) + } + + likeButton.snp.makeConstraints { make in + make.top.equalTo(courseImageView.snp.bottom).offset(7) + make.trailing.equalToSuperview() + make.width.height.equalTo(14) + } + + labelStackView.snp.makeConstraints { make in + make.top.equalTo(courseImageView.snp.bottom).offset(4) + make.leading.equalToSuperview() + make.width.equalTo(courseImageView.snp.width).multipliedBy(0.7) + } + } + + func setCellType(type: CourseListCVCType) { + switch type { + case .title: + self.locationLabel.isHidden = true + self.likeButton.isHidden = true + case .titleWithLocation: + self.locationLabel.isHidden = false + self.likeButton.isHidden = true + case .all: + self.locationLabel.isHidden = false + self.likeButton.isHidden = false + } + } +} diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/CourseListView/PrivateCourseListView.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/CourseListView/PrivateCourseListView.swift new file mode 100644 index 00000000..9f5069c4 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/CourseListView/PrivateCourseListView.swift @@ -0,0 +1,137 @@ +// +// PrivateCourseListView.swift +// Runnect-iOS +// +// Created by sejin on 2023/01/06. +// + +import UIKit +import Combine + +final class PrivateCourseListView: UIView { + + // MARK: - Properties + + var courseDrawButtonTapped = PassthroughSubject() + + final let collectionViewInset = UIEdgeInsets(top: 28, left: 16, bottom: 28, right: 16) + final let itemSpacing: CGFloat = 10 + final let lineSpacing: CGFloat = 20 + + // MARK: - UI Components + + private let collectionViewLayout = UICollectionViewFlowLayout().then { + $0.scrollDirection = .vertical + } + + private lazy var courseListCollectionView = UICollectionView( + frame: .zero, + collectionViewLayout: collectionViewLayout + ).then { + $0.backgroundColor = .clear + } + + private let emptyView = ListEmptyView(description: "아직 내가 그린코스가 없어요\n직접 코스를 그려주세요", + buttonTitle: "코스 그리기") + + // MARK: - initialization + + init() { + super.init(frame: .zero) + self.setUI() + self.setLayout() + self.setDelegate() + self.register() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - Methods + +extension PrivateCourseListView { + private func setDelegate() { + courseListCollectionView.delegate = self + courseListCollectionView.dataSource = self + + emptyView.delegate = self + } + + private func register() { + courseListCollectionView.register(CourseListCVC.self, + forCellWithReuseIdentifier: CourseListCVC.className) + } +} + +// MARK: - UI & Layout + +extension PrivateCourseListView { + private func setUI() { + self.backgroundColor = .w1 + self.emptyView.isHidden = true + } + + private func setLayout() { + self.addSubviews(courseListCollectionView) + courseListCollectionView.addSubviews(emptyView) + + courseListCollectionView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.leading.bottom.trailing.equalToSuperview() + } + + emptyView.snp.makeConstraints { make in + make.center.equalToSuperview() + make.leading.trailing.equalToSuperview().inset(80) + } + } +} + +// MARK: - UICollectionViewDelegate, UICollectionViewDataSource + +extension PrivateCourseListView: UICollectionViewDelegate, UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return 15 + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CourseListCVC.className, + for: indexPath) + as? CourseListCVC else { return UICollectionViewCell() } + cell.setCellType(type: .title) + return cell + } +} + +// MARK: - UICollectionViewDelegateFlowLayout + +extension PrivateCourseListView: UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let cellWidth = (UIScreen.main.bounds.width - (self.itemSpacing + 2*self.collectionViewInset.left)) / 2 + let cellHeight = CourseListCVCType.getCellHeight(type: .title, cellWidth: cellWidth) + + return CGSize(width: cellWidth, height: cellHeight) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + return self.collectionViewInset + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return self.itemSpacing + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return self.lineSpacing + } +} + +// MARK: - Section Heading + +extension PrivateCourseListView: ListEmptyViewDelegate { + func emptyViewButtonTapped() { + self.courseDrawButtonTapped.send() + } +} diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/CourseListView/ScrapCourseListView.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/CourseListView/ScrapCourseListView.swift new file mode 100644 index 00000000..ce744c2c --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseStorage/Views/CourseListView/ScrapCourseListView.swift @@ -0,0 +1,137 @@ +// +// ScrapCourseListView.swift +// Runnect-iOS +// +// Created by sejin on 2023/01/06. +// + +import UIKit +import Combine + +final class ScrapCourseListView: UIView { + + // MARK: - Properties + + var scrapButtonTapped = PassthroughSubject() + + final let collectionViewInset = UIEdgeInsets(top: 28, left: 16, bottom: 28, right: 16) + final let itemSpacing: CGFloat = 10 + final let lineSpacing: CGFloat = 20 + + // MARK: - UI Components + + private let collectionViewLayout = UICollectionViewFlowLayout().then { + $0.scrollDirection = .vertical + } + + private lazy var courseListCollectionView = UICollectionView( + frame: .zero, + collectionViewLayout: collectionViewLayout + ).then { + $0.backgroundColor = .clear + } + + private let emptyView = ListEmptyView(description: "아직 스크랩한 코스가 없어요\n코스를 스크랩 해주세요", + buttonTitle: "스크랩 하기") + + // MARK: - initialization + + init() { + super.init(frame: .zero) + self.setUI() + self.setLayout() + self.setDelegate() + self.register() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - Methods + +extension ScrapCourseListView { + private func setDelegate() { + courseListCollectionView.delegate = self + courseListCollectionView.dataSource = self + + emptyView.delegate = self + } + + private func register() { + courseListCollectionView.register(CourseListCVC.self, + forCellWithReuseIdentifier: CourseListCVC.className) + } +} + +// MARK: - UI & Layout + +extension ScrapCourseListView { + private func setUI() { + self.backgroundColor = .w1 + self.emptyView.isHidden = true + } + + private func setLayout() { + self.addSubviews(courseListCollectionView) + courseListCollectionView.addSubviews(emptyView) + + courseListCollectionView.snp.makeConstraints { make in + make.top.equalToSuperview() + make.leading.bottom.trailing.equalToSuperview() + } + + emptyView.snp.makeConstraints { make in + make.center.equalToSuperview() + make.leading.trailing.equalToSuperview().inset(80) + } + } +} + +// MARK: - UICollectionViewDelegate, UICollectionViewDataSource + +extension ScrapCourseListView: UICollectionViewDelegate, UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return 15 + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CourseListCVC.className, + for: indexPath) + as? CourseListCVC else { return UICollectionViewCell() } + cell.setCellType(type: .all) + return cell + } +} + +// MARK: - UICollectionViewDelegateFlowLayout + +extension ScrapCourseListView: UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let cellWidth = (UIScreen.main.bounds.width - (self.itemSpacing + 2*self.collectionViewInset.left)) / 2 + let cellHeight = CourseListCVCType.getCellHeight(type: .all, cellWidth: cellWidth) + + return CGSize(width: cellWidth, height: cellHeight) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + return self.collectionViewInset + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return self.itemSpacing + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return self.lineSpacing + } +} + +// MARK: - Section Heading + +extension ScrapCourseListView: ListEmptyViewDelegate { + func emptyViewButtonTapped() { + self.scrapButtonTapped.send() + } +} diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/CountDownVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/Running/VC/CountDownVC.swift similarity index 100% rename from Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/CountDownVC.swift rename to Runnect-iOS/Runnect-iOS/Presentation/Running/VC/CountDownVC.swift diff --git a/Runnect-iOS/Runnect-iOS/Presentation/RunTracking/VC/RunTrackingVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/Running/VC/RunTrackingVC.swift similarity index 100% rename from Runnect-iOS/Runnect-iOS/Presentation/RunTracking/VC/RunTrackingVC.swift rename to Runnect-iOS/Runnect-iOS/Presentation/Running/VC/RunTrackingVC.swift diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/RunningRecordVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/Running/VC/RunningRecordVC.swift similarity index 100% rename from Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/RunningRecordVC.swift rename to Runnect-iOS/Runnect-iOS/Presentation/Running/VC/RunningRecordVC.swift