일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 러닝타이머
- xcode로 날씨앱 만들기
- Startign Assignments
- Timer
- swift
- font book
- Protocol
- 러닝기록앱
- CLLocationManagerDelegate
- 단일 책임원칙
- RunningTimer
- 서체관리자
- Required Reason API
- WeatherManager
- 한국어 개인정보처리방침
- weak var
- AnyObject
- App Store Connect
- MKMapItem
- Xcode
- addannotation
- UICollectionViewFlowLayout
- UIAlertAction
- SwiftUI Boolean 값
- CoreLocation
- 영문 개인정보처리방침
- dispatchsource
- 클로저의 캡슐화
- MKMapViewDelegate
- weatherKit
- Today
- Total
VesselWheel
MapKit에서 실시간 사용자 위치 추적 및 경로 표시하기 본문
Mapkit을 사용하기 위해서는 첫번째로
CLLocationManager과 MKMapView의 객체를 정의해야한다.
lazy var locationManager: CLLocationManager = {
let manager = CLLocationManager()
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.startUpdatingLocation() // startUpdate를 해야 didUpdateLocation 메서드가 호출됨.
manager.delegate = self
manager.pausesLocationUpdatesAutomatically = false
manager.allowsBackgroundLocationUpdates = true
manager.showsBackgroundLocationIndicator = true
return manager
}()
lazy var mapView: MKMapView = {
let mapView = MKMapView()
mapView.showsUserLocation = true
mapView.isZoomEnabled = true
mapView.isRotateEnabled = true
mapView.mapType = MKMapType.standard
mapView.showsCompass = false
return mapView
}()
lazy var compassButton: MKCompassButton = {
let compassButton = MKCompassButton(mapView: self.mapView)
compassButton.compassVisibility = .visible
var config = UIButton.Configuration.filled()
config.cornerStyle = .capsule
compassButton.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
return compassButton
}()
lazy var currentLocationButton: UIButton = {
let button = UIButton()
var config = UIButton.Configuration.filled()
config.baseBackgroundColor = .white
config.cornerStyle = .capsule
config.baseForegroundColor = .black
config.image = UIImage(systemName: "location")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 20, weight: .medium))
button.configuration = config
button.layer.cornerRadius = 25
button.addTarget(self, action: #selector(currentLocationButtonAction), for: .touchUpInside)
return button
}()
-> CLLocationManager는 NSObject로 CLLocationManager을 사용하고자 하는 곳에서 객체를 생성하고, CLLocationManager에서 지원하는 매소드, 프로퍼티의 상속 및 참조 기능을 제공한다.
lazy var locationManager: CLLocationManager = {
let manager = CLLocationManager()
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.startUpdatingLocation() // startUpdate를 해야 didUpdateLocation 메서드가 호출됨.
manager.delegate = self
manager.pausesLocationUpdatesAutomatically = false
manager.allowsBackgroundLocationUpdates = true
manager.showsBackgroundLocationIndicator = true
return manager
}()
-> manager.desiredAccuracy = kCLLocationAccuracyBest : 위치 정확도를 최고로 지정
-> startUpdatingLocation() : 객체 생성과 함께 위치 업데이트 시작
-> delegate = self : delegate 지정은 viewDidLoad에서 설정해줘야 한다.
manager.pausesLocationUpdatesAutomatically = false
manager.allowsBackgroundLocationUpdates = true
manager.showsBackgroundLocationIndicator = true
-> 러닝타이머와 mapView는 사용자가 화면을 끄고 이동하더라고 지원해야하기 때문에,
-> pausesLocationUpdatesAutomatically : 메모리와 배터리 성능을 고려하여 애플은 위치업데이트를 자동 종결시키는 것을 디폴트로 하고 있기 때문에, 이 사항은 false로 해줘야한다.
-> allowsBackgroundLocationUpdates : 마찬가지로 앱화면이 백그라운드, 즉 다른 앱을 사용하더라도 위치업데이트는 진행되어야 한다.
-> showsBackgroundLocationIndicator : 백그라운드에서 위치업데이트가 되고 있다는 것을 사용자에게 알려야한다. (배터리 소모, 메모리 사용은 사용자 권한)
-> allowsBackgroundLocationUpdates 를 위해서는 앱 - TARGETS + Capability에서 [Background Modes]를 추가하고, Location updates를 체크해줘야 정상 작동한다.
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
addSubview()
setLayout()
mapView.delegate = self
locationManager.delegate = self
RunningTimerLocationManager.shared.getLocationUsagePermission() //viewDidLoad 되었을 때 권한요청을 할 것인지, 현재 위치를 눌렀을 때 권한요청을 할 것인지
}
override func viewWillDisappear(_ animated: Bool) {
// RunningTimerLocationManager.shared.stopUpdatingLocation() // 러닝 중에 지도가 보인다면, viewWillDisappear 할 때 stopUpdatingLocation()가 호출되면 안됨.
}
-> 뷰가 로드되었을 때,
mapView.delegate = self
locationManager.delegate = self
mapView와 locationManager의 위임자를 해당 ViewController로 지정해줘야, 상기 기능을 사용할 수 있다.
(이 상관관계를 잘 몰라서, 아래에 구현한 매소드를 정의했지만, 해당 ViewController는 상속을 선언하지 않았기 때문에 사용할 수 없었다.)
RunningTimerLocationManager.shared.getLocationUsagePermission()
//viewDidLoad 되었을 때 권한요청을 할 것인지, 현재 위치를 눌렀을 때 권한요청
-> 싱글톤 패턴으로 RunningTimerLocationManager을 정의하고 해당 클래스에서 권한요청 관련 사항을 정의했다. (이전 글 참조)
-> viewWillDisappear에서 stopUpdatingLocation()을 하면 화면을 다른 뷰로 넘기면 위치서비스 업데이트가 중지된다. (선택사항)
//
// RunningTimerManager.swift
// Run-It
//
// Created by Jason Yang on 2/25/24.
//
import Foundation
import CoreLocation
// 위치 권한 요청, 사용자 위치를 계속 요청하므로 추적 배터리 소모 고려 조정 필요
class RunningTimerLocationManager: NSObject, CLLocationManagerDelegate {
static let shared = RunningTimerLocationManager()
var locationManager: CLLocationManager = CLLocationManager()
var previousLocation: CLLocation?
var totalDistance: Double = 0
var startTime: Date?
var pace: Double = 0
var updateLocationClosure: ((CLLocation) -> Void)?
override init() {
super.init()
locationManager.delegate = self
locationManager.startUpdatingLocation()
locationManager.pausesLocationUpdatesAutomatically = false
locationManager.allowsBackgroundLocationUpdates = true
locationManager.showsBackgroundLocationIndicator = true
locationManager.desiredAccuracy = kCLLocationAccuracyBest
}
//위치사용 권한 요청
func getLocationUsagePermission() {
self.locationManager.requestWhenInUseAuthorization()
}
// Call this method to start updating the location
func startUpdatingLocation() {
locationManager.startUpdatingLocation()
}
// Call this method to stop updating the location
func stopUpdatingLocation() {
locationManager.stopUpdatingLocation()
}
//위치서비스의 권한 상태가 변경될 때 호출되는 매서드
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
switch manager.authorizationStatus {
case .authorizedAlways, .authorizedWhenInUse:
self.locationManager.startUpdatingLocation()
print("GPS 권한 설정됨")
case .denied, .restricted:
DispatchQueue.main.async {
self.locationManager.requestWhenInUseAuthorization()
}
print("GPS 권한 요청함")
case .notDetermined:
//결정이 안되었을 경우 권한 요청
DispatchQueue.main.async {
self.locationManager.requestWhenInUseAuthorization()
}
print("GPS 권한 요청함")
default:
return
}
}
// 오류 처리
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Location Error: \(error)")
}
// CLLocationManagerDelegate 메서드
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
// 현재 위치 정보 출력
print("Current location: \(location.coordinate.latitude), \(location.coordinate.longitude)")
if let previousLocation = self.previousLocation {
// 이전 위치와 현재 위치 사이의 거리 계산
let distance = previousLocation.distance(from: location)
totalDistance += distance
// 계산된 거리 정보 출력
print("Distance from previous location: \(distance) meters")
print("Total distance: \(totalDistance) meters")
} else {
// 이전 위치 정보가 없을 경우, 처음 위치 업데이트를 받은 것임
print("Starting location updates...")
}
// 새 위치를 이전 위치로 업데이트
self.previousLocation = location
// 위치 업데이트 클로저 호출 (새 위치 데이터 전달)
updateLocationClosure?(location)
}
}
맵뷰로 잠시 넘어가서 이어가자면,
lazy var mapView: MKMapView = {
let mapView = MKMapView()
mapView.showsUserLocation = true
mapView.isZoomEnabled = true
mapView.isRotateEnabled = true
mapView.mapType = MKMapType.standard
mapView.showsCompass = false
return mapView
}()
MKMapView는 기본적으로 UIView, NSCoding을 따르고,
delegate로 MKMapViewDelegate을 통해 해당되는 프로퍼티와 매소드를 사용할 수 있다.
https://developer.apple.com/documentation/mapkit/mkmapviewdelegate
MKMapViewDelegate에서는 지도이동, 맵데이터 로딩, 사용자 추적, 맵주석 뷰, 맵주석의 이동 시, 주석 선택 혹은 미선택 시, 맵 층의 관리에 관한 다양한 매소드가 있다.
//MARK: - MKMapViewDelegate
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
guard let customAnnotation = view.annotation as? CustomAnnotation, let mapItem = customAnnotation.mapItem else { return }
mapView.removeOverlays(mapView.overlays)
let annotation = CustomAnnotation()
let name = mapItem.name ?? "Unknown"
let address = mapItem.placemark.title ?? "No Address"
let category = "편의점" // 확장 필요
// 어노테이션까지 거리 계산
let userLocation = CLLocation(latitude: mapView.userLocation.coordinate.latitude, longitude: mapView.userLocation.coordinate.longitude)
let annotationLocation = CLLocation(latitude: customAnnotation.coordinate.latitude, longitude: customAnnotation.coordinate.longitude)
let distance = userLocation.distance(from: annotationLocation)
let url = mapItem.url?.absoluteString ?? "No URL"
let isFavorite = favoritesViewModel.isFavorite(storeName: annotation.title ?? "", latitude: annotation.coordinate.latitude, longitude: annotation.coordinate.longitude)
// 예시 정보를 `AnnotationInfo`로 생성
let info = AnnotationInfo(
name: name,
category: category,
address: address,
url: url,
latitude: customAnnotation.coordinate.latitude,
longitude: customAnnotation.coordinate.longitude,
isOpenNow: true, // 로직 수정 필요
distance: Int(distance), // 계산된 거리 정보 사용
isFavorite: isFavorite
)
// 사용자 현재 위치와 선택한 어노테이션 위치 사이의 경로 계산 및 표시
let sourceCoordinate = mapView.userLocation.coordinate
let destinationCoordinate = customAnnotation.coordinate
let sourcePlacemark = MKPlacemark(coordinate: sourceCoordinate)
let destinationPlacemark = MKPlacemark(coordinate: destinationCoordinate)
let sourceItem = MKMapItem(placemark: sourcePlacemark)
let destinationItem = MKMapItem(placemark: destinationPlacemark)
let directionRequest = MKDirections.Request()
directionRequest.source = sourceItem
directionRequest.destination = destinationItem
directionRequest.transportType = .walking // 또는 .automobile
let directions = MKDirections(request: directionRequest)
directions.calculate { (response, error) in
guard let response = response else {
if let error = error {
print("Error: \(error)")
}
return
}
let route = response.routes[0]
mapView.addOverlay(route.polyline, level: .aboveRoads)
let rect = route.polyline.boundingMapRect
mapView.setRegion(MKCoordinateRegion(rect), animated: true)
}
// StoreViewController에 정보 전달 및 표시
let storeVC = StoreViewController()
storeVC.stores = [info] // 단일 어노테이션 정보 전달
storeVC.modalPresentationStyle = .formSheet
storeVC.modalTransitionStyle = .coverVertical
storeVC.view.backgroundColor = UIColor.systemBackground
self.present(storeVC, animated: true, completion: nil)
}
func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
}
// 지도에 경로 및 주변원을 표시하기 위한 MKMapViewDelegate에서 MKPolylineRenderer, MKCircleRenderer를 설정
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if overlay is MKPolyline {
let renderer = MKPolylineRenderer(overlay: overlay)
renderer.strokeColor = UIColor.systemIndigo // 경로의 색상을 설정
renderer.lineCap = .round
renderer.lineWidth = 5.0 // 경로의 두께를 설정합니다.
return renderer
// 지도에 서클을 표시하기 위해 MKCircle를 활용
} else if let circleOverlay = overlay as? MKCircle {
let renderer = MKCircleRenderer(circle: circleOverlay)
renderer.fillColor = UIColor.systemIndigo.withAlphaComponent(0.2)
return renderer
}
return MKOverlayRenderer(overlay: overlay)
}
//지도에 표시될 annotation 아이콘 설정 매소드
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation {
return nil
}
switch annotation.title {
case "end":
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "endPin")
if annotationView == nil {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: "endPin")
annotationView?.image = UIImage(named: "DestinationIcon")
} else {
annotationView?.annotation = annotation
}
return annotationView
case "start":
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "startPin")
if annotationView == nil {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: "startPin")
annotationView?.image = UIImage(systemName: "figure.stand")
} else {
annotationView?.annotation = annotation
}
return annotationView
default:
return nil
}
}
-> func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) 는 맵 주석이 선택되었을 때 호출되는 매소드이다.
-> mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRendere 는 맵 계층의 경로나, 서클 등을 추가할 때 정의하는 매소드이다.
-> func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? 는 애플에서 제공하는 기본 주석에 커스텀하거나 정의하는 매소드이다.
'Xcode Study' 카테고리의 다른 글
weatherKit을 활용한 러닝맵에 날씨정보 호출하기 (0) | 2024.03.12 |
---|---|
Apple Developer 등록하기 및 협업하기 (0) | 2024.03.11 |
MapKit으로 출발지, 도착지 그리고 경로 구현하기(.feat CLLocationManagerDelegate, MKMapViewDelegate) (0) | 2024.03.05 |
Mapkit의 지도에 MKLocalSearch활용하여 검색된 annotation 노출하기 (0) | 2024.03.04 |
러닝기록 타이머를 정지하며 코어데이터에 저장하기(작성중) (0) | 2024.03.04 |