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)
}