diff --git a/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj b/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj index 5b59a50e..cdc5fe6f 100644 --- a/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj +++ b/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj @@ -21,6 +21,8 @@ CE17F0352961BEF800E1DED0 /* Pretendard-SemiBold.otf in Resources */ = {isa = PBXBuildFile; fileRef = CE17F0312961BEF800E1DED0 /* Pretendard-SemiBold.otf */; }; CE17F0362961BEF800E1DED0 /* Pretendard-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = CE17F0322961BEF800E1DED0 /* Pretendard-Regular.otf */; }; CE17F0382961BF8B00E1DED0 /* FontLiterals.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE17F0372961BF8B00E1DED0 /* FontLiterals.swift */; }; + CE29D582296402B500F47542 /* CourseDrawingVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE29D581296402B500F47542 /* CourseDrawingVC.swift */; }; + CE29D584296416D800F47542 /* caculateStatusBarHeight.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE29D583296416D800F47542 /* caculateStatusBarHeight.swift */; }; CE4545C9295D7AF4003201E1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4545C8295D7AF4003201E1 /* AppDelegate.swift */; }; CE4545CB295D7AF4003201E1 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4545CA295D7AF4003201E1 /* SceneDelegate.swift */; }; CE4545CD295D7AF4003201E1 /* TaBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4545CC295D7AF4003201E1 /* TaBarController.swift */; }; @@ -102,6 +104,8 @@ CE17F0312961BEF800E1DED0 /* Pretendard-SemiBold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-SemiBold.otf"; sourceTree = ""; }; CE17F0322961BEF800E1DED0 /* Pretendard-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-Regular.otf"; sourceTree = ""; }; CE17F0372961BF8B00E1DED0 /* FontLiterals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontLiterals.swift; sourceTree = ""; }; + CE29D581296402B500F47542 /* CourseDrawingVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDrawingVC.swift; sourceTree = ""; }; + CE29D583296416D800F47542 /* caculateStatusBarHeight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = caculateStatusBarHeight.swift; sourceTree = ""; }; CE4545C5295D7AF4003201E1 /* Runnect-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Runnect-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; CE4545C8295D7AF4003201E1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; CE4545CA295D7AF4003201E1 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -308,6 +312,7 @@ children = ( CEEC6B392961C4F300D00E1E /* CourseDrawingHomeVC.swift */, CEC2A6912962BE2900160BF7 /* DepartureSearchVC.swift */, + CE29D581296402B500F47542 /* CourseDrawingVC.swift */, CEB8416F2963360800BF8080 /* CountDownVC.swift */, ); path = VC; @@ -527,6 +532,7 @@ CE58759F29601500005D967E /* Toast.swift */, CE6655C9295D84DD00C64E12 /* UserDefaultKeyList.swift */, CEC2A68F2962B06C00160BF7 /* convertLocationObject.swift */, + CE29D583296416D800F47542 /* caculateStatusBarHeight.swift */, ); path = Utils; sourceTree = ""; @@ -821,6 +827,7 @@ CE6655F4295D898400C64E12 /* UIViewController+.swift in Sources */, CE5645162961B72E000A2856 /* ImageLiterals.swift in Sources */, CE6655CD295D856300C64E12 /* KeyPathFindable.swift in Sources */, + CE29D582296402B500F47542 /* CourseDrawingVC.swift in Sources */, CE6655E4295D884600C64E12 /* UILabel+.swift in Sources */, CE6655FA295D90E000C64E12 /* applyShadow.swift in Sources */, A3BC2F2B2962C3D500198261 /* GoalRewardInfoVC.swift in Sources */, @@ -855,6 +862,7 @@ CEEC6B3A2961C4F300D00E1E /* CourseDrawingHomeVC.swift in Sources */, CEC2A6902962B06C00160BF7 /* convertLocationObject.swift in Sources */, CEC2A6852961F92C00160BF7 /* CustomButton.swift in Sources */, + CE29D584296416D800F47542 /* caculateStatusBarHeight.swift in Sources */, CE66560C295D928300C64E12 /* setRootViewController.swift in Sources */, CE6655D9295D871B00C64E12 /* URL+.swift in Sources */, CE6655DE295D877F00C64E12 /* UIColor+.swift in Sources */, diff --git a/Runnect-iOS/Runnect-iOS/Global/UIComponents/CustomNavigationBar.swift b/Runnect-iOS/Runnect-iOS/Global/UIComponents/CustomNavigationBar.swift index 69fcdc0c..182df0c6 100644 --- a/Runnect-iOS/Runnect-iOS/Global/UIComponents/CustomNavigationBar.swift +++ b/Runnect-iOS/Runnect-iOS/Global/UIComponents/CustomNavigationBar.swift @@ -107,11 +107,24 @@ extension CustomNavigationBar { return self } + @discardableResult + func setTextFieldText(text: String) -> Self { + self.textField.text = text + self.textField.isUserInteractionEnabled = false + return self + } + @discardableResult func showKeyboard() -> Self { self.textField.becomeFirstResponder() return self } + + @discardableResult + func hideRightButton() -> Self { + self.rightButton.isHidden = true + return self + } } // MARK: - @objc Function diff --git a/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNMapView.swift b/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNMapView.swift index b65e8360..135aad29 100644 --- a/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNMapView.swift +++ b/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNMapView.swift @@ -17,6 +17,8 @@ final class RNMapView: UIView { // MARK: - Properties @Published var pathDistance: Double = 0 + @Published var markerCount = 0 + let pathImage = PassthroughSubject() var cancelBag = Set() @@ -24,8 +26,8 @@ final class RNMapView: UIView { private var isDrawMode: Bool = false private var markers = [RNMarker]() { didSet { + markerCount = markers.count + 1 self.makePath() - self.setUndoButton() } } /// startMarker를 포함한 모든 마커들의 위치 정보 @@ -40,7 +42,6 @@ final class RNMapView: UIView { private var startMarker = RNStartMarker() private let pathOverlay = NMFPath() private let locationButton = UIButton(type: .custom) - private let undoButton = UIButton(type: .custom) // MARK: - initialization @@ -89,19 +90,25 @@ extension RNMapView { /// 지정 위치에 startMarker와 출발 infoWindow 생성 (기존의 startMarker는 제거) @discardableResult - func makeStartMarker(at location: NMGLatLng) -> Self { + func makeStartMarker(at location: NMGLatLng, withCameraMove: Bool = false) -> Self { self.startMarker.position = location self.startMarker.mapView = self.map.mapView self.startMarker.showInfoWindow() + if withCameraMove { + moveToLocation(location: location) + } + markerCount = 1 return self } /// 사용자 위치에 startMarker와 출발 infoWindow 생성 (기존의 startMarker는 제거) @discardableResult - func makeStartMarkerAtUserLocation() -> Self { + func makeStartMarkerAtUserLocation(withCameraMove: Bool = false) -> Self { self.startMarker.position = getUserLocation() self.startMarker.mapView = self.map.mapView self.startMarker.showInfoWindow() + moveToUserLocation() + markerCount = 1 return self } @@ -149,6 +156,18 @@ extension RNMapView { return self } + /// 지정 위치로 카메라 이동 + @discardableResult + func moveToLocation(location: NMGLatLng) -> Self { + let cameraUpdate = NMFCameraUpdate(scrollTo: location) + + DispatchQueue.main.async { [self] in + cameraUpdate.animation = .easeIn + self.map.mapView.moveCamera(cameraUpdate) + } + return self + } + /// 저장된 위치들로 경로선 그리기 @discardableResult func makePath() -> Self { @@ -168,13 +187,6 @@ extension RNMapView { return self } - /// undoButton 설정 - @discardableResult - func showUndoButton(toShow: Bool) -> Self { - self.undoButton.isHidden = !toShow - return self - } - /// 지도에 ContentPadding을 지정하여 중심 위치가 변경되게 설정 @discardableResult func makeContentPadding(padding: UIEdgeInsets) -> Self { @@ -217,7 +229,7 @@ extension RNMapView { } } - // 바운더리(MBR) 생성 + /// 바운더리(MBR) 생성 func makeMBR() -> NMGLatLngBounds { var latitudes = [Double]() var longitudes = [Double]() @@ -231,6 +243,13 @@ extension RNMapView { return NMGLatLngBounds(southWest: southWest, northEast: northEast) } + /// 직전의 마커 생성을 취소하고 경로선도 제거 + func undo() { + guard let lastMarker = self.markers.popLast() else { return } + substractDistance(with: lastMarker.position) + lastMarker.mapView = nil + } + // 두 지점 사이의 거리(m) 추가 private func addDistance(with newLocation: NMGLatLng) { let lastCLLoc = markersLatLngs.last?.toCLLocation() @@ -254,7 +273,7 @@ extension RNMapView { map.showLocationButton = false map.showScaleBar = false - map.mapView.logoAlign = .leftTop + map.mapView.logoAlign = .rightTop } private func getLocationAuth() { @@ -278,13 +297,9 @@ extension RNMapView { } private func setPathOverlay() { - pathOverlay.width = 3 + pathOverlay.width = 4 pathOverlay.outlineWidth = 0 - pathOverlay.color = .purple - } - - private func setUndoButton() { - self.undoButton.isEnabled = (markers.count >= 1) + pathOverlay.color = .m1 } } @@ -296,14 +311,10 @@ extension RNMapView { self.locationButton.setImage(ImageLiterals.icMapLocation, for: .normal) self.locationButton.isHidden = true self.locationButton.addTarget(self, action: #selector(locationButtonDidTap), for: .touchUpInside) - - self.undoButton.setImage(ImageLiterals.icCancel, for: .normal) - self.undoButton.isHidden = true - self.undoButton.addTarget(self, action: #selector(undoButtonDidTap), for: .touchUpInside) } private func setLayout() { - addSubviews(map, locationButton, undoButton) + addSubviews(map, locationButton) map.snp.makeConstraints { make in make.edges.equalToSuperview() @@ -313,15 +324,10 @@ extension RNMapView { make.bottom.equalToSuperview().inset(98+bottomPadding) make.trailing.equalToSuperview().inset(24) } - - undoButton.snp.makeConstraints { make in - make.bottom.equalToSuperview().inset(98+bottomPadding) - make.trailing.equalToSuperview().inset(24) - } } private func updateSubviewsConstraints() { - [locationButton, undoButton].forEach { view in + [locationButton].forEach { view in view.snp.updateConstraints { make in make.bottom.equalToSuperview().inset(98+bottomPadding) } @@ -335,12 +341,6 @@ extension RNMapView { @objc func locationButtonDidTap() { self.setPositionMode(mode: .direction) } - - @objc func undoButtonDidTap() { - guard let lastMarker = self.markers.popLast() else { return } - substractDistance(with: lastMarker.position) - lastMarker.mapView = nil - } } // MARK: - NMFMapViewCameraDelegate, NMFMapViewTouchDelegate @@ -348,7 +348,7 @@ extension RNMapView { extension RNMapView: NMFMapViewCameraDelegate, NMFMapViewTouchDelegate { // 지도 탭 이벤트 func mapView(_ mapView: NMFMapView, didTapMap latlng: NMGLatLng, point: CGPoint) { - guard isDrawMode else { return } + guard isDrawMode && markers.count < 19 else { return } self.makeMarker(at: latlng) } } diff --git a/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNStartMarker.swift b/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNStartMarker.swift index a54503a9..be891500 100644 --- a/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNStartMarker.swift +++ b/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNStartMarker.swift @@ -60,7 +60,7 @@ extension RNStartMarker: NMFOverlayImageDataSource { func view(with overlay: NMFOverlay) -> UIView { // 마커 위에 보여줄 InfoView 이미지 리턴 let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 58, height: 34)) - imageView.image = ImageLiterals.icMapDeparture + imageView.image = ImageLiterals.icMapStart return imageView } } diff --git a/Runnect-iOS/Runnect-iOS/Global/Utils/caculateStatusBarHeight.swift b/Runnect-iOS/Runnect-iOS/Global/Utils/caculateStatusBarHeight.swift new file mode 100644 index 00000000..5ef16c8e --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/Utils/caculateStatusBarHeight.swift @@ -0,0 +1,25 @@ +// +// caculateStatusBarHeight.swift +// Runnect-iOS +// +// Created by sejin on 2023/01/03. +// + +import UIKit + +extension UIApplication { + var statusBarHeight: CGFloat { + connectedScenes + .compactMap { + $0 as? UIWindowScene + } + .compactMap { + $0.statusBarManager + } + .map { + $0.statusBarFrame + } + .map(\.height) + .max() ?? 0 + } +} diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/CourseDrawingVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/CourseDrawingVC.swift new file mode 100644 index 00000000..5bcbfbc3 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/CourseDrawingVC.swift @@ -0,0 +1,251 @@ +// +// CourseDrawingVC.swift +// Runnect-iOS +// +// Created by sejin on 2023/01/03. +// + +import UIKit +import Combine + +final class CourseDrawingVC: UIViewController { + + // MARK: - Properties + + private var cancelBag = CancelBag() + + // MARK: - UI Components + + private let notchCoverView = UIView().then { + $0.backgroundColor = .w1 + } + + private lazy var naviBar = CustomNavigationBar(self, type: .search) + .setTextFieldText(text: "검색 결과") + .hideRightButton() + + private lazy var naviBarForEditing = CustomNavigationBar(self, type: .titleWithLeftButton) + .then { + $0.alpha = 0 + } + + private lazy var naviBarContainerStackView = UIStackView( + arrangedSubviews: [notchCoverView, naviBar] + ).then { + $0.axis = .vertical + } + + private let mapView = RNMapView().makeStartMarkerAtUserLocation() + + private let departureLocationLabel = UILabel().then { + $0.font = .b1 + $0.textColor = .g1 + $0.numberOfLines = 1 + $0.textAlignment = .left + $0.text = "장소 이름" + } + + private let departureDetailLocationLabel = UILabel().then { + $0.font = .b6 + $0.textColor = .g2 + $0.numberOfLines = 1 + $0.textAlignment = .left + $0.text = "상세 주소" + } + + private let decideDepartureButton = CustomButton(title: "출발지 설정하기") + + private let departureInfoContainerView = UIView().then { + $0.backgroundColor = .w1 + $0.layer.cornerRadius = 20 + $0.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] + } + + private let distanceLabel = UILabel().then { + $0.font = .h1 + $0.textColor = .g1 + $0.text = "0.0" + } + + private let kilometerLabel = UILabel().then { + $0.font = .b4 + $0.textColor = .g2 + $0.text = "km" + } + + private lazy var distanceStackView = UIStackView( + arrangedSubviews: [distanceLabel, kilometerLabel] + ).then { + $0.spacing = 3 + $0.alignment = .bottom + } + + private let distanceContainerView = UIView().then { + $0.backgroundColor = .w1 + $0.layer.cornerRadius = 22 + } + + private let undoButton = UIButton(type: .custom).then { + $0.setImage(ImageLiterals.icCancel, for: .normal) + } + + private let completeButton = CustomButton(title: "완성하기").setEnabled(false) + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + self.setUI() + self.setLayout() + self.setAddTarget() + self.bindMapView() + } +} + +// MARK: - Methods + +extension CourseDrawingVC { + private func setAddTarget() { + self.decideDepartureButton.addTarget(self, action: #selector(decideDepartureButtonDidTap), for: .touchUpInside) + self.undoButton.addTarget(self, action: #selector(undoButtonDidTap), for: .touchUpInside) + } + + private func bindMapView() { + mapView.$pathDistance.sink { distance in + 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) + }.store(in: cancelBag) + } +} + +// MARK: - @objc Function + +extension CourseDrawingVC { + @objc private func decideDepartureButtonDidTap() { + showHiddenViews() + + mapView.setDrawMode(to: true) + + UIView.animate(withDuration: 0.7) { + let naviBarContainerStackViewHeight = self.naviBarContainerStackView.frame.height + self.naviBarContainerStackView.transform = CGAffineTransform(translationX: 0, y: -naviBarContainerStackViewHeight) + self.departureInfoContainerView.transform = CGAffineTransform(translationX: 0, y: 172) + } + } + + @objc private func undoButtonDidTap() { + mapView.undo() + } +} + +// MARK: - UI & Layout + +extension CourseDrawingVC { + private func setUI() { + self.view.backgroundColor = .w1 + self.naviBarForEditing.backgroundColor = .clear + self.departureInfoContainerView.layer.applyShadow(alpha: 0.35, x: 0, y: 3, blur: 10) + self.distanceContainerView.layer.applyShadow(alpha: 0.2, x: 2, y: 4, blur: 9) + } + + private func setLayout() { + setHiddenViewsLayout() + self.view.addSubviews(naviBarContainerStackView, mapView, departureInfoContainerView) + self.departureInfoContainerView.addSubviews(departureLocationLabel, departureDetailLocationLabel, decideDepartureButton) + view.bringSubviewToFront(naviBarContainerStackView) + + notchCoverView.snp.makeConstraints { make in + var notchHeight = calculateTopInset() + if notchHeight == -44 { + let statusBarHeight = UIApplication.shared.statusBarHeight + notchHeight = -statusBarHeight + } + make.height.equalTo(-notchHeight) + } + + naviBar.snp.makeConstraints { make in + make.height.equalTo(48) + } + + naviBarContainerStackView.snp.makeConstraints { make in + make.leading.top.trailing.equalToSuperview() + } + + mapView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + departureInfoContainerView.snp.makeConstraints { make in + make.leading.bottom.trailing.equalToSuperview() + make.height.equalTo(172) + } + + departureLocationLabel.snp.makeConstraints { make in + make.top.equalToSuperview().inset(28) + make.leading.trailing.equalToSuperview().inset(16) + } + + departureDetailLocationLabel.snp.makeConstraints { make in + make.top.equalTo(departureLocationLabel.snp.bottom).offset(6) + make.leading.trailing.equalToSuperview().inset(16) + } + + decideDepartureButton.snp.makeConstraints { make in + make.top.equalTo(departureDetailLocationLabel.snp.bottom).offset(24) + make.leading.trailing.equalToSuperview().inset(16) + make.height.equalTo(44) + } + } + + private func setHiddenViewsLayout() { + view.addSubviews(naviBarForEditing, distanceContainerView, completeButton, undoButton) + view.sendSubviewToBack(naviBarForEditing) + + naviBarForEditing.snp.makeConstraints { make in + make.leading.top.trailing.equalTo(view.safeAreaLayoutGuide) + make.height.equalTo(48) + } + + distanceContainerView.snp.makeConstraints { make in + make.width.equalTo(96) + make.height.equalTo(44) + make.leading.equalTo(view.safeAreaLayoutGuide).inset(16) + make.top.equalTo(view.snp.bottom) + } + + distanceContainerView.addSubviews(distanceStackView) + + distanceStackView.snp.makeConstraints { make in + make.center.equalToSuperview() + } + + undoButton.snp.makeConstraints { make in + make.trailing.equalTo(view.safeAreaLayoutGuide) + make.top.equalTo(view.snp.bottom) + } + + completeButton.snp.makeConstraints { make in + make.leading.trailing.equalTo(view.safeAreaLayoutGuide).inset(16) + make.height.equalTo(44) + make.top.equalTo(view.snp.bottom).offset(34) + } + } + + private func showHiddenViews() { + [naviBarForEditing, distanceContainerView, completeButton, undoButton].forEach { subView in + view.bringSubviewToFront(subView) + } + + UIView.animate(withDuration: 0.7) { + self.naviBarForEditing.alpha = 1 + self.distanceContainerView.transform = CGAffineTransform(translationX: 0, y: -151) + self.completeButton.transform = CGAffineTransform(translationX: 0, y: -112) + self.undoButton.transform = CGAffineTransform(translationX: 0, y: -(self.undoButton.frame.height+95)) + } + } +} diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/DepartureSearchVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/DepartureSearchVC.swift index a037957e..29183b24 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/DepartureSearchVC.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/DepartureSearchVC.swift @@ -119,6 +119,12 @@ extension DepartureSearchVC: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 68 } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let courseDrawingVC = CourseDrawingVC() + courseDrawingVC.hidesBottomBarWhenPushed = true + self.navigationController?.pushViewController(courseDrawingVC, animated: true) + } } // MARK: - CustomNavigationBarDelegate