일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- AnyObject
- UIAlertAction
- SwiftUI Boolean 값
- 클로저의 캡슐화
- dispatchsource
- 서체관리자
- addannotation
- MKMapItem
- 러닝타이머
- 한국어 개인정보처리방침
- Protocol
- 영문 개인정보처리방침
- WeatherManager
- CoreLocation
- UICollectionViewFlowLayout
- Xcode
- xcode로 날씨앱 만들기
- 러닝기록앱
- CLLocationManagerDelegate
- MKMapViewDelegate
- weatherKit
- Required Reason API
- 단일 책임원칙
- swift
- Timer
- font book
- Startign Assignments
- RunningTimer
- App Store Connect
- weak var
- Today
- Total
VesselWheel
Mapkit의 지도에 MKLocalSearch활용하여 검색된 annotation 노출하기 본문
들어가기 앞서, MapKit은 SwiftUI로 간편하게 구현할 수 있고, 커스텀하기도 편하다.
https://developer.apple.com/documentation/mapkit/
공식문서를 통해서, WWDC 2023에서 발표한 영상과 설명에 따르면, 개발자가 SwiftUI에서 기능 구현을 자유롭게 할 수 있도록 다양한 매소드와 인스턴스를 제공한다.
https://developer.apple.com/wwdc23/10043
23분 정도의 영상인데, 듀토리얼이 간편하고 크게 부담없이 따라할 수 있다.
개발의 시작은 모방이니, 개인적으로 영상을 보면서 따라해보니 크게 어려움없이 실행할 수 있었다.
SwiftUI를 UIkit과 연결하면, SwiftUI의 Mapkit에서 지원하는 다양한 기능들을 사용할 수 있다.
-> UIkit에 SwiftUI 연결하는 방법
하지만 MapViewController와 다른 ViewController 간의 데이터 전송을 위해 통일성을 위해서,
이번에는 기존에 UIKit으로 구현된 MapViewController를 발전시켜서 개발하고자한다.
(SwiftUI와 UIKit 간에도 코어데이터 연동이 가능하다.)
아래는 본격적으로 구현하기에 앞서, 유트브의 간단한 영상을 참고했다.
https://www.youtube.com/watch?v=4zqEl1s2Vas
https://www.youtube.com/watch?v=xngSMYXmh5Q
https://www.youtube.com/watch?v=czmgGFkXzHE&t=20s
//
// RunningViewController.swift
// Running&Eat
//
// Created by Jason Yang on 2/21/24.
//
import UIKit
import SnapKit
import MapKit
import CoreLocation
class RunningMapViewController: UIViewController, MKMapViewDelegate{
//MARK: - UI Properties
lazy var locationManager: CLLocationManager = {
let manager = CLLocationManager()
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.startUpdatingLocation() // startUpdate를 해야 didUpdateLocation 메서드가 호출됨.
manager.delegate = self
manager.allowsBackgroundLocationUpdates = true
manager.showsBackgroundLocationIndicator = true
return manager
}()
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
}()
var currentLocationButton: UIButton = {
let button = UIButton()
var config = UIButton.Configuration.filled()
config.baseBackgroundColor = .systemCyan
config.cornerStyle = .capsule
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
}()
var storeButton: UIButton = {
let button = UIButton()
var config = UIButton.Configuration.filled()
config.baseBackgroundColor = .systemIndigo
config.cornerStyle = .capsule
config.image = UIImage(systemName: "plus")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 20, weight: .medium))
button.configuration = config
button.layer.cornerRadius = 25
button.layer.shadowRadius = 10
button.layer.shadowOpacity = 0.3
button.addTarget(self, action: #selector(didTapFloatingButton), for: .touchUpInside)
return button
}()
var convenienceStoreButton: UIButton = {
let button = UIButton()
var config = UIButton.Configuration.filled()
config.baseBackgroundColor = .systemPink
config.cornerStyle = .capsule
config.image = UIImage(systemName: "storefront")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 20, weight: .medium))
button.configuration = config
button.layer.shadowRadius = 10
button.layer.shadowOpacity = 0.3
button.alpha = 0.0
button.addTarget(self, action: #selector(storeHalfModalButtonAction), for: .touchUpInside)
return button
}()
private var isActive: Bool = false {
didSet {
showActionButtons()
}
}
private var animation: UIViewPropertyAnimator? //회전 버튼 프로퍼티
lazy var startRunningButton: UIButton = {
let button = UIButton()
button.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
button.tintColor = .white
let configuration = UIImage.SymbolConfiguration(pointSize: 50)
if let image = UIImage(systemName: "figure.run", withConfiguration: configuration) {
button.setImage(image, for: .normal)
}
button.backgroundColor = .systemIndigo
button.layer.cornerRadius = 45
button.clipsToBounds = true
button.addTarget(self, action: #selector(TappedstartRunningButton), for: .touchUpInside)
return button
}()
var routeLine: MKPolyline?
lazy var restaurantsearch: MKLocalSearch = {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = "Restaurant"
request.region = self.mapView.region
let filter = MKPointOfInterestFilter(including: [.restaurant])
request.pointOfInterestFilter = filter
return MKLocalSearch(request: request)
}()
lazy var foodMarketsearch: MKLocalSearch = {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = "FoodMarket"
request.region = self.mapView.region
let filter = MKPointOfInterestFilter(including: [.foodMarket])
request.pointOfInterestFilter = filter
return MKLocalSearch(request: request)
}()
lazy var Cafesearch: MKLocalSearch = {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = "Cafe"
request.region = self.mapView.region
let filter = MKPointOfInterestFilter(including: [.cafe])
request.pointOfInterestFilter = filter
return MKLocalSearch(request: request)
}()
//MARK: - LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
addSubview()
setLayout()
getLocationUsagePermission()
}
override func viewWillDisappear(_ animated: Bool) {
self.locationManager.stopUpdatingLocation()
}
//MARK: - @objc functions
@objc private func TappedstartRunningButton() {
print("TappedstartRunningButton()")
let startRunningViewController = StartRunningViewController()
startRunningViewController.modalPresentationStyle = .fullScreen
self.present(startRunningViewController, animated: true)
}
@objc func currentLocationButtonAction() {
locationManager.requestWhenInUseAuthorization()
mapView.showsUserLocation = true
mapView.setUserTrackingMode(.follow, animated: true)
print("확인")
}
@objc private func didTapFloatingButton() {
isActive.toggle() //
}
@objc func storeHalfModalButtonAction() {
// getConvenienceStoreLocation()
// let storeViewController = StoreViewController()
// showMyViewControllerInACustomizedSheet(storeViewController)
}
}
//MARK: - Button setup
extension RunningMapViewController {
// private func getConvenienceStoreLocation() {
// guard let userLocation = locationManager.location else { return } // 사용자의 현재 위치 가져오기
//
// // 사용자의 현재 위치에서 500m 반경 내의 영역 설정
// let region = MKCoordinateRegion(center: userLocation.coordinate, latitudinalMeters: 500, longitudinalMeters: 500)
//
// let search = createFoodMarketSearch(in: region)
// search.start { (response, error) in
// guard let response = response else { return }
//
// for item in response.mapItems {
// let annotation = MKPointAnnotation()
// annotation.coordinate = item.placemark.coordinate
// annotation.title = item.name
// self.mapView.addAnnotation(annotation)
// }
// }
// }
// // 'lazy var' 대신 'func' 사용
// private func createFoodMarketSearch(in region: MKCoordinateRegion) -> MKLocalSearch {
// let request = MKLocalSearch.Request()
// request.naturalLanguageQuery = "FoodMarket"
// request.region = region
//
// let filter = MKPointOfInterestFilter(including: [.foodMarket])
// request.pointOfInterestFilter = filter
//
// return MKLocalSearch(request: request)
// }
private func showActionButtons() {
popButtons()
rotateFloatingButton()
}
private func popButtons() {
if isActive {
convenienceStoreButton.layer.transform = CATransform3DMakeScale(0.4, 0.4, 1)
UIView.animate(withDuration: 0.3, delay: 0.2, usingSpringWithDamping: 0.55, initialSpringVelocity: 0.3, options: [.curveEaseInOut], animations: { [weak self] in
guard let self = self else { return }
self.convenienceStoreButton.layer.transform = CATransform3DIdentity
self.convenienceStoreButton.alpha = 1.0
})
} else {
UIView.animate(withDuration: 0.15, delay: 0.2, options: []) { [weak self] in
guard let self = self else { return }
self.convenienceStoreButton.layer.transform = CATransform3DMakeScale(0.4, 0.4, 0.1)
self.convenienceStoreButton.alpha = 0.0
}
}
}
private func rotateFloatingButton() {
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
let fromValue = isActive ? 0 : CGFloat.pi / 4
let toValue = isActive ? CGFloat.pi / 4 : 0
animation.fromValue = fromValue
animation.toValue = toValue
animation.duration = 0.3
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
storeButton.layer.add(animation, forKey: nil)
}
}
//MARK: - Layout setup
extension RunningMapViewController {
private func addSubview() {
view.addSubview(mapView)
view.addSubview(compassButton)
view.addSubview(startRunningButton)
view.addSubview(currentLocationButton)
view.addSubview(storeButton)
view.addSubview(convenienceStoreButton)
}
private func setLayout() {
mapView.snp.makeConstraints {
$0.top.leading.trailing.equalToSuperview()
$0.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom)
}
compassButton.snp.makeConstraints {
$0.width.height.equalTo(50)
$0.trailing.equalToSuperview().offset(-20)
$0.top.equalToSuperview().offset(120)
}
currentLocationButton.snp.makeConstraints {
$0.width.height.equalTo(50)
$0.trailing.equalToSuperview().offset(-20)
$0.top.equalTo(compassButton.snp.bottom).offset(20)
}
storeButton.snp.makeConstraints {
$0.width.height.equalTo(50)
$0.trailing.equalToSuperview().offset(-20)
$0.top.equalTo(currentLocationButton.snp.bottom).offset(20)
}
convenienceStoreButton.snp.makeConstraints {
$0.top.equalTo(storeButton.snp.bottom).offset(20)
$0.centerX.equalTo(storeButton)
}
startRunningButton.snp.makeConstraints {
$0.width.height.equalTo(90)
$0.centerX.equalToSuperview()
$0.bottom.equalToSuperview().offset(-120)
}
}
} // extension
//MARK: - CLLocationManagerDelegate
extension RunningMapViewController: CLLocationManagerDelegate {
// 사용자에게 위치 관한 요청하기
func getLocationUsagePermission() {
self.locationManager.requestWhenInUseAuthorization()
}
// 위치 정보 업데이트 받기
func startLocationUpdates() {
locationManager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.last {
print("Latitude: \(location.coordinate.latitude), Longitude: \(location.coordinate.longitude)")
let userLocation = location.coordinate
calculateAndShowRoute(from: userLocation)
}
}
// 사용자의 현재 위치를 기반으로 경로를 계산하고 지도에 표시하는 메서드
func calculateAndShowRoute(from userLocation: CLLocationCoordinate2D) {
// 예를 들어, 사용자의 현재 위치로부터 목적지(서울역)까지의 경로를 계산
let destinationCoordinate = CLLocationCoordinate2D(latitude: 37.554722, longitude: 126.970833)
// MKDirectionsRequest를 사용하여 경로를 요청
let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: userLocation))
request.destination = MKMapItem(placemark: MKPlacemark(coordinate: destinationCoordinate))
request.transportType = .walking // 적절한 이동 수단을 선택
// MKDirections를 사용하여 경로를 추적
let directions = MKDirections(request: request)
directions.calculate { response, error in
guard let route = response?.routes.first else {
if let error = error {
print("Error calculating route: \(error.localizedDescription)")
}
return
}
// 경로를 지도에 추가합니다.
self.mapView.addOverlay(route.polyline)
// 경로가 모두 표시되도록 지도를 조정합니다.
let region = MKCoordinateRegion(route.polyline.boundingMapRect)
self.mapView.setRegion(region, animated: true)
}
}
// 지도에 경로를 표시하기 위해 MKMapViewDelegate에서 MKPolylineRenderer를 설정
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if overlay is MKPolyline {
let renderer = MKPolylineRenderer(overlay: overlay)
renderer.strokeColor = UIColor.systemIndigo // 경로의 색상을 설정
renderer.lineWidth = 3 // 경로의 두께를 설정합니다.
return renderer
}
return MKOverlayRenderer(overlay: overlay)
}
// 오류 처리
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Location Error: \(error)")
}
// 위치 관한 상태 감지
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
switch status {
case .authorizedAlways, .authorizedWhenInUse:
startLocationUpdates()
print("GPS 권한 설정됨")
case .restricted, .notDetermined:
print("GPS 권한 설정되지 않음")
DispatchQueue.main.async {
self.getLocationUsagePermission()
}
case .denied:
print("GPS 권한 요청 거부됨")
DispatchQueue.main.async {
self.getLocationUsagePermission()
}
default:
print("GPS: Default")
}
}
}
//extension RunningMapViewController: UISheetPresentationControllerDelegate {
// func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_ sheetPresentationController: UISheetPresentationController) {
// //크기 변경 됐을 경우
// print(sheetPresentationController.selectedDetentIdentifier == .large ? "large" : "medium")
// }
//}
https://developer.apple.com/documentation/mapkit/mklocalsearch
https://developer.apple.com/documentation/mapkit/mklocalsearch/request
-> MKLocalSearch.Request() 클래스를 활용하여, 자연어로 "coffee"를 검색했을 때,
private func removeAnnotationsFromMap() {
// mapView.annotations 배열에서 MKUserLocation 인스턴스를 제외하고 모두 제거
let annotationsToRemove = mapView.annotations.filter { $0 !== mapView.userLocation }
mapView.removeAnnotations(annotationsToRemove)
}
private func getAnnotationLocation() {
guard let currentLocation = self.mapView.userLocation.location else {
print("Failed to get user location")
return
}
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = "GS25" // 원하는 POI 유형을 검색어로 지정
// request.region = mapView.region // 검색 범위를 지정
request.region = MKCoordinateRegion(center: currentLocation.coordinate, latitudinalMeters: 150, longitudinalMeters: 150) // 150m 범위를 지정
let search = MKLocalSearch(request: request)
search.start { (response, error) in
guard let response = response else {
print("Search error: \(error?.localizedDescription ?? "Unknown error")")
return
}
for item in response.mapItems {
// 검색한 POI를 지도에 추가
let annotation = MKPointAnnotation()
annotation.coordinate = item.placemark.coordinate
annotation.title = item.name
self.mapView.addAnnotation(annotation)
}
}
}
-> 동일한 패턴으로 GS25를 자연어로 검색하고, 검색결과인 response의 mapItems를 MKPointAnnotation() 클래스를 활용하여 위경도좌표에 placemark로 mapView란 지도에 GS25 검색결과의 annotation을 추가했다.
https://developer.apple.com/documentation/mapkit/mklocalsearch/request/1452353-naturallanguagequery
https://developer.apple.com/documentation/mapkit/mkmapview/1452069-addannotation
https://developer.apple.com/documentation/mapkit/mkmapview/1452130-removeannotations
https://developer.apple.com/documentation/mapkit/mkpointannotation
https://developer.apple.com/documentation/mapkit/mkmapitem
https://developer.apple.com/documentation/mapkit/mkmapitem/1452134-placemark
'Xcode Study' 카테고리의 다른 글
MapKit에서 실시간 사용자 위치 추적 및 경로 표시하기 (1) | 2024.03.08 |
---|---|
MapKit으로 출발지, 도착지 그리고 경로 구현하기(.feat CLLocationManagerDelegate, MKMapViewDelegate) (0) | 2024.03.05 |
러닝기록 타이머를 정지하며 코어데이터에 저장하기(작성중) (0) | 2024.03.04 |
SceneDeleagte(for 러닝기록 타이머) (0) | 2024.02.29 |
러닝기록 타이머 만들기(3/3)(with dispatchsource ) (0) | 2024.02.27 |