Skip to content

Latest commit

ย 

History

History
391 lines (314 loc) ยท 11.5 KB

File metadata and controls

391 lines (314 loc) ยท 11.5 KB

Core Location AI Reference

์œ„์น˜ ์„œ๋น„์Šค ๋ฐ ์ง€์˜คํŽœ์‹ฑ ๊ฐ€์ด๋“œ. ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ณ  Core Location ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฐœ์š”

Core Location์€ ๊ธฐ๊ธฐ์˜ ์œ„์น˜, ๊ณ ๋„, ๋ฐฉํ–ฅ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค. GPS, Wi-Fi, ์…€๋ฃฐ๋Ÿฌ, ๋น„์ฝ˜์„ ํ™œ์šฉํ•ด ์œ„์น˜๋ฅผ ํŒŒ์•…ํ•ฉ๋‹ˆ๋‹ค.

ํ•„์ˆ˜ Import

import CoreLocation

ํ”„๋กœ์ ํŠธ ์„ค์ • (Info.plist)

<!-- ์•ฑ ์‚ฌ์šฉ ์ค‘ ์œ„์น˜ -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>ํ˜„์žฌ ์œ„์น˜๋ฅผ ์ง€๋„์— ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.</string>

<!-- ํ•ญ์ƒ ์œ„์น˜ (๋ฐฑ๊ทธ๋ผ์šด๋“œ) -->
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์œ„์น˜ ๊ธฐ๋ฐ˜ ์•Œ๋ฆผ์„ ๋ณด๋‚ด๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.</string>

ํ•ต์‹ฌ ๊ตฌ์„ฑ์š”์†Œ

1. CLLocationManager

@Observable
class LocationManager: NSObject, CLLocationManagerDelegate {
    private let manager = CLLocationManager()
    
    var location: CLLocation?
    var authorizationStatus: CLAuthorizationStatus = .notDetermined
    
    override init() {
        super.init()
        manager.delegate = self
        manager.desiredAccuracy = kCLLocationAccuracyBest
    }
    
    func requestPermission() {
        manager.requestWhenInUseAuthorization()
    }
    
    func startUpdating() {
        manager.startUpdatingLocation()
    }
    
    func stopUpdating() {
        manager.stopUpdatingLocation()
    }
    
    // MARK: - Delegate
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        location = locations.last
    }
    
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        authorizationStatus = manager.authorizationStatus
    }
}

2. ๊ถŒํ•œ ์ƒํƒœ

switch manager.authorizationStatus {
case .notDetermined:
    // ์•„์ง ์š”์ฒญ ์•ˆ ํ•จ
    manager.requestWhenInUseAuthorization()
case .restricted, .denied:
    // ์„ค์ •์œผ๋กœ ์œ ๋„
    if let url = URL(string: UIApplication.openSettingsURLString) {
        UIApplication.shared.open(url)
    }
case .authorizedWhenInUse:
    // ์•ฑ ์‚ฌ์šฉ ์ค‘๋งŒ ํ—ˆ์šฉ
    manager.startUpdatingLocation()
case .authorizedAlways:
    // ํ•ญ์ƒ ํ—ˆ์šฉ (๋ฐฑ๊ทธ๋ผ์šด๋“œ ๊ฐ€๋Šฅ)
    manager.startUpdatingLocation()
@unknown default:
    break
}

3. ์ •ํ™•๋„ ์„ค์ •

// ์ตœ๊ณ  ์ •ํ™•๋„ (๋ฐฐํ„ฐ๋ฆฌ ์†Œ๋ชจ ๋†’์Œ)
manager.desiredAccuracy = kCLLocationAccuracyBest

// ๋„ค๋น„๊ฒŒ์ด์…˜์šฉ
manager.desiredAccuracy = kCLLocationAccuracyBestForNavigation

// 10๋ฏธํ„ฐ ์ •ํ™•๋„
manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters

// 100๋ฏธํ„ฐ ์ •ํ™•๋„ (๋ฐฐํ„ฐ๋ฆฌ ์ ˆ์•ฝ)
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters

// ํ‚ฌ๋กœ๋ฏธํ„ฐ ์ •ํ™•๋„
manager.desiredAccuracy = kCLLocationAccuracyKilometer

// ์ตœ์†Œ ์ด๋™ ๊ฑฐ๋ฆฌ (๋ฏธํ„ฐ)
manager.distanceFilter = 10  // 10m ์ด๋™ ์‹œ๋งˆ๋‹ค ์—…๋ฐ์ดํŠธ

์ „์ฒด ์ž‘๋™ ์˜ˆ์ œ

import SwiftUI
import CoreLocation

// MARK: - Location Manager
@Observable
class LocationManager: NSObject, CLLocationManagerDelegate {
    private let manager = CLLocationManager()
    
