diff --git a/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj b/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj index d51883ff..e74cadf1 100644 --- a/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj +++ b/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 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 */; }; CE17F02D2961BBA100E1DED0 /* ColorLiterals.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE17F02C2961BBA100E1DED0 /* ColorLiterals.swift */; }; CE17F0332961BEF800E1DED0 /* Pretendard-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = CE17F02F2961BEF800E1DED0 /* Pretendard-Medium.otf */; }; CE17F0342961BEF800E1DED0 /* Pretendard-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = CE17F0302961BEF800E1DED0 /* Pretendard-Bold.otf */; }; @@ -78,6 +79,9 @@ 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 */; }; + 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 */; }; CEB8416E2962C45300BF8080 /* LocationSearchResultTVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB8416D2962C45300BF8080 /* LocationSearchResultTVC.swift */; }; CEB841702963360800BF8080 /* CountDownVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB8416F2963360800BF8080 /* CountDownVC.swift */; }; CEC2A6852961F92C00160BF7 /* CustomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC2A6842961F92C00160BF7 /* CustomButton.swift */; }; @@ -119,6 +123,7 @@ 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 = ""; }; CE17F02C2961BBA100E1DED0 /* ColorLiterals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorLiterals.swift; sourceTree = ""; }; CE17F02F2961BEF800E1DED0 /* Pretendard-Medium.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-Medium.otf"; sourceTree = ""; }; CE17F0302961BEF800E1DED0 /* Pretendard-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-Bold.otf"; sourceTree = ""; }; @@ -182,6 +187,9 @@ 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 = ""; }; + 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 = ""; }; CEB8416D2962C45300BF8080 /* LocationSearchResultTVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSearchResultTVC.swift; sourceTree = ""; }; CEB8416F2963360800BF8080 /* CountDownVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountDownVC.swift; sourceTree = ""; }; CEC2A6842961F92C00160BF7 /* CustomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomButton.swift; sourceTree = ""; }; @@ -377,6 +385,7 @@ CEC2A6912962BE2900160BF7 /* DepartureSearchVC.swift */, CE29D581296402B500F47542 /* CourseDrawingVC.swift */, CEB8416F2963360800BF8080 /* CountDownVC.swift */, + CE14677B2965C1B100DCEA1B /* RunningRecordVC.swift */, ); path = VC; sourceTree = ""; @@ -604,6 +613,7 @@ CEC2A68F2962B06C00160BF7 /* convertLocationObject.swift */, CE29D583296416D800F47542 /* caculateStatusBarHeight.swift */, CE14677729658C7200DCEA1B /* Stopwatch.swift */, + CE9291282965E01D0010959C /* RNTimeFormatter.swift */, ); path = Utils; sourceTree = ""; @@ -656,6 +666,8 @@ CEC2A6842961F92C00160BF7 /* CustomButton.swift */, CE0D9FD229648DA300CEB5CD /* CustomAlertVC.swift */, CE1467792965A80700DCEA1B /* CustomBottomSheetVC.swift */, + CE9291242965C9FB0010959C /* CourseDetailInfoView.swift */, + CE9291262965D0ED0010959C /* StatsInfoView.swift */, ); path = UIComponents; sourceTree = ""; @@ -903,6 +915,7 @@ CE665604295D91B100C64E12 /* makeAlert.swift in Sources */, A3BC2F2F2962C40A00198261 /* UploadedCourseInfoVC.swift in Sources */, CE6655EA295D88B200C64E12 /* UITabBar+.swift in Sources */, + CE9291272965D0ED0010959C /* StatsInfoView.swift in Sources */, CEC2A68729629B9B00160BF7 /* SignInVC.swift in Sources */, CE665602295D918000C64E12 /* JsonCoder.swift in Sources */, CE4545CD295D7AF4003201E1 /* TaBarController.swift in Sources */, @@ -918,6 +931,7 @@ CE66560A295D924A00C64E12 /* Result+.swift in Sources */, DA20D847296697A600F1581F /* PlusDetailViewController.swift in Sources */, CE66560E295D92A500C64E12 /* setStatusBarBackgroundColor.swift in Sources */, + CE9291292965E01D0010959C /* RNTimeFormatter.swift in Sources */, CE6655D7295D86F900C64E12 /* String+.swift in Sources */, CE58759E29601476005D967E /* LoadingIndicator.swift in Sources */, CE5875A2296015A2005D967E /* NetworkLoggerPlugin.swift in Sources */, @@ -930,6 +944,7 @@ CE17F0382961BF8B00E1DED0 /* FontLiterals.swift in Sources */, CE6655E8295D889600C64E12 /* UISwitch+.swift in Sources */, CE5875A029601500005D967E /* Toast.swift in Sources */, + CE14677C2965C1B100DCEA1B /* RunningRecordVC.swift in Sources */, CE6655F6295D90B600C64E12 /* addToolBar.swift in Sources */, CEC2A68A2962ADCD00160BF7 /* RNMapView.swift in Sources */, CE6655F0295D891B00C64E12 /* UITextView+.swift in Sources */, @@ -981,6 +996,7 @@ CEB8416E2962C45300BF8080 /* LocationSearchResultTVC.swift in Sources */, CE6655CA295D84DD00C64E12 /* UserDefaultKeyList.swift in Sources */, CE6655F2295D894D00C64E12 /* UIView+.swift in Sources */, + CE9291252965C9FB0010959C /* CourseDetailInfoView.swift in Sources */, CE665600295D915D00C64E12 /* getClassName.swift in Sources */, A3BC2F2D2962C3F200198261 /* ActivityRecordInfoVC.swift in Sources */, CE6655FC295D90F500C64E12 /* calculatePastTime.swift in Sources */, diff --git a/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UITextField+.swift b/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UITextField+.swift index 0eb4d443..e6209888 100644 --- a/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UITextField+.swift +++ b/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UITextField+.swift @@ -34,4 +34,13 @@ extension UITextField { attributedStr.addAttribute(NSAttributedString.Key.kern, value: spacing, range: NSMakeRange(0, attributedStr.length)) self.attributedText = attributedStr } + + /// 하단에 Border 생성 + func addBottomBorder(height: CGFloat) { + let bottomLine = CALayer() + bottomLine.frame = CGRect(x: 0, y: self.frame.size.height - height, width: self.frame.size.width, height: height) + bottomLine.backgroundColor = UIColor.g5.cgColor + borderStyle = .none + layer.addSublayer(bottomLine) + } } diff --git a/Runnect-iOS/Runnect-iOS/Global/UIComponents/CourseDetailInfoView.swift b/Runnect-iOS/Runnect-iOS/Global/UIComponents/CourseDetailInfoView.swift new file mode 100644 index 00000000..0390cc0b --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/UIComponents/CourseDetailInfoView.swift @@ -0,0 +1,79 @@ +// +// CourseDetailInfoView.swift +// Runnect-iOS +// +// Created by sejin on 2023/01/04. +// + +import UIKit + +final class CourseDetailInfoView: UIView { + + // MARK: - UI Components + + private let leftImageView = UIImageView().then { + $0.image = ImageLiterals.icStar + } + + private let titleLabel = UILabel().then { + $0.font = .b5 + $0.textColor = .g1 + } + + private let descriptionLabel = UILabel().then { + $0.font = .b6 + $0.textColor = .g1 + } + + // MARK: - initialization + + init(title: String, description: String) { + super.init(frame: .zero) + self.setUI(title: title, description: description) + self.setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - Methods + +extension CourseDetailInfoView { + @discardableResult + func setDescriptionText(description: String) -> Self { + self.descriptionLabel.text = description + return self + } +} + +// MARK: - UI & Layout + +extension CourseDetailInfoView { + private func setUI(title: String, description: String) { + self.backgroundColor = .w1 + self.titleLabel.text = title + self.descriptionLabel.text = description + } + + private func setLayout() { + self.addSubviews(leftImageView, titleLabel, descriptionLabel) + + leftImageView.snp.makeConstraints { make in + make.top.leading.bottom.equalToSuperview() + make.width.equalTo(leftImageView.snp.height) + } + + titleLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.leading.equalTo(leftImageView.snp.trailing).offset(9) + } + + descriptionLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.leading.equalTo(titleLabel.snp.leading).offset(57) + make.trailing.greaterThanOrEqualToSuperview() + } + } +} diff --git a/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNMapView.swift b/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNMapView.swift index a571f326..55b44a5c 100644 --- a/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNMapView.swift +++ b/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNMapView.swift @@ -19,7 +19,7 @@ final class RNMapView: UIView { @Published var pathDistance: Double = 0 @Published var markerCount = 0 - let pathImage = PassthroughSubject() + let pathImage = PassthroughSubject() var cancelBag = Set() let locationManager = CLLocationManager() @@ -37,6 +37,10 @@ final class RNMapView: UIView { private var bottomPadding: CGFloat = 0 private let locationOverlayIcon = NMFOverlayImage(image: ImageLiterals.icLocationOverlay) + private lazy var dummyMap = RNMapView(frame: CGRect(x: 0, y: 0, width: 300, height: 300)).then { + $0.isUserInteractionEnabled = false + } + // MARK: - UI Components let map = NMFNaverMapView() @@ -137,15 +141,39 @@ extension RNMapView { /// NMGLatLng 어레이를 받아서 첫 위치를 startMarker로 설정하고 나머지를 일반 마커로 생성 @discardableResult - func makeMarkersWithStartMarker(at locations: [NMGLatLng]) -> Self { + func makeMarkersWithStartMarker(at locations: [NMGLatLng], moveCameraToStartMarker: Bool) -> Self { + removeMarkers() if locations.count < 2 { return self } - makeStartMarker(at: locations[0], withCameraMove: true) + makeStartMarker(at: locations[0], withCameraMove: moveCameraToStartMarker) locations[1...].forEach { location in makeMarker(at: location) } + return self } + /// 캡처를 위한 좌표 설정 및 카메라 이동 + func makeDummyMarkerAndCameraMove(at locations: [NMGLatLng]) { + addSubview(dummyMap) + sendSubviewToBack(dummyMap) + dummyMap.makeMarkersWithStartMarker(at: locations, moveCameraToStartMarker: false) + let bounds = makeMBR(at: locations) + let cameraUpdate = NMFCameraUpdate(fit: bounds, padding: 100) + cameraUpdate.animation = .none + dummyMap.map.mapView.moveCamera(cameraUpdate) { isCancelled in + if isCancelled { + print("카메라 이동 취소") + } else { + LoadingIndicator.showLoading() + DispatchQueue.main.asyncAfter(deadline: .now()+2) { + self.dummyMap.map.mapView.zoomLevel -= 1 + self.makePathImage() + LoadingIndicator.hideLoading() + } + } + } + } + /// 사용자 위치로 카메라 이동 @discardableResult func moveToUserLocation() -> Self { @@ -216,27 +244,21 @@ extension RNMapView { return pathDistance } - /// 경로 뷰를 UIImage로 변환하여 pathImage에 send - func getPathImage() { - let bounds = makeMBR() - let dummyMap = RNMapView(frame: CGRect(x: 50, y: 50, width: 300, height: 250)) - .makeMarkersWithStartMarker(at: self.markersLatLngs) - addSubview(dummyMap) - sendSubviewToBack(dummyMap) - let cameraUpdate = NMFCameraUpdate(fit: bounds, padding: 150) - cameraUpdate.animation = .none - dummyMap.map.mapView.moveCamera(cameraUpdate) - - DispatchQueue.main.asyncAfter(deadline: .now()+1) { - self.pathImage.send(UIImage(view: dummyMap.map.mapView)) - } + /// 더미 뷰를 UIImage로 변환하여 pathImage에 send + func makePathImage() { + self.pathImage.send(UIImage(view: dummyMap.map.mapView)) + } + + /// 현재 시점까지의 마커들을 캡쳐하여 pahImage에 send + func capturePathImage() { + makeDummyMarkerAndCameraMove(at: self.markersLatLngs) } /// 바운더리(MBR) 생성 - func makeMBR() -> NMGLatLngBounds { + func makeMBR(at locations: [NMGLatLng]) -> NMGLatLngBounds { var latitudes = [Double]() var longitudes = [Double]() - self.markersLatLngs.forEach { latLng in + locations.forEach { latLng in latitudes.append(latLng.lat) longitudes.append(latLng.lng) } @@ -253,6 +275,13 @@ extension RNMapView { lastMarker.mapView = nil } + /// 출발지 마커를 제외한 모든 마커 제거 + func removeMarkers() { + while self.markers.count != 0 { + undo() + } + } + // 두 지점 사이의 거리(m) 추가 private func addDistance(with newLocation: NMGLatLng) { let lastCLLoc = markersLatLngs.last?.toCLLocation() diff --git a/Runnect-iOS/Runnect-iOS/Global/UIComponents/StatsInfoView.swift b/Runnect-iOS/Runnect-iOS/Global/UIComponents/StatsInfoView.swift new file mode 100644 index 00000000..69295e3a --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/UIComponents/StatsInfoView.swift @@ -0,0 +1,90 @@ +// +// StatsInfoView.swift +// Runnect-iOS +// +// Created by sejin on 2023/01/05. +// + +import UIKit + +final class StatsInfoView: UIView { + + // MARK: - UI Components + + private let titleLabel = UILabel().then { + $0.font = .b4 + $0.textColor = .g2 + } + + private let statsLabel = UILabel().then { + $0.font = .h3 + $0.textColor = .g1 + } + + private lazy var statsContainerStackView = UIStackView( + arrangedSubviews: [titleLabel, statsLabel] + ).then { + $0.axis = .vertical + $0.alignment = .center + $0.spacing = 9 + } + + // MARK: - initialization + + init(title: String, stats: String) { + super.init(frame: .zero) + self.setUI(title: title, stats: stats) + self.setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - Methods + +extension StatsInfoView { + /// statsLabel의 text를 attributedString으로 변경 (기본 값은 Km) + @discardableResult + func setAttributedStats(stats: String, unit: String = " Km") -> Self { + let attributedString = NSMutableAttributedString( + string: stats, + attributes: [.font: UIFont.h3, .foregroundColor: UIColor.g1] + ) + + attributedString.append( + NSAttributedString( + string: unit, + attributes: [.font: UIFont.b4, .foregroundColor: UIColor.g2] + ) + ) + + self.statsLabel.attributedText = attributedString + return self + } + + @discardableResult + func setStats(stats: String) -> Self { + self.statsLabel.text = stats + return self + } +} + +// MARK: - UI & Layout + +extension StatsInfoView { + private func setUI(title: String, stats: String) { + self.backgroundColor = .w1 + self.titleLabel.text = title + self.statsLabel.text = stats + } + + private func setLayout() { + self.addSubviews(statsContainerStackView) + + statsContainerStackView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + } +} diff --git a/Runnect-iOS/Runnect-iOS/Global/Utils/RNTimeFormatter.swift b/Runnect-iOS/Runnect-iOS/Global/Utils/RNTimeFormatter.swift new file mode 100644 index 00000000..30eb9e92 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/Utils/RNTimeFormatter.swift @@ -0,0 +1,33 @@ +// +// RNTimeFormatter.swift +// Runnect-iOS +// +// Created by sejin on 2023/01/05. +// + +import Foundation + +class RNTimeFormatter { + static func secondsToHHMMSS(seconds: Int) -> String { + let interval = seconds + + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.hour, .minute, .second] + formatter.unitsStyle = .positional + formatter.zeroFormattingBehavior = .pad + + let formattedString = formatter.string(from: TimeInterval(interval))! + return formattedString + } + + static func getCurrentTimeToString(date: Date) -> String { + let formatter = DateFormatter() + + formatter.locale = Locale(identifier: "ko_kr") + formatter.timeZone = TimeZone(abbreviation: "KST") + + formatter.dateFormat = "yyyy.MM.dd HH:mm" + + return formatter.string(from: date) + } +} diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/CountDownVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/CountDownVC.swift index 37c00fbe..cf4515aa 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/CountDownVC.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/CountDownVC.swift @@ -15,6 +15,7 @@ final class CountDownVC: UIViewController { private var count = 3 var locations = [NMGLatLng]() var distance: String? + var pathImage: UIImage? // MARK: - UI Components @@ -60,6 +61,7 @@ extension CountDownVC { } else { let runTrackingVC = RunTrackingVC() runTrackingVC.makePath(locations: self.locations, distance: self.distance ?? "0:0") + runTrackingVC.pathImage = self.pathImage self.navigationController?.pushViewController(runTrackingVC, animated: true) // CountDownVC를 navigationController 스택에서 제거하여 pop 하였을 때 더 이전 뷰로 넘어가지도록 함 @@ -69,6 +71,12 @@ extension CountDownVC { } }) } + + func setData(locations: [NMGLatLng], distance: String?, pathImage: UIImage?) { + self.locations = locations + self.distance = distance + self.pathImage = pathImage + } } // MARK: - UI & Layout diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/CourseDrawingVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/CourseDrawingVC.swift index d253778d..6c1d1ee6 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/CourseDrawingVC.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/CourseDrawingVC.swift @@ -11,6 +11,7 @@ import Combine final class CourseDrawingVC: UIViewController { // MARK: - Properties + var pathImage: UIImage? private var cancelBag = CancelBag() @@ -113,36 +114,30 @@ extension CourseDrawingVC { } private func bindMapView() { - mapView.$pathDistance.sink { distance in + mapView.$pathDistance.sink { [weak self] distance in + guard let self = self else { return } let kilometers = String(format: "%.1f", distance/1000) self.distanceLabel.text = kilometers }.store(in: cancelBag) mapView.$markerCount.sink { [weak self] count in - self?.completeButton.setEnabled(count >= 2) - self?.undoButton.isEnabled = (count >= 2) + guard let self = self else { return } + self.completeButton.setEnabled(count >= 2) + self.undoButton.isEnabled = (count >= 2) + }.store(in: cancelBag) + + mapView.pathImage.sink { [weak self] image in + guard let self = self else { return } + self.pathImage = image + self.presentAlertVC() }.store(in: cancelBag) } private func setNavigationGesture(_ enabled: Bool) { navigationController?.interactivePopGestureRecognizer?.isEnabled = enabled } -} - -// MARK: - @objc Function - -extension CourseDrawingVC { - @objc private func decideDepartureButtonDidTap() { - showHiddenViews(withDuration: 0.7) - - mapView.setDrawMode(to: true) - } - - @objc private func undoButtonDidTap() { - mapView.undo() - } - @objc private func completeButtonDidTap() { + private func presentAlertVC() { let alertVC = CustomAlertVC() alertVC.modalPresentationStyle = .overFullScreen @@ -156,8 +151,9 @@ extension CourseDrawingVC { alertVC.rightButtonTapped.sink { [weak self] _ in guard let self = self else { return } let countDownVC = CountDownVC() - countDownVC.locations = self.mapView.getMarkersLatLng() - countDownVC.distance = self.distanceLabel.text + countDownVC.setData(locations: self.mapView.getMarkersLatLng(), + distance: self.distanceLabel.text, + pathImage: self.pathImage) self.navigationController?.pushViewController(countDownVC, animated: true) alertVC.dismiss(animated: true) }.store(in: cancelBag) @@ -166,6 +162,24 @@ extension CourseDrawingVC { } } +// MARK: - @objc Function + +extension CourseDrawingVC { + @objc private func decideDepartureButtonDidTap() { + showHiddenViews(withDuration: 0.7) + + mapView.setDrawMode(to: true) + } + + @objc private func undoButtonDidTap() { + mapView.undo() + } + + @objc private func completeButtonDidTap() { + mapView.capturePathImage() + } +} + // MARK: - UI & Layout extension CourseDrawingVC { diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/RunningRecordVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/RunningRecordVC.swift new file mode 100644 index 00000000..b524114d --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/RunningRecordVC.swift @@ -0,0 +1,258 @@ +// +// RunningRecordVC.swift +// Runnect-iOS +// +// Created by sejin on 2023/01/04. +// + +import UIKit + +final class RunningRecordVC: UIViewController { + + // MARK: - Properties + + private let courseTitleMaxLength = 20 + + // MARK: - UI Components + + private lazy var naviBar = CustomNavigationBar(self, type: .titleWithLeftButton) + .setTitle("러닝 기록") + + private let scrollView = UIScrollView() + + private let contentView = UIView() + + private let courseImageView = UIImageView().then { + $0.backgroundColor = .gray + $0.contentMode = .scaleToFill + } + + private let courseTitleTextField = UITextField().then { + $0.attributedPlaceholder = NSAttributedString( + string: "글 제목", + attributes: [.font: UIFont.h4, .foregroundColor: UIColor.g3] + ) + $0.font = .h4 + $0.textColor = .g1 + $0.addLeftPadding(width: 2) + } + + private let dateInfoView = CourseDetailInfoView(title: "날짜", description: RNTimeFormatter.getCurrentTimeToString(date: Date())) + + private let departureInfoView = CourseDetailInfoView(title: "출발지", description: "출발지 주소") + + private let dividerView = UIView().then { + $0.backgroundColor = .g5 + } + + private let distanceStatsView = StatsInfoView(title: "거리", stats: "0.0 Km").setAttributedStats(stats: "0.0") + private let totalTimeStatsView = StatsInfoView(title: "이동 시간", stats: "00:00:00") + private let averagePaceStatsView = StatsInfoView(title: "평균 페이스", stats: "0'00'") + private let verticalDividerView = UIView().then { + $0.backgroundColor = .g2 + } + private let verticalDividerView2 = UIView().then { + $0.backgroundColor = .g2 + } + + private lazy var statsContainerStackView = UIStackView( + arrangedSubviews: [distanceStatsView, + verticalDividerView, + totalTimeStatsView, + verticalDividerView2, + averagePaceStatsView] + ).then { + $0.spacing = 25 + } + + private let saveButton = CustomButton(title: "저장하기") + .setEnabled(false) + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + self.setUI() + self.setLayout() + self.setAddTarget() + self.setKeyboardNotification() + self.setTapGesture() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(true) + self.setTextFieldBottomBorder() + } +} + +// MARK: - Methods + +extension RunningRecordVC { + private func setAddTarget() { + self.courseTitleTextField.addTarget(self, action: #selector(textFieldTextDidChange), for: .editingChanged) + } + + // 키보드가 올라오면 scrollView 위치 조정 + private func setKeyboardNotification() { + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardWillShow), + name: UIResponder.keyboardWillShowNotification, + object: nil) + + NotificationCenter.default.addObserver( + self, + selector: #selector(keyboardWillHide), + name: UIResponder.keyboardWillHideNotification, + object: nil) + } + + // 화면 터치 시 키보드 내리기 + private func setTapGesture() { + let tap = UITapGestureRecognizer(target: view, action: #selector(UIView.endEditing)) + tap.cancelsTouchesInView = false + view.addGestureRecognizer(tap) + } + + func setData(distance: String, totalTime: String, averagePace: String, pathImage: UIImage?) { + self.distanceStatsView.setAttributedStats(stats: distance) + self.totalTimeStatsView.setStats(stats: totalTime) + self.averagePaceStatsView.setStats(stats: averagePace) + self.courseImageView.image = pathImage + } +} + +// MARK: - @objc Function + +extension RunningRecordVC { + @objc private func textFieldTextDidChange() { + guard let text = courseTitleTextField.text else { return } + + saveButton.isEnabled = !text.isEmpty + + if text.count > courseTitleMaxLength { + let index = text.index(text.startIndex, offsetBy: courseTitleMaxLength) + let newString = text[text.startIndex..