diff --git a/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj b/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj index 818d63f0..52ff1bfa 100644 --- a/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj +++ b/Runnect-iOS/Runnect-iOS.xcodeproj/project.pbxproj @@ -65,8 +65,14 @@ 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 */; }; + CEB8416E2962C45300BF8080 /* LocationSearchResultTVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB8416D2962C45300BF8080 /* LocationSearchResultTVC.swift */; }; CEC2A6852961F92C00160BF7 /* CustomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC2A6842961F92C00160BF7 /* CustomButton.swift */; }; CEC2A68729629B9B00160BF7 /* SignInVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC2A68629629B9B00160BF7 /* SignInVC.swift */; }; + CEC2A68A2962ADCD00160BF7 /* RNMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC2A6892962ADCD00160BF7 /* RNMapView.swift */; }; + CEC2A68C2962AE1B00160BF7 /* RNStartMarker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC2A68B2962AE1B00160BF7 /* RNStartMarker.swift */; }; + CEC2A68E2962AF2C00160BF7 /* RNMarker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC2A68D2962AF2C00160BF7 /* RNMarker.swift */; }; + CEC2A6902962B06C00160BF7 /* convertLocationObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC2A68F2962B06C00160BF7 /* convertLocationObject.swift */; }; + CEC2A6922962BE2900160BF7 /* DepartureSearchVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC2A6912962BE2900160BF7 /* DepartureSearchVC.swift */; }; CEEC6B3A2961C4F300D00E1E /* CourseDrawingHomeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEC6B392961C4F300D00E1E /* CourseDrawingHomeVC.swift */; }; CEEC6B3C2961C51A00D00E1E /* CourseStorageVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEC6B3B2961C51A00D00E1E /* CourseStorageVC.swift */; }; CEEC6B3E2961C53700D00E1E /* CourseDiscoveryVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEC6B3D2961C53700D00E1E /* CourseDiscoveryVC.swift */; }; @@ -142,13 +148,18 @@ 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 = ""; }; + CEB8416D2962C45300BF8080 /* LocationSearchResultTVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSearchResultTVC.swift; sourceTree = ""; }; CEC2A6842961F92C00160BF7 /* CustomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomButton.swift; sourceTree = ""; }; CEC2A68629629B9B00160BF7 /* SignInVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInVC.swift; sourceTree = ""; }; + CEC2A6892962ADCD00160BF7 /* RNMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNMapView.swift; sourceTree = ""; }; + CEC2A68B2962AE1B00160BF7 /* RNStartMarker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNStartMarker.swift; sourceTree = ""; }; + CEC2A68D2962AF2C00160BF7 /* RNMarker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNMarker.swift; sourceTree = ""; }; + CEC2A68F2962B06C00160BF7 /* convertLocationObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = convertLocationObject.swift; sourceTree = ""; }; + CEC2A6912962BE2900160BF7 /* DepartureSearchVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DepartureSearchVC.swift; sourceTree = ""; }; CEEC6B392961C4F300D00E1E /* CourseDrawingHomeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseDrawingHomeVC.swift; sourceTree = ""; }; 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 = ""; }; - CEEC6B412961C58B00D00E1E /* .gitkeep */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitkeep; 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 = ""; }; @@ -266,6 +277,7 @@ isa = PBXGroup; children = ( CEEC6B392961C4F300D00E1E /* CourseDrawingHomeVC.swift */, + CEC2A6912962BE2900160BF7 /* DepartureSearchVC.swift */, ); path = VC; sourceTree = ""; @@ -273,7 +285,7 @@ CE17F0402961C3CD00E1DED0 /* Views */ = { isa = PBXGroup; children = ( - CEEC6B412961C58B00D00E1E /* .gitkeep */, + CEB8416D2962C45300BF8080 /* LocationSearchResultTVC.swift */, ); path = Views; sourceTree = ""; @@ -483,6 +495,7 @@ CE58759D29601476005D967E /* LoadingIndicator.swift */, CE58759F29601500005D967E /* Toast.swift */, CE6655C9295D84DD00C64E12 /* UserDefaultKeyList.swift */, + CEC2A68F2962B06C00160BF7 /* convertLocationObject.swift */, ); path = Utils; sourceTree = ""; @@ -530,6 +543,7 @@ CE6655B6295D803C00C64E12 /* UIComponents */ = { isa = PBXGroup; children = ( + CEC2A6882962ADB900160BF7 /* MapView */, CEEC6B4A2961D89700D00E1E /* CustomNavigationBar.swift */, CEC2A6842961F92C00160BF7 /* CustomButton.swift */, ); @@ -610,6 +624,16 @@ path = "UIKit+"; sourceTree = ""; }; + CEC2A6882962ADB900160BF7 /* MapView */ = { + isa = PBXGroup; + children = ( + CEC2A6892962ADCD00160BF7 /* RNMapView.swift */, + CEC2A68B2962AE1B00160BF7 /* RNStartMarker.swift */, + CEC2A68D2962AF2C00160BF7 /* RNMarker.swift */, + ); + path = MapView; + sourceTree = ""; + }; CEEC6B472961C5CA00D00E1E /* VC */ = { isa = PBXGroup; children = ( @@ -784,7 +808,9 @@ CE6655E8295D889600C64E12 /* UISwitch+.swift in Sources */, CE5875A029601500005D967E /* Toast.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 */, CE6655F8295D90CF00C64E12 /* adjusted+.swift in Sources */, CE4545CB295D7AF4003201E1 /* SceneDelegate.swift in Sources */, @@ -795,6 +821,7 @@ CE6655D4295D865B00C64E12 /* Publisher+UIControl.swift in Sources */, CE6655EC295D88D000C64E12 /* UITableView+.swift in Sources */, CEEC6B3A2961C4F300D00E1E /* CourseDrawingHomeVC.swift in Sources */, + CEC2A6902962B06C00160BF7 /* convertLocationObject.swift in Sources */, CEC2A6852961F92C00160BF7 /* CustomButton.swift in Sources */, CE66560C295D928300C64E12 /* setRootViewController.swift in Sources */, CE6655D9295D871B00C64E12 /* URL+.swift in Sources */, @@ -804,11 +831,14 @@ CE665612295D92E400C64E12 /* UserDefaultWrapper.swift in Sources */, CE665610295D92C200C64E12 /* setTextLineHeight.swift in Sources */, CE6655E2295D87EB00C64E12 /* UIImage+.swift in Sources */, + CEC2A68C2962AE1B00160BF7 /* RNStartMarker.swift in Sources */, CE5875A4296015D2005D967E /* Encodable+.swift in Sources */, CEEC6B4B2961D89700D00E1E /* CustomNavigationBar.swift in Sources */, CE17F02D2961BBA100E1DED0 /* ColorLiterals.swift in Sources */, + CEC2A68E2962AF2C00160BF7 /* RNMarker.swift in Sources */, CE6655D2295D862A00C64E12 /* Publisher+Driver.swift in Sources */, CE6655E6295D887F00C64E12 /* UIStackView+.swift in Sources */, + CEB8416E2962C45300BF8080 /* LocationSearchResultTVC.swift in Sources */, CE6655CA295D84DD00C64E12 /* UserDefaultKeyList.swift in Sources */, CE6655F2295D894D00C64E12 /* UIView+.swift in Sources */, CE665600295D915D00C64E12 /* getClassName.swift in Sources */, diff --git a/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UIImage+.swift b/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UIImage+.swift index 50a36e0a..86abf3bd 100644 --- a/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UIImage+.swift +++ b/Runnect-iOS/Runnect-iOS/Global/Extension/UIKit+/UIImage+.swift @@ -33,4 +33,13 @@ extension UIImage { } return renderImage } + + /// UIView를 image로 변환 + convenience init(view: UIView) { + UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0.0) + view.drawHierarchy(in: view.bounds, afterScreenUpdates: false) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + self.init(cgImage: (image?.cgImage)!) + } } diff --git a/Runnect-iOS/Runnect-iOS/Global/Literal/ImageLiterals.swift b/Runnect-iOS/Runnect-iOS/Global/Literal/ImageLiterals.swift index 4cd9b493..0cedee41 100644 --- a/Runnect-iOS/Runnect-iOS/Global/Literal/ImageLiterals.swift +++ b/Runnect-iOS/Runnect-iOS/Global/Literal/ImageLiterals.swift @@ -35,6 +35,8 @@ enum ImageLiterals { static var icStorageFill: UIImage { .load(named: "ic_storage_fill") } static var icStorage: UIImage { .load(named: "ic_storage") } static var icTime: UIImage { .load(named: "ic_time") } + static var icLocationPoint: UIImage { .load(named: "ic_location_point") } + static var icAlert: UIImage { .load(named: "ic_alert") } // img static var imgBackground: UIImage { .load(named: "img_background") } static var imgLogo: UIImage { .load(named: "img_logo") } diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_alert.imageset/Contents.json b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_alert.imageset/Contents.json new file mode 100644 index 00000000..20315ec1 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_alert.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Group 9279.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group 9279@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group 9279@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_alert.imageset/Group 9279.png b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_alert.imageset/Group 9279.png new file mode 100644 index 00000000..90997267 Binary files /dev/null and b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_alert.imageset/Group 9279.png differ diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_alert.imageset/Group 9279@2x.png b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_alert.imageset/Group 9279@2x.png new file mode 100644 index 00000000..6c20c469 Binary files /dev/null and b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_alert.imageset/Group 9279@2x.png differ diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_alert.imageset/Group 9279@3x.png b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_alert.imageset/Group 9279@3x.png new file mode 100644 index 00000000..e002a502 Binary files /dev/null and b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_alert.imageset/Group 9279@3x.png differ diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_location_point.imageset/Contents.json b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_location_point.imageset/Contents.json new file mode 100644 index 00000000..df240cf1 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_location_point.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Vector.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Vector@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Vector@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_location_point.imageset/Vector.png b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_location_point.imageset/Vector.png new file mode 100644 index 00000000..a1260ea5 Binary files /dev/null and b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_location_point.imageset/Vector.png differ diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_location_point.imageset/Vector@2x.png b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_location_point.imageset/Vector@2x.png new file mode 100644 index 00000000..2b602116 Binary files /dev/null and b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_location_point.imageset/Vector@2x.png differ diff --git a/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_location_point.imageset/Vector@3x.png b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_location_point.imageset/Vector@3x.png new file mode 100644 index 00000000..8ba0ba01 Binary files /dev/null and b/Runnect-iOS/Runnect-iOS/Global/Resource/Assets.xcassets/ic_location_point.imageset/Vector@3x.png differ diff --git a/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNMapView.swift b/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNMapView.swift new file mode 100644 index 00000000..b65e8360 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNMapView.swift @@ -0,0 +1,356 @@ +// +// RNMapView.swift +// Runnect-iOS +// +// Created by sejin on 2023/01/02. +// +import UIKit +import CoreLocation +import Combine + +import NMapsMap +import SnapKit +import Then + +final class RNMapView: UIView { + + // MARK: - Properties + + @Published var pathDistance: Double = 0 + let pathImage = PassthroughSubject() + var cancelBag = Set() + + let locationManager = CLLocationManager() + private var isDrawMode: Bool = false + private var markers = [RNMarker]() { + didSet { + self.makePath() + self.setUndoButton() + } + } + /// startMarker를 포함한 모든 마커들의 위치 정보 + private var markersLatLngs: [NMGLatLng] { + [self.startMarker.position] + self.markers.map { $0.position } + } + private var bottomPadding: CGFloat = 0 + + // MARK: - UI Components + + let map = NMFNaverMapView() + private var startMarker = RNStartMarker() + private let pathOverlay = NMFPath() + private let locationButton = UIButton(type: .custom) + private let undoButton = UIButton(type: .custom) + + // MARK: - initialization + + public init() { + super.init(frame: .zero) + setUI() + setLayout() + setDelegate() + setMap() + getLocationAuth() + setPathOverlay() + } + + override init(frame: CGRect) { + super.init(frame: frame) + setUI() + setLayout() + setDelegate() + setMap() + getLocationAuth() + setPathOverlay() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - Methods + +extension RNMapView { + + /// isDrawMode (편집 모드) 설정 + @discardableResult + func setDrawMode(to isDrawMode: Bool) -> Self { + self.isDrawMode = isDrawMode + return self + } + + /// 카메라가 따라가는 mode 설정 + @discardableResult + func setPositionMode(mode: NMFMyPositionMode) -> Self { + map.mapView.positionMode = mode + return self + } + + /// 지정 위치에 startMarker와 출발 infoWindow 생성 (기존의 startMarker는 제거) + @discardableResult + func makeStartMarker(at location: NMGLatLng) -> Self { + self.startMarker.position = location + self.startMarker.mapView = self.map.mapView + self.startMarker.showInfoWindow() + return self + } + + /// 사용자 위치에 startMarker와 출발 infoWindow 생성 (기존의 startMarker는 제거) + @discardableResult + func makeStartMarkerAtUserLocation() -> Self { + self.startMarker.position = getUserLocation() + self.startMarker.mapView = self.map.mapView + self.startMarker.showInfoWindow() + return self + } + + /// 지정 위치에 마커 생성 + @discardableResult + func makeMarker(at location: NMGLatLng) -> Self { + let marker = RNMarker() + marker.position = location + marker.mapView = self.map.mapView + addDistance(with: location) + self.markers.append(marker) + return self + } + + /// NMGLatLng 어레이를 받아서 모든 위치에 마커 생성 + @discardableResult + func makeMarkers(at locations: [NMGLatLng]) -> Self { + locations.forEach { location in + makeMarker(at: location) + } + return self + } + + /// NMGLatLng 어레이를 받아서 첫 위치를 startMarker로 설정하고 나머지를 일반 마커로 생성 + @discardableResult + func makeMarkersWithStartMarker(at locations: [NMGLatLng]) -> Self { + if locations.count < 2 { return self } + makeStartMarker(at: locations[0]) + locations[1...].forEach { location in + makeMarker(at: location) + } + return self + } + + /// 사용자 위치로 카메라 이동 + @discardableResult + func moveToUserLocation() -> Self { + let userLatLng = getUserLocation() + let cameraUpdate = NMFCameraUpdate(scrollTo: userLatLng) + + DispatchQueue.main.async { [self] in + cameraUpdate.animation = .easeIn + self.map.mapView.moveCamera(cameraUpdate) + } + return self + } + + /// 저장된 위치들로 경로선 그리기 + @discardableResult + func makePath() -> Self { + if self.markersLatLngs.count == 1 { + self.pathOverlay.mapView = nil + return self + } + pathOverlay.path = NMGLineString(points: self.markersLatLngs) + pathOverlay.mapView = map.mapView + return self + } + + /// locationButton 설정 + @discardableResult + func showLocationButton(toShow: Bool) -> Self { + self.locationButton.isHidden = !toShow + return self + } + + /// undoButton 설정 + @discardableResult + func showUndoButton(toShow: Bool) -> Self { + self.undoButton.isHidden = !toShow + return self + } + + /// 지도에 ContentPadding을 지정하여 중심 위치가 변경되게 설정 + @discardableResult + func makeContentPadding(padding: UIEdgeInsets) -> Self { + map.mapView.contentInset = padding + self.bottomPadding = padding.bottom + updateSubviewsConstraints() + return self + } + + /// 현재 존재하는 Marker들 위치 리턴 + func getMarkersLatLng() -> [NMGLatLng] { + return self.markersLatLngs + } + + /// 사용자 위치 가져오기 + func getUserLocation() -> NMGLatLng { + let userLocation = locationManager.location?.coordinate + let userLatLng = userLocation.toNMGLatLng() + return userLatLng + } + + /// 경로 총 거리 가져오기 + func getPathDistance() -> Double { + 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)) + } + } + + // 바운더리(MBR) 생성 + func makeMBR() -> NMGLatLngBounds { + var latitudes = [Double]() + var longitudes = [Double]() + self.markersLatLngs.forEach { latLng in + latitudes.append(latLng.lat) + longitudes.append(latLng.lng) + } + + let southWest = NMGLatLng(lat: latitudes.min() ?? 0, lng: longitudes.min() ?? 0) + let northEast = NMGLatLng(lat: latitudes.max() ?? 0, lng: longitudes.max() ?? 0) + return NMGLatLngBounds(southWest: southWest, northEast: northEast) + } + + // 두 지점 사이의 거리(m) 추가 + private func addDistance(with newLocation: NMGLatLng) { + let lastCLLoc = markersLatLngs.last?.toCLLocation() + let newCLLoc = newLocation.toCLLocation() + guard let distance = lastCLLoc?.distance(from: newCLLoc) else { return } + pathDistance += distance + } + + // 마지막 지점까지의 거리(m) 제거 + private func substractDistance(with targetLocation: NMGLatLng) { + let lastCLLoc = markersLatLngs.last?.toCLLocation() + let targetCLLoc = targetLocation.toCLLocation() + guard let distance = lastCLLoc?.distance(from: targetCLLoc) else { return } + pathDistance -= distance + if pathDistance < 1 { pathDistance = 0 } + } + + private func setMap() { + // 카메라 대상 지점을 한반도로 고정 + map.mapView.extent = NMGLatLngBounds(southWestLat: 31.43, southWestLng: 122.37, northEastLat: 44.35, northEastLng: 132) + map.showLocationButton = false + map.showScaleBar = false + + map.mapView.logoAlign = .leftTop + } + + private func getLocationAuth() { + DispatchQueue.global().async { [self] in + if CLLocationManager.locationServicesEnabled() { + print("위치 상태 On 상태") + self.locationManager.startUpdatingLocation() + } else { + print("위치 상태 Off 상태") + } + } + } + + private func setDelegate() { + locationManager.delegate = self + locationManager.desiredAccuracy = CLLocationAccuracy.greatestFiniteMagnitude + locationManager.requestWhenInUseAuthorization() + + map.mapView.addCameraDelegate(delegate: self) + map.mapView.touchDelegate = self + } + + private func setPathOverlay() { + pathOverlay.width = 3 + pathOverlay.outlineWidth = 0 + pathOverlay.color = .purple + } + + private func setUndoButton() { + self.undoButton.isEnabled = (markers.count >= 1) + } +} + +// MARK: - UI & Layout + +extension RNMapView { + private func setUI() { + self.backgroundColor = .white + 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) + + map.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + locationButton.snp.makeConstraints { make in + 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 + view.snp.updateConstraints { make in + make.bottom.equalToSuperview().inset(98+bottomPadding) + } + } + } +} + +// MARK: - @objc Function + +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 + +extension RNMapView: NMFMapViewCameraDelegate, NMFMapViewTouchDelegate { + // 지도 탭 이벤트 + func mapView(_ mapView: NMFMapView, didTapMap latlng: NMGLatLng, point: CGPoint) { + guard isDrawMode else { return } + self.makeMarker(at: latlng) + } +} + +extension RNMapView: CLLocationManagerDelegate {} diff --git a/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNMarker.swift b/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNMarker.swift new file mode 100644 index 00000000..386b0f41 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNMarker.swift @@ -0,0 +1,38 @@ +// +// RNMarker.swift +// Runnect-iOS +// +// Created by sejin on 2023/01/02. +// + +import UIKit + +import NMapsMap +import SnapKit +import Then + +final class RNMarker: NMFMarker { + + // MARK: - initialization + + override init() { + super.init() + setUI() + } +} + +// MARK: - UI & Layout + +extension RNMarker { + private func setUI() { + let image = NMFOverlayImage(image: ImageLiterals.icMapPoint) + self.iconImage = image + + self.width = CGFloat(NMF_MARKER_SIZE_AUTO) + self.height = CGFloat(NMF_MARKER_SIZE_AUTO) + + self.anchor = CGPoint(x: 0.5, y: 0.5) + + self.iconPerspectiveEnabled = true + } +} diff --git a/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNStartMarker.swift b/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNStartMarker.swift new file mode 100644 index 00000000..a54503a9 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/UIComponents/MapView/RNStartMarker.swift @@ -0,0 +1,66 @@ +// +// RNStartMarker.swift +// Runnect-iOS +// +// Created by sejin on 2023/01/02. +// + +import UIKit + +import NMapsMap +import SnapKit +import Then + +final class RNStartMarker: NMFMarker { + + // MARK: - UI & Layout + + let startInfoWindow = NMFInfoWindow() + + // MARK: - initialization + + override init() { + super.init() + setUI() + setInfoWindow() + } +} + +// MARK: - UI & Layout + +extension RNStartMarker { + private func setUI() { + let image = NMFOverlayImage(image: ImageLiterals.icMapDeparture) + self.iconImage = image + + self.width = CGFloat(NMF_MARKER_SIZE_AUTO) + self.height = CGFloat(NMF_MARKER_SIZE_AUTO) + + self.anchor = CGPoint(x: 0.5, y: 0.5) + + self.iconPerspectiveEnabled = true + } + + private func setInfoWindow() { + startInfoWindow.dataSource = self + } + + func showInfoWindow() { + startInfoWindow.open(with: self) + } + + func hideInfoWindow() { + startInfoWindow.close() + } +} + +// MARK: - NMFOverlayImageDataSource + +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 + return imageView + } +} diff --git a/Runnect-iOS/Runnect-iOS/Global/Utils/convertLocationObject.swift b/Runnect-iOS/Runnect-iOS/Global/Utils/convertLocationObject.swift new file mode 100644 index 00000000..cc95fe70 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Global/Utils/convertLocationObject.swift @@ -0,0 +1,29 @@ +// +// convertLocationObject.swift +// Runnect-iOS +// +// Created by sejin on 2023/01/02. +// + +import Foundation +import CoreLocation + +import NMapsMap + +extension CLLocationCoordinate2D? { + func toNMGLatLng() -> NMGLatLng { + return NMGLatLng(lat: self?.latitude ?? 37.52901832956373, lng: self?.longitude ?? 126.9136196847032) + } +} + +extension CLLocationCoordinate2D { + func toNMGLatLng() -> NMGLatLng { + return NMGLatLng(lat: self.latitude, lng: self.longitude) + } +} + +extension NMGLatLng { + func toCLLocation() -> CLLocation { + return CLLocation(latitude: lat, longitude: lng) + } +} diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/CourseDrawingHomeVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/CourseDrawingHomeVC.swift index 9da09656..55a0bcae 100644 --- a/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/CourseDrawingHomeVC.swift +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/CourseDrawingHomeVC.swift @@ -7,36 +7,69 @@ import UIKit -final class CourseDrawingHomeVC: UIViewController, CustomNavigationBarDelegate { - lazy var naviBar = CustomNavigationBar(self, type: .title).setTitle("보관함") -// lazy var naviBar = CustomNavigationBar(self, type: .titleWithLeftButton) -// lazy var naviBar = CustomNavigationBar(self, type: .titleWithLeftButton).setTitle("목표 보상") -// lazy var naviBar = CustomNavigationBar(self, type: .search).showKeyboard().setTextFieldPlaceholder(placeholder: "출발지 검색") +final class CourseDrawingHomeVC: UIViewController { - let button = CustomButton(title: "코스 그리기") + // MARK: - Properties + + private lazy var tabBarHeight = self.tabBarController?.tabBar.frame.size.height ?? 49 + + // MARK: - UI Components + + private lazy var mapView = RNMapView() + .setPositionMode(mode: .normal) + .makeContentPadding(padding: UIEdgeInsets(top: -calculateTopInset(), left: 0, bottom: tabBarHeight, right: 0)) + .moveToUserLocation() + .showLocationButton(toShow: true) + + private let drawCourseButton = CustomButton(title: "코스 그리기") + + // MARK: - View Life Cycle override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .white - - naviBar.delegate = self - - view.addSubviews(naviBar, button) + self.setUI() + self.setLayout() + self.setAddTarget() + } +} + +// MARK: - Methods + +extension CourseDrawingHomeVC { + private func setAddTarget() { + drawCourseButton.addTarget(self, action: #selector(pushToDepartureSearchVC), for: .touchUpInside) + } +} + +// MARK: - @objc Function +extension CourseDrawingHomeVC { + @objc private func pushToDepartureSearchVC() { + let departureSearchVC = DepartureSearchVC() + self.navigationController?.pushViewController(departureSearchVC, animated: true) + } +} + +// MARK: - UI & Layout + +extension CourseDrawingHomeVC { + private func setUI() { + view.backgroundColor = .w1 + } + + private func setLayout() { + view.addSubviews(mapView, drawCourseButton) - naviBar.snp.makeConstraints { make in - make.leading.top.trailing.equalTo(view.safeAreaLayoutGuide) - make.height.equalTo(48) + mapView.snp.makeConstraints { make in + make.top.bottom.equalToSuperview() + make.leading.trailing.equalTo(view.safeAreaLayoutGuide) } - button.snp.makeConstraints { make in - make.leading.bottom.trailing.equalTo(view.safeAreaLayoutGuide).inset(20) + drawCourseButton.snp.makeConstraints { make in + make.leading.trailing.equalTo(view.safeAreaLayoutGuide).inset(75) + make.bottom.equalTo(view.safeAreaLayoutGuide).inset(24) make.height.equalTo(44) } } - - func searchButtonDidTap(text: String) { - print(text) - } } diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/DepartureSearchVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/DepartureSearchVC.swift new file mode 100644 index 00000000..a037957e --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/VC/DepartureSearchVC.swift @@ -0,0 +1,131 @@ +// +// DepartureSearchVC.swift +// Runnect-iOS +// +// Created by sejin on 2023/01/02. +// + +import UIKit + +final class DepartureSearchVC: UIViewController { + + // MARK: - UI Components + + private lazy var naviBar = CustomNavigationBar(self, type: .search).setTextFieldPlaceholder(placeholder: "지역과 키워드 위주로 검색해보세요") + + private let dividerView = UIView().then { + $0.backgroundColor = .g4 + } + + private let locationTableView = UITableView(frame: .zero, style: .plain).then { + $0.backgroundColor = .white + $0.separatorStyle = .none + } + + private let alertImageView = UIImageView().then { + $0.image = ImageLiterals.icAlert + $0.tintColor = .g3 + } + + private let descriptionLabel = UILabel().then { + $0.text = "검색결과가 없습니다\n검색어를 다시 확인해주세요" + $0.font = .b4 + $0.textColor = .g3 + $0.numberOfLines = 2 + $0.textAlignment = .center + } + + private lazy var emptyDataView = UIStackView(arrangedSubviews: [alertImageView, descriptionLabel]).then { + $0.axis = .vertical + $0.spacing = 22 + $0.alignment = .center + } + + // MARK: - View Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + self.setUI() + self.setLayout() + self.setDelegate() + self.registerCell() + } +} + +// MARK: - Methods + +extension DepartureSearchVC { + private func setDelegate() { + self.naviBar.delegate = self + self.locationTableView.delegate = self + self.locationTableView.dataSource = self + } + + private func registerCell() { + self.locationTableView.register(LocationSearchResultTVC.self, + forCellReuseIdentifier: LocationSearchResultTVC.className) + } +} + +// MARK: - UI & Layout + +extension DepartureSearchVC { + private func setUI() { + view.backgroundColor = .w1 + emptyDataView.isHidden = true // 데이터가 없으면 false로 설정 + } + + private func setLayout() { + view.addSubviews(naviBar, dividerView, locationTableView) + + naviBar.snp.makeConstraints { make in + make.leading.top.trailing.equalTo(view.safeAreaLayoutGuide) + make.height.equalTo(48) + } + + dividerView.snp.makeConstraints { make in + make.top.equalTo(naviBar.snp.bottom) + make.leading.trailing.equalTo(view.safeAreaLayoutGuide) + make.height.equalTo(6) + } + + locationTableView.snp.makeConstraints { make in + make.top.equalTo(dividerView.snp.bottom) + make.leading.bottom.trailing.equalTo(view.safeAreaLayoutGuide) + } + + locationTableView.addSubview(emptyDataView) + emptyDataView.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } +} + +// MARK: - UITableViewDelegate, UITableViewDataSource + +extension DepartureSearchVC: UITableViewDelegate, UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 5 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: LocationSearchResultTVC.className, for: indexPath) + as? LocationSearchResultTVC + else { return UITableViewCell() } + cell.selectionStyle = .none + return cell + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return 68 + } +} + +// MARK: - CustomNavigationBarDelegate + +extension DepartureSearchVC: CustomNavigationBarDelegate { + func searchButtonDidTap(text: String) { + print(text) + // 서버 통신 구현 + } +} diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/Views/.gitkeep b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/Views/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/Views/LocationSearchResultTVC.swift b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/Views/LocationSearchResultTVC.swift new file mode 100644 index 00000000..7e2edb92 --- /dev/null +++ b/Runnect-iOS/Runnect-iOS/Presentation/CourseDrawing/Views/LocationSearchResultTVC.swift @@ -0,0 +1,82 @@ +// +// LocationSearchResultTVC.swift +// Runnect-iOS +// +// Created by sejin on 2023/01/02. +// + +import UIKit + +final class LocationSearchResultTVC: UITableViewCell { + + // MARK: - UI Components + + private let locationPointImageView = UIImageView().then { + $0.image = ImageLiterals.icLocationPoint + $0.tintColor = .g3 + } + + private let locationLabel = UILabel().then { + $0.text = "장소" + $0.font = .b1 + $0.textColor = .g1 + } + + private let detailLocationLabel = UILabel().then { + $0.text = "상세 주소" + $0.font = .b6 + $0.textColor = .g2 + } + + private lazy var locationStackView = UIStackView(arrangedSubviews: [locationLabel, detailLocationLabel]).then { + $0.axis = .vertical + $0.spacing = 4 + $0.alignment = .leading + } + + private let dividerView = UIView().then { + $0.backgroundColor = .g4 + } + + // MARK: - initialization + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + self.setUI() + self.setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - UI & Layout + +extension LocationSearchResultTVC { + private func setUI() { + self.backgroundColor = .w1 + self.contentView.backgroundColor = .w1 + } + + private func setLayout() { + self.addSubviews(locationPointImageView, locationStackView, dividerView) + + locationPointImageView.snp.makeConstraints { make in + make.leading.equalToSuperview().inset(24) + make.centerY.equalToSuperview() + } + + locationStackView.snp.makeConstraints { make in + make.leading.equalTo(locationPointImageView.snp.trailing).offset(14) + make.centerY.equalToSuperview() + make.trailing.equalToSuperview().inset(24) + } + + dividerView.snp.makeConstraints { make in + make.bottom.equalToSuperview() + make.leading.trailing.equalToSuperview() + make.height.equalTo(1) + } + } +}