    var location: CLLocation?
    var placemark: CLPlacemark?
    var authorizationStatus: CLAuthorizationStatus = .notDetermined
    var isLoading = false
    var error: Error?
    
    override init() {
        super.init()
        manager.delegate = self
        manager.desiredAccuracy = kCLLocationAccuracyBest
        manager.distanceFilter = 10
        authorizationStatus = manager.authorizationStatus
    }
    
    func requestPermission() {
        manager.requestWhenInUseAuthorization()
    }
    
    func requestLocation() {
        isLoading = true
        manager.requestLocation()  // ๋‹จ์ผ ์œ„์น˜ ์š”์ฒญ
    }
    
    func startContinuousUpdates() {
        manager.startUpdatingLocation()
    }
    
    func stopUpdates() {
        manager.stopUpdatingLocation()
    }
    
    private func reverseGeocode(_ location: CLLocation) {
        let geocoder = CLGeocoder()
        geocoder.reverseGeocodeLocation(location) { [weak self] placemarks, error in
            self?.placemark = placemarks?.first
        }
    }
    
    // MARK: - CLLocationManagerDelegate
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        isLoading = false
        guard let newLocation = locations.last else { return }
        
        // ์ •ํ™•๋„ ํ•„ํ„ฐ๋ง
        guard newLocation.horizontalAccuracy > 0 && newLocation.horizontalAccuracy < 100 else { return }
        
        location = newLocation
        reverseGeocode(newLocation)
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        isLoading = false
        self.error = error
    }
    
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        authorizationStatus = manager.authorizationStatus
        
        if authorizationStatus == .authorizedWhenInUse || authorizationStatus == .authorizedAlways {
            requestLocation()
        }
    }
}

// MARK: - View
struct LocationView: View {
    @State private var locationManager = LocationManager()
    
    var body: some View {
        NavigationStack {
            VStack(spacing: 24) {
                // ๊ถŒํ•œ ์ƒํƒœ
                StatusBadge(status: locationManager.authorizationStatus)
                
                // ํ˜„์žฌ ์œ„์น˜
                if let location = locationManager.location {
                    VStack(spacing: 8) {
                        Text("ํ˜„์žฌ ์œ„์น˜")
                            .font(.headline)
                        
                        Text("\(location.coordinate.latitude, specifier: "%.4f"), \(location.coordinate.longitude, specifier: "%.4f")")
                            .font(.system(.body, design: .monospaced))
                        
                        if let placemark = locationManager.placemark {
                            Text(formatAddress(placemark))
                                .foregroundStyle(.secondary)
                        }
                        
                        Text("์ •ํ™•๋„: \(Int(location.horizontalAccuracy))m")
                            .font(.caption)
                            .foregroundStyle(.secondary)
                    }
                    .padding()
                    .background(.regularMaterial)
                    .clipShape(RoundedRectangle(cornerRadius: 12))
                } else if locationManager.isLoading {
                    ProgressView("์œ„์น˜ ํ™•์ธ ์ค‘...")
                }
                
                // ๋ฒ„ํŠผ
                VStack(spacing: 12) {
                    if locationManager.authorizationStatus == .notDetermined {
                        Button("์œ„์น˜ ๊ถŒํ•œ ์š”์ฒญ") {
                            locationManager.requestPermission()
                        }
                        .buttonStyle(.borderedProminent)
                    } else if locationManager.authorizationStatus == .authorizedWhenInUse ||
                              locationManager.authorizationStatus == .authorizedAlways {
                        Button("ํ˜„์žฌ ์œ„์น˜ ์ƒˆ๋กœ๊ณ ์นจ") {
                            locationManager.requestLocation()
                        }
                        .buttonStyle(.bordered)
                    } else {
                        Button("์„ค์ •์—์„œ ๊ถŒํ•œ ํ—ˆ์šฉ") {
                            if let url = URL(string: UIApplication.openSettingsURLString) {
                                UIApplication.shared.open(url)
                            }
                        }
                        .buttonStyle(.bordered)
                    }
                }
                
                Spacer()
            }
            .padding()
            .navigationTitle("์œ„์น˜")
        }
    }
    
    func formatAddress(_ placemark: CLPlacemark) -> String {
        [placemark.locality, placemark.thoroughfare, placemark.subThoroughfare]
            .compactMap { $0 }
            .joined(separator: " ")
    }
}

struct StatusBadge: View {
    let status: CLAuthorizationStatus
    
    var body: some View {
        HStack {
            Image(systemName: icon)
            Text(text)
        }
        .font(.caption)
        .padding(.horizontal, 12)
        .padding(.vertical, 6)
        .background(color.opacity(0.2))
        .foregroundStyle(color)
        .clipShape(Capsule())
    }
    
