Project/HiddenGem
🤔 여러 ViewController에서 위치 권한을 사용할 경우에는?
밤새는 탐험가89
2025. 6. 10. 01:56
https://explorer89.tistory.com/423
✅ 위치 정보 및 권한 설정하기
https://explorer89.tistory.com/159 위치정보를 받아오는 방법참고 사이트 https://velog.io/@maddie/iOS-UIKit-CoreLocation-%EC%82%AC%EC%9A%A9%EB%B2%95-%EC%A0%95%EB%A6%AC" data-og-host="velog.io" data-og-source-url="https://velog.io/@maddie/iOS-
explorer89.tistory.com
🔍 구현하려는 상황
- HomeViewController가 위치 권한 요청 및 위치 갱신 로직을 가지고 있고,
- LocationSearchViewController는 UISheetPresentationController로 띄워지는 뷰이며, 여기서 버튼을 눌러 현재 위치를 다시 가져오고 싶어 함.
- 즉, 위치를 다루는 로직이 HomeViewController에만 있고, LocationSearchViewController는 버튼 이벤트만 처리.
- 위치 권한 로직이 여러 VC에서 중복되면 안 되므로, 재사용 가능한 구조로 만들고 싶음.
✅ 구조 추천
- LocationManagerService: 위치 관련 로직을 담당하는 싱글턴 클래스 생성
import Foundation
import UIKit
import CoreLocation
final class LocationManagerService: NSObject, CLLocationManagerDelegate {
// MARK: - Variable
static let shared = LocationManagerService()
private let manager = CLLocationManager()
private let geocoder = CLGeocoder()
var onUpdateAddress: ((String, CLLocationCoordinate2D) -> Void)?
var onFail: ((String) -> Void)?
// MARK: - Init
override init() {
super.init()
manager.delegate = self
}
// MARK: - Function
func requestAuthorization() {
guard CLLocationManager.locationServicesEnabled() else {
onFail?("위치 서비스가 꺼져 있습니다.")
return
}
manager.requestWhenInUseAuthorization()
}
func startUpdatingLocation() {
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.startUpdatingLocation()
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
handleAuthorization(manager.authorizationStatus)
}
private func handleAuthorization(_ status: CLAuthorizationStatus) {
switch status {
case .authorizedWhenInUse, .authorizedAlways:
startUpdatingLocation()
case .denied, .restricted:
onFail?("LocationDenied")
case .notDetermined:
manager.requestWhenInUseAuthorization()
@unknown default:
break
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
reverseGeoCode(location: location)
//fetchAddressFromCoordinates(location.coordinate)
manager.stopUpdatingLocation()
}
private func reverseGeoCode(location: CLLocation) {
geocoder.reverseGeocodeLocation(location) { [weak self] placemarks, error in
guard let self = self else { return }
if let placemark = placemarks?.first {
var addressComponents: [String] = []
// 주/지역명 추가 (도시명과 동일하지 않은 경우에만 추가)
if let administrativeArea = placemark.administrativeArea, administrativeArea != placemark.locality {
addressComponents.append(administrativeArea)
}
// 도시명 추가
if let locality = placemark.locality {
addressComponents.append(locality)
}
// 도로명 주소 또는 지번 주소 추가 (도로명 주소가 있는 경우 우선적으로 추가)
if let thoroughfare = placemark.thoroughfare {
addressComponents.append(thoroughfare)
if let subThoroughfare = placemark.subThoroughfare {
addressComponents.append(subThoroughfare)
}
} else if let name = placemark.name, !addressComponents.contains(name) {
// 도로명 주소가 없고 지번 주소가 이미 추가된 주소 구성 요소에 포함되지 않은 경우 지번 주소 추가
addressComponents.append(name)
}
let addressString = addressComponents.joined(separator: " ")
let coordinate = location.coordinate
self.onUpdateAddress?(addressString, coordinate)
} else {
self.onFail?("주소 변환에 실패했습니다.")
}
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
onFail?("위치 정보를 가져오지 못했습니다.")
}
// `reloadData` 대체용 (선택사항)
private func reloadDataIfNeeded() {
// 권한이 거부된 후 UI를 갱신하거나 데이터를 다시 로드하고 싶다면 여기에 작성
print("🔄 위치 권한 거부: UI 갱신 필요 시 이곳에서 처리")
}
func setHandlers(
onUpdate: @escaping (String, CLLocationCoordinate2D) -> Void,
onFail: @escaping (String) -> Void
) {
self.onUpdateAddress = onUpdate
self.onFail = onFail
}
}
📍 reverseGeoCode()
- reverseGeoCode(location:) 메서드는 CLLocation 객체를 기반으로 대한민국 행정구역 형식에 맞는 주소 문자열을 생성하는 함수
- Apple의 기본 reverseGeocodeLocation을 그대로 사용할 경우, "인천광역시 인천광역시"와 같이 시·도 이름이 중복되는 문제가 발생
- 이를 해결하기 위해 administrativeArea와 locality를 비교하여 중복을 제거하고, 도로명 주소 또는 지번 주소를 적절히 결합해 보다 자연스럽고 간결한 주소 포맷을 구성
/// CLLocation 객체를 받아 대한민국 행정표기 방식에 맞춘 주소 문자열로 변환합니다.
/// - Apple의 기본 reverse geocoding 결과는 행정구역명이 중복되는 경우가 있어 이를 방지합니다.
/// - 도로명 주소(thoroughfare)가 있을 경우 우선 사용하며, 없을 경우 지번 주소(name)를 사용합니다.
/// - 최종적으로 구성된 주소 문자열과 좌표를 클로저로 전달합니다.
private func reverseGeoCode(location: CLLocation) {
geocoder.reverseGeocodeLocation(location) { [weak self] placemarks, error in
guard let self = self else { return }
if let placemark = placemarks?.first {
var addressComponents: [String] = []
// 시/도(예: 서울특별시, 인천광역시) 추가 (단, 시/군/구와 중복될 경우 생략)
if let administrativeArea = placemark.administrativeArea,
administrativeArea != placemark.locality {
addressComponents.append(administrativeArea)
}
// 시/군/구(예: 강남구, 서구) 추가
if let locality = placemark.locality {
addressComponents.append(locality)
}
// 도로명 주소가 있는 경우 우선적으로 사용
if let thoroughfare = placemark.thoroughfare {
addressComponents.append(thoroughfare)
// 상세 주소 (건물 번호 등) 추가
if let subThoroughfare = placemark.subThoroughfare {
addressComponents.append(subThoroughfare)
}
}
// 도로명 주소가 없는 경우 지번 주소로 대체
else if let name = placemark.name, !addressComponents.contains(name) {
addressComponents.append(name)
}
// 최종 주소 문자열 생성
let addressString = addressComponents.joined(separator: " ")
let coordinate = location.coordinate
// 콜백 클로저를 통해 주소와 좌표 전달
self.onUpdateAddress?(addressString, coordinate)
} else {
self.onFail?("주소 변환에 실패했습니다.")
}
}
}
✅ HomeViewController 에서 사용
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
checkLocation()
}
// 상태바 숨기기
override var prefersStatusBarHidden: Bool {
return true
}
// MARK: - Function
private func checkLocation() {
LocationManagerService.shared.setHandlers(
onUpdate: { [weak self] address, coordinate in
print("✅ 주소: \(address)")
print("📍 위도: \(coordinate.latitude), 경도: \(coordinate.longitude)")
self?.userLocation = address
self?.geocoder = coordinate
},
onFail: { message in
if message == "LocationDenied" {
self.showRequestLocationServiceAlert()
} else {
print("❌ \(message)")
}
}
)
}
✅ LocationSearchViewController
func handleSearchCurrentLocation() {
searchCurrentLocationButton.addTarget(self, action: #selector(didTappedSearchCurrentLocationButton), for: .touchUpInside)
cancelButton.addTarget(self, action: #selector(didTappedCancelButton), for: .touchUpInside)
}
// MARK: - Actions
@objc private func didTappedSearchCurrentLocationButton() {
print("✅ 현재위치 확인 버튼 눌림")
LocationManagerService.shared.setHandlers(
onUpdate: {[weak self] address, coordinate in
self?.delegate?.checkCurrentLocation(with: address, coordinate: coordinate)
self?.dismiss(animated: true)
},
onFail: { message in
print("❌ \(message)")
self.dismiss(animated: true)
}
)
LocationManagerService.shared.startUpdatingLocation()
}
@objc private func didTappedCancelButton() {
dismiss(animated: true)
}