    var icon: String {
        switch status {
        case .authorizedAlways, .authorizedWhenInUse: return "checkmark.circle.fill"
        case .denied, .restricted: return "xmark.circle.fill"
        default: return "questionmark.circle.fill"
        }
    }
    
    var text: String {
        switch status {
        case .authorizedAlways: return "ํ•ญ์ƒ ํ—ˆ์šฉ"
        case .authorizedWhenInUse: return "์•ฑ ์‚ฌ์šฉ ์ค‘ ํ—ˆ์šฉ"
        case .denied: return "๊ฑฐ๋ถ€๋จ"
        case .restricted: return "์ œํ•œ๋จ"
        case .notDetermined: return "๊ถŒํ•œ ํ•„์š”"
        @unknown default: return "์•Œ ์ˆ˜ ์—†์Œ"
        }
    }
    
    var color: Color {
        switch status {
        case .authorizedAlways, .authorizedWhenInUse: return .green
        case .denied, .restricted: return .red
        default: return .orange
        }
    }
}

๊ณ ๊ธ‰ ํŒจํ„ด

1. ์ง€์˜คํŽœ์‹ฑ

func setupGeofence(center: CLLocationCoordinate2D, radius: Double, identifier: String) {
    let region = CLCircularRegion(
        center: center,
        radius: radius,
        identifier: identifier
    )
    region.notifyOnEntry = true
    region.notifyOnExit = true
    
    manager.startMonitoring(for: region)
}

// Delegate
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
    print("์ง„์ž…: \(region.identifier)")
    // ๋กœ์ปฌ ์•Œ๋ฆผ ๋“ฑ
}

func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
    print("์ดํƒˆ: \(region.identifier)")
}

2. ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์œ„์น˜

// 1. Capabilities: Background Modes โ†’ Location updates ์ฒดํฌ
// 2. Info.plist: NSLocationAlwaysAndWhenInUseUsageDescription

func enableBackgroundLocation() {
    manager.allowsBackgroundLocationUpdates = true
    manager.pausesLocationUpdatesAutomatically = false
    manager.showsBackgroundLocationIndicator = true  // ํŒŒ๋ž€ ๋ฐ” ํ‘œ์‹œ
}

3. ๋ฐฉํ–ฅ (Heading)

func startHeadingUpdates() {
    if CLLocationManager.headingAvailable() {
        manager.startUpdatingHeading()
    }
}

func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
    let trueHeading = newHeading.trueHeading  // ์ง„๋ถ ๊ธฐ์ค€ (0-360)
    let magneticHeading = newHeading.magneticHeading  // ์ž๋ถ ๊ธฐ์ค€
    print("๋ฐฉํ–ฅ: \(trueHeading)ยฐ")
}

4. ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ

let seoul = CLLocation(latitude: 37.5665, longitude: 126.9780)
let busan = CLLocation(latitude: 35.1796, longitude: 129.0756)

let distance = seoul.distance(from: busan)  // ๋ฏธํ„ฐ ๋‹จ์œ„
print("์„œ์šธ-๋ถ€์‚ฐ: \(distance / 1000) km")  // ~325 km

์ฃผ์˜์‚ฌํ•ญ

  1. ๊ถŒํ•œ ์š”์ฒญ ํƒ€์ด๋ฐ

    • ์•ฑ ์‹œ์ž‘ ์‹œ ๋ฐ”๋กœ ์š”์ฒญ โŒ
    • ๊ธฐ๋Šฅ ์‚ฌ์šฉ ์ง์ „์— ์š”์ฒญ โœ…
    • ์™œ ํ•„์š”ํ•œ์ง€ ์„ค๋ช… UI ์ถ”๊ฐ€
  2. ๋ฐฐํ„ฐ๋ฆฌ ์ตœ์ ํ™”

    • ํ•„์š”ํ•  ๋•Œ๋งŒ startUpdatingLocation()
    • ๋‹จ์ผ ์š”์ฒญ์€ requestLocation() ์‚ฌ์šฉ
    • distanceFilter ์ ์ ˆํžˆ ์„ค์ •
  3. ์ •ํ™•๋„ vs ๋ฐฐํ„ฐ๋ฆฌ

    • kCLLocationAccuracyBest: GPS ์‚ฌ์šฉ, ๋ฐฐํ„ฐ๋ฆฌ ๋งŽ์ด ์†Œ๋ชจ
    • kCLLocationAccuracyHundredMeters: Wi-Fi/Cell, ์ ˆ์•ฝ
  4. ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ํ…Œ์ŠคํŠธ

    • Features โ†’ Location โ†’ Custom Location
    • ๋˜๋Š” GPX ํŒŒ์ผ๋กœ ๊ฒฝ๋กœ ์‹œ๋ฎฌ๋ ˆ์ด์…˜