Ver3.5 ๐บ๏ธ iOS ์์น ์๋น์ค์ Core Location ยท MapKit์ผ๋ก ๋ง๋๋ LBS ์ฑ ๊ฐ๋ฐ ๊ฐ์ด๋

๐บ๏ธ iOS ์์น ์๋น์ค์ Core Location ยท MapKit์ผ๋ก ๋ง๋๋ LBS ์ฑ ๊ฐ๋ฐ ๊ฐ์ด๋
Core Location๋ถํฐ MapKit ์ปค์คํฐ๋ง์ด์ง๊น์ง, ์ค์ LBS ์ฑ ๊ฐ๋ฐ์ ๋ชจ๋ ํ๋ฆ์ ํํค์ณ๋ณด์!
์ผ ์์งํ ๋งํด์ ๐ ์์น ๊ธฐ๋ฐ ์ฑ(LBS, Location-Based Service)์ ์ด์ ๊ทธ๋ฅ "์์ผ๋ฉด ์ข์ ๊ธฐ๋ฅ"์ด ์๋๋ผ ์์ผ๋ฉด ์ฑ์ด ๋ฐ์ชฝ์ง๋ฆฌ๊ฐ ๋๋ ์๋์์ใ ใ ๋ฐฐ๋ฌ ์ฑ, ์นด์นด์ค๋งต, ๋น๊ทผ๋ง์ผ, ์ฌ์ง์ด ํผํธ๋์ค ์ฑ๊น์ง ์ ๋ถ ์์น ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋์๊ฐ๊ณ ์์ผ๋๊น. ๊ทธ๋์ ์ค๋์ iOS์์ ์์น ์๋น์ค๋ฅผ ๊ตฌํํ๋ ํต์ฌ ํ๋ ์์ํฌ์ธ Core Location๊ณผ MapKit์ ์ ๋๋ก ๋ฏ์ด๋ณด๋ ค๊ณ ํด!
C ์ธ์ด ๊ธฐ๋ฐ์ ์ ์์ค API๋ถํฐ Swift๋ก ๊ฐ์ธ์ง ๊ณ ์์ค ์ธํฐํ์ด์ค๊น์ง, ์ค์ ๋ก ์ฑ์์ ์ด๋ป๊ฒ ๋์ํ๋์ง ์ฝ๋๋ ๊ฐ๋ ์ ๊ฐ์ด ๋ณด๋ฉด์ ๊ฐ๋ณด์๊ณ ~ ๐
๐ก 1. LBS ์ฑ์ด๋? ์์น ๊ธฐ๋ฐ ์๋น์ค์ ์ธ๊ณ
LBS(Location-Based Service)๋ ๋ง ๊ทธ๋๋ก ์ฌ์ฉ์์ ํ์ฌ ์์น ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์๋น์ค๋ฅผ ์ ๊ณตํ๋ ์ฑ์ด์ผ. ์ฐ๋ฆฌ๊ฐ ๋งค์ผ ์ฐ๋ ์ฑ๋ค ๋๋ถ๋ถ์ด ์ฌ๊ธฐ์ ํด๋นํดใ ใ
๐ ๋ฐฐ๋ฌ ์ฑ ๐บ๏ธ ๋ด๋น๊ฒ์ด์ ๐ ๋ฌ๋ ํธ๋์ปค ๐ ๊ทผ์ฒ ๊ฐ๊ฒ ์ฐพ๊ธฐ
๐ ์นดํ/ํ์ ๐ธ ์์น ํ๊ทธ ์ฌ์ง ๐ฎ AR ๊ฒ์ ๐ฅ ์๊ธ ์๋น์ค
์ด ๋ชจ๋ ์ฑ๋ค์ด iOS์์ Core Location๊ณผ MapKit์ ๊ธฐ๋ฐ์ผ๋ก ๋์ํด!
iOS์์ ์์น ์๋น์ค๋ฅผ ๊ตฌํํ๋ ๋ฐ ์ฐ์ด๋ ํต์ฌ ํ๋ ์์ํฌ๋ ํฌ๊ฒ ๋ ๊ฐ์ง์ผ:
Core Location์ ์์น ๋ฐ์ดํฐ ์์ง์ ์ง์คํ๊ณ , MapKit์ ์ง๋ ์๊ฐํ์ ์ง์คํด. ๋์ ๋ ๋ฆฝ์ ์ผ๋ก๋ ์ธ ์ ์์ด โ ์ง๋ ์์ด ์์น๋ง ์ถ์ ํ๊ฑฐ๋, ์์น ์ถ์ ์์ด ์ ์ ์ธ ์ง๋๋ง ๋ณด์ฌ์ค ์๋ ์๊ฑฐ๋ !
๐ต 2. Core Location ํ๋ ์์ํฌ ํํค์น๊ธฐ
2-1. CLLocationManager โ ์์น ์๋น์ค์ ์ฌ์ฅ
CLLocationManager๋ Core Location์ ํต์ฌ ํด๋์ค์ผ. ์ด ์น๊ตฌ๊ฐ ์์ผ๋ฉด ์์น ์๋น์ค ์์ฒด๊ฐ ๋ถ๊ฐ๋ฅํดใ
ใ
์๋ฅผ ํตํด์ ๊ถํ ์์ฒญ, ์์น ์
๋ฐ์ดํธ ์์/์ค์ง, ์ง์คํ์ฑ ์ค์ ๋ฑ ๋ชจ๋ ๊ฑธ ์ ์ดํด.
import CoreLocation
class LocationManager: NSObject, CLLocationManagerDelegate {
// CLLocationManager ์ธ์คํด์ค ์์ฑ
private let locationManager = CLLocationManager()
override init() {
super.init()
// ๋ธ๋ฆฌ๊ฒ์ดํธ ์ค์ - ์์น ์
๋ฐ์ดํธ๋ฅผ ๋ฐ์ ๊ฐ์ฒด ์ง์
locationManager.delegate = self
// ์์น ์ ํ๋ ์ค์ (๋ฐฐํฐ๋ฆฌ vs ์ ๋ฐ๋ ํธ๋ ์ด๋์คํ)
locationManager.desiredAccuracy = kCLLocationAccuracyBest
// ์ต์ ์ด๋ ๊ฑฐ๋ฆฌ ์ค์ (๋ฏธํฐ ๋จ์) - ์ด ๊ฑฐ๋ฆฌ ์ด์ ์์ง์ฌ์ผ ์
๋ฐ์ดํธ
locationManager.distanceFilter = 10.0
}
// ์์น ๊ถํ ์์ฒญ
func requestPermission() {
locationManager.requestWhenInUseAuthorization()
}
// ์์น ์
๋ฐ์ดํธ ์์
func startUpdating() {
locationManager.startUpdatingLocation()
}
// ์์น ์
๋ฐ์ดํธ ์ค์ง
func stopUpdating() {
locationManager.stopUpdatingLocation()
}
}
2-2. ์์น ๊ถํ โ ์ด๊ฒ ์ ์ผ ์ค์ํด!
iOS์์ ์์น ์๋น์ค๋ฅผ ์ฐ๋ ค๋ฉด ๋ฐ๋์ ์ฌ์ฉ์์๊ฒ ๊ถํ์ ์์ฒญํด์ผ ํด. ์ ํ์ด ํ๋ผ์ด๋ฒ์์ ์์ฒญ ์๊ฒฉํ๊ฑฐ๋ ใ ใ ๊ถํ ์ข ๋ฅ๋ ํฌ๊ฒ ์ธ ๊ฐ์ง์ผ:
| ๊ถํ ์ข ๋ฅ | ์ค๋ช | ์ฌ์ฉ ์๋๋ฆฌ์ค |
|---|---|---|
| When In Use | ์ฑ์ด ํฌ๊ทธ๋ผ์ด๋์ ์์ ๋๋ง ์์น ์ ๊ทผ | ์ง๋ ์ฑ, ์์ ์ฃผ๋ฌธ |
| Always | ๋ฐฑ๊ทธ๋ผ์ด๋์์๋ ์์น ์ ๊ทผ ๊ฐ๋ฅ | ๋ฌ๋ ํธ๋์ปค, ์ง์คํ์ฑ |
| Denied/Restricted | ์ฌ์ฉ์๊ฐ ๊ฑฐ๋ถํ๊ฑฐ๋ ์์คํ ์ ํ | ์ค์ ์ฑ์ผ๋ก ์ ๋ ํ์ |
๊ถํ ์์ฒญ ์ ์ ๋ฐ๋์
Info.plist์ ์ฌ์ฉ ๋ชฉ์ ๋ฌธ์์ด์ ์ถ๊ฐํด์ผ ํด. ์์ผ๋ฉด ์ฑ์ด ํฌ๋์๋๊ฑฐ๋ ์ฑ์คํ ์ด ์ฌ์ฌ์์ ๋ฆฌ์ ๋นํด!NSLocationWhenInUseUsageDescription โ "์ฑ ์ฌ์ฉ ์ค ์์น ์ ๊ทผ ์ด์ "NSLocationAlwaysAndWhenInUseUsageDescription โ "ํญ์ ์์น ์ ๊ทผ ์ด์ "NSLocationAlwaysUsageDescription โ iOS 10 ์ดํ ํธํ์ฉ
// ๊ถํ ์ํ ๋ณํ ๊ฐ์ง (iOS 14+)
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
switch manager.authorizationStatus {
case .notDetermined:
// ์์ง ๊ฒฐ์ ์ ๋จ - ๊ถํ ์์ฒญ
manager.requestWhenInUseAuthorization()
case .authorizedWhenInUse:
// ์ฑ ์ฌ์ฉ ์ค ํ์ฉ - ์์น ์
๋ฐ์ดํธ ์์ ๊ฐ๋ฅ
manager.startUpdatingLocation()
case .authorizedAlways:
// ํญ์ ํ์ฉ - ๋ฐฑ๊ทธ๋ผ์ด๋ ์์น๋ ๊ฐ๋ฅ
manager.startUpdatingLocation()
case .denied:
// ๊ฑฐ๋ถ๋จ - ์ค์ ์ฑ์ผ๋ก ์ ๋
print("์์น ๊ถํ์ด ๊ฑฐ๋ถ๋์์ต๋๋ค. ์ค์ ์์ ๋ณ๊ฒฝํด์ฃผ์ธ์.")
case .restricted:
// ์์คํ
์ ํ (์๋
๋ณดํธ ๋ฑ)
print("์์น ์๋น์ค๊ฐ ์ ํ๋์ด ์์ต๋๋ค.")
@unknown default:
break
}
}
2-3. CLLocation โ ์์น ๋ฐ์ดํฐ ๊ฐ์ฒด
CLLocation์ ์ค์ ์์น ์ ๋ณด๋ฅผ ๋ด๋ ๊ฐ์ฒด์ผ. ๋จ์ํ ์๋/๊ฒฝ๋๋ง ์๋ ๊ฒ ์๋๋ผ ์์ฒญ ๋ง์ ์ ๋ณด๋ฅผ ํฌํจํ๊ณ ์์ด:
coordinate.latitude ์๋ (-90 ~ 90)
coordinate.longitude ๊ฒฝ๋ (-180 ~ 180)
altitude ๊ณ ๋ (๋ฏธํฐ)
horizontalAccuracy ์ํ ์ ํ๋ (๋ฏธํฐ, ์์์๋ก ์ ํ)
verticalAccuracy ์์ง ์ ํ๋ (๋ฏธํฐ)
speed ์๋ (m/s)
course ์ด๋ ๋ฐฉํฅ (0~360๋, ๋ถ์ชฝ์ด 0)
timestamp ์ธก์ ์๊ฐ
// ์์น ์
๋ฐ์ดํธ ์์ ๋ธ๋ฆฌ๊ฒ์ดํธ ๋ฉ์๋
func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
// ๊ฐ์ฅ ์ต๊ทผ ์์น ๊ฐ์ ธ์ค๊ธฐ
guard let location = locations.last else { return }
let latitude = location.coordinate.latitude
let longitude = location.coordinate.longitude
let accuracy = location.horizontalAccuracy
let speed = location.speed // m/s
let altitude = location.altitude
// ์ ํ๋ ํํฐ๋ง - ๋๋ฌด ๋ถ์ ํํ ๋ฐ์ดํฐ๋ ๋ฌด์
// horizontalAccuracy๊ฐ ์์๋ฉด ์ ํจํ์ง ์์ ๋ฐ์ดํฐ
guard accuracy > 0 && accuracy < 100 else { return }
print("์์น: \(latitude), \(longitude)")
print("์ ํ๋: \(accuracy)m")
print("์๋: \(speed * 3.6)km/h") // m/s โ km/h ๋ณํ
}
// ์์น ์ค๋ฅ ์ฒ๋ฆฌ
func locationManager(_ manager: CLLocationManager,
didFailWithError error: Error) {
if let clError = error as? CLError {
switch clError.code {
case .denied:
print("์์น ์ ๊ทผ ๊ฑฐ๋ถ๋จ")
case .locationUnknown:
print("์์น๋ฅผ ์ผ์์ ์ผ๋ก ์ ์ ์์")
default:
print("์์น ์ค๋ฅ: \(clError.localizedDescription)")
}
}
}
2-4. ์์น ์ ํ๋ ์ค์ โ ๋ฐฐํฐ๋ฆฌ์์ ํํ
์์น ์ ํ๋๋ ๋ฐฐํฐ๋ฆฌ ์๋ชจ์ ์ง๊ฒฐ๋ผ. ์ ํํ ์๋ก ๋ฐฐํฐ๋ฆฌ๋ฅผ ๋ง์ด ๋จน์ดใ ใ ์ฑ์ ์ฉ๋์ ๋ง๊ฒ ์ ์ ํ ์ ํํด์ผ ํด:
| ์ ํ๋ ์์ | ์ค์ฐจ ๋ฒ์ | ๋ฐฐํฐ๋ฆฌ ์๋ชจ | ์ถ์ฒ ์ฉ๋ |
|---|---|---|---|
kCLLocationAccuracyBestForNavigation |
~1m | ๋งค์ฐ ๋์ | ๋ด๋น๊ฒ์ด์ , AR |
kCLLocationAccuracyBest |
~5m | ๋์ | ๋ฌ๋ ํธ๋์ปค |
kCLLocationAccuracyNearestTenMeters |
~10m | ์ค๊ฐ | ์ผ๋ฐ ์ง๋ ์ฑ |
kCLLocationAccuracyHundredMeters |
~100m | ๋ฎ์ | ๋ ์จ, ์ง์ญ ์ ๋ณด |
kCLLocationAccuracyKilometer |
~1km | ๋งค์ฐ ๋ฎ์ | ๋์ ์์ค ์์น |
kCLLocationAccuracyThreeKilometers |
~3km | ์ต์ | ๊ด์ญ ์ง์ญ ์๋น์ค |
2-5. ์ง์คํ์ฑ โ ์์ญ ์ง์ /์ดํ ๊ฐ์ง
์ง์คํ์ฑ(Geofencing)์ ํน์ ์ง๋ฆฌ์ ์์ญ์ ์ค์ ํด๋๊ณ , ์ฌ์ฉ์๊ฐ ๊ทธ ์์ญ์ ์ง์ ํ๊ฑฐ๋ ์ดํํ ๋ ์๋ฆผ์ ๋ฐ๋ ๊ธฐ๋ฅ์ด์ผ. ํธ์์ ์ฑ์์ "๊ทผ์ฒ์ ์์ด์!" ์๋ฆผ ๊ฐ์ ๊ฑฐใ ใ
// ์ง์คํ์ค ์์ญ ์ค์
func setupGeofence() {
// ๋ชจ๋ํฐ๋งํ ์ค์ฌ ์ขํ (์: ์์ธ ์์ฒญ)
let center = CLLocationCoordinate2D(latitude: 37.5665, longitude: 126.9780)
// ์ํ ์์ญ ์์ฑ (๋ฐ๊ฒฝ 200๋ฏธํฐ)
let region = CLCircularRegion(
center: center,
radius: 200.0, // ๋ฏธํฐ ๋จ์
identifier: "์์ธ์์ฒญ_์์ญ"
)
// ์ง์
/์ดํ ์ด๋ฒคํธ ์ค์
region.notifyOnEntry = true // ์ง์
์ ์๋ฆผ
region.notifyOnExit = true // ์ดํ ์ ์๋ฆผ
// ๋ชจ๋ํฐ๋ง ์์
locationManager.startMonitoring(for: region)
}
// ์์ญ ์ง์
๊ฐ์ง
func locationManager(_ manager: CLLocationManager,
didEnterRegion region: CLRegion) {
print("์์ญ ์ง์
: \(region.identifier)")
// ํธ์ ์๋ฆผ ๋ฐ์ก ๋ฑ์ ๋ก์ง
sendLocalNotification(title: "๋์ฐฉ!", body: "์์ธ์์ฒญ ๊ทผ์ฒ์ ๋์ฐฉํ์ด์!")
}
// ์์ญ ์ดํ ๊ฐ์ง
func locationManager(_ manager: CLLocationManager,
didExitRegion region: CLRegion) {
print("์์ญ ์ดํ: \(region.identifier)")
}
// ์ง์คํ์ค ์ ํ์ฌํญ
// - ๊ธฐ๊ธฐ๋น ์ต๋ 20๊ฐ ์์ญ ๋์ ๋ชจ๋ํฐ๋ง ๊ฐ๋ฅ
// - ์ต์ ๋ฐ๊ฒฝ: 100๋ฏธํฐ (์ค์ ๋ก๋ ๋ ํด ์ ์์)
// - Always ๊ถํ ํ์ (๋ฐฑ๊ทธ๋ผ์ด๋ ๋์)
์ง์คํ์ฑ์ GPS๋ง ์ฐ๋ ๊ฒ ์๋๋ผ Wi-Fi, ์ ๋ฃฐ๋ฌ ์ ํธ๋ ํ์ฉํด์ ๋ฐฐํฐ๋ฆฌ๋ฅผ ํจ์จ์ ์ผ๋ก ์จ. ๊ทธ๋์ Always ๊ถํ์ด ์์ด๋ ์๊ฐ๋ณด๋ค ๋ฐฐํฐ๋ฆฌ๋ฅผ ๋ง์ด ์ ๋จน์ด! ๋จ, ๊ธฐ๊ธฐ๋น ์ต๋ 20๊ฐ ์์ญ ์ ํ์ด ์์ผ๋๊น ์ฐ์ ์์ ๊ด๋ฆฌ๊ฐ ์ค์ํด.
๐บ๏ธ 3. MapKit ํ๋ ์์ํฌ โ ์ง๋๋ฅผ ๋ด ์์ผ๋ก!
3-1. MKMapView ๊ธฐ๋ณธ ์ค์
MapKit์ ํต์ฌ์ MKMapView์ผ. ์ด ๋ทฐ ํ๋๋ก Apple Maps๋ฅผ ์ฑ์ ํต์งธ๋ก ์ง์ด๋ฃ์ ์ ์์ดใ
ใ
์คํ ๋ฆฌ๋ณด๋๋ ์ฝ๋๋ก ์ถ๊ฐํ ์ ์์ด.
import MapKit
class MapViewController: UIViewController, MKMapViewDelegate {
@IBOutlet weak var mapView: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
// ๋ธ๋ฆฌ๊ฒ์ดํธ ์ค์
mapView.delegate = self
// ์ง๋ ํ์
์ค์
mapView.mapType = .standard // ๊ธฐ๋ณธ ์ง๋
// mapView.mapType = .satellite // ์์ฑ ์ฌ์ง
// mapView.mapType = .hybrid // ์์ฑ + ๋๋ก
// mapView.mapType = .hybridFlyover // 3D ์์ฑ
// ์ฌ์ฉ์ ์์น ํ์ (ํ๋ ์ )
mapView.showsUserLocation = true
// ๋์นจ๋ฐ ํ์
mapView.showsCompass = true
// ์ถ์ฒ ํ์
mapView.showsScale = true
// ๊ตํต ์ ๋ณด ํ์
mapView.showsTraffic = true
// ๊ฑด๋ฌผ 3D ํ์
mapView.showsBuildings = true
// ์ด๊ธฐ ์ง๋ ์์ญ ์ค์ (์์ธ ์ค์ฌ)
let seoulCenter = CLLocationCoordinate2D(
latitude: 37.5665,
longitude: 126.9780
)
let span = MKCoordinateSpan(
latitudeDelta: 0.05, // ์๋ ๋ฒ์ (์์์๋ก ํ๋)
longitudeDelta: 0.05 // ๊ฒฝ๋ ๋ฒ์
)
let region = MKCoordinateRegion(center: seoulCenter, span: span)
mapView.setRegion(region, animated: true)
}
}
3-2. ์ด๋ ธํ ์ด์ โ ์ง๋ ์์ ํ ๊ฝ๊ธฐ
์ด๋ ธํ ์ด์ (Annotation)์ ์ง๋ ์์ ํน์ ์์น๋ฅผ ํ์ํ๋ ๋ง์ปค์ผ. ์นด์นด์ค๋งต์์ ๊ฐ๊ฒ ์์น ํ์ํ๋ ๊ทธ ํ๋ค์ด ๋ค ์ด๋ ธํ ์ด์ ์ด์ผใ ใ
// ์ปค์คํ
์ด๋
ธํ
์ด์
๋ชจ๋ธ ์ ์
class CustomAnnotation: NSObject, MKAnnotation {
var coordinate: CLLocationCoordinate2D
var title: String?
var subtitle: String?
var category: String // ์นดํ
๊ณ ๋ฆฌ (์นดํ, ์๋น ๋ฑ)
init(coordinate: CLLocationCoordinate2D,
title: String,
subtitle: String,
category: String) {
self.coordinate = coordinate
self.title = title
self.subtitle = subtitle
self.category = category
}
}
// ์ด๋
ธํ
์ด์
์ถ๊ฐ
func addAnnotations() {
let places = [
("์คํ๋ฒ
์ค ๊ฐ๋จ์ ", "์์ธ ๊ฐ๋จ๊ตฌ", 37.4979, 127.0276, "์นดํ"),
("๋งฅ๋๋ ๋ ํ๋์ ", "์์ธ ๋งํฌ๊ตฌ", 37.5563, 126.9236, "ํจ์คํธํธ๋"),
("๊ตญ๋ฆฝ์ค์๋ฐ๋ฌผ๊ด", "์์ธ ์ฉ์ฐ๊ตฌ", 37.5236, 126.9803, "๋ฌธํ")
]
for place in places {
let annotation = CustomAnnotation(
coordinate: CLLocationCoordinate2D(
latitude: place.2,
longitude: place.3
),
title: place.0,
subtitle: place.1,
category: place.4
)
mapView.addAnnotation(annotation)
}
}
// ์ด๋
ธํ
์ด์
๋ทฐ ์ปค์คํฐ๋ง์ด์ง
func mapView(_ mapView: MKMapView,
viewFor annotation: MKAnnotation) -> MKAnnotationView? {
// ์ฌ์ฉ์ ์์น ์ด๋
ธํ
์ด์
์ ๊ธฐ๋ณธ ๋ทฐ ์ฌ์ฉ
guard let customAnnotation = annotation as? CustomAnnotation else {
return nil
}
let identifier = "CustomPin"
var annotationView = mapView.dequeueReusableAnnotationView(
withIdentifier: identifier
) as? MKMarkerAnnotationView
if annotationView == nil {
annotationView = MKMarkerAnnotationView(
annotation: customAnnotation,
reuseIdentifier: identifier
)
annotationView?.canShowCallout = true // ํญ ์ ๋งํ์ ํ์
} else {
annotationView?.annotation = customAnnotation
}
// ์นดํ
๊ณ ๋ฆฌ๋ณ ์์ ์ค์
switch customAnnotation.category {
case "์นดํ":
annotationView?.markerTintColor = .brown
annotationView?.glyphText = "โ"
case "ํจ์คํธํธ๋":
annotationView?.markerTintColor = .red
annotationView?.glyphText = "๐"
case "๋ฌธํ":
annotationView?.markerTintColor = .purple
annotationView?.glyphText = "๐๏ธ"
default:
annotationView?.markerTintColor = .blue
}
return annotationView
}
3-3. ์ค๋ฒ๋ ์ด โ ์ ๊ณผ ๋ฉด์ผ๋ก ์ง๋ ๊พธ๋ฏธ๊ธฐ
์ค๋ฒ๋ ์ด(Overlay)๋ ์ง๋ ์์ ์ , ์, ๋ค๊ฐํ ๋ฑ์ ๊ทธ๋ฆฌ๋ ๊ธฐ๋ฅ์ด์ผ. ๊ฒฝ๋ก ํ์๋ ํน์ ๊ตฌ์ญ ๊ฐ์กฐํ ๋ ์ฐ์ฌ:
// ์ํ ์ค๋ฒ๋ ์ด (์ง์คํ์ค ์๊ฐํ)
func addCircleOverlay() {
let center = CLLocationCoordinate2D(latitude: 37.5665, longitude: 126.9780)
let circle = MKCircle(center: center, radius: 500) // ๋ฐ๊ฒฝ 500m
mapView.addOverlay(circle)
}
// ๊ฒฝ๋ก ์ ์ค๋ฒ๋ ์ด
func addPolylineOverlay() {
var coordinates = [
CLLocationCoordinate2D(latitude: 37.5665, longitude: 126.9780),
CLLocationCoordinate2D(latitude: 37.5700, longitude: 126.9820),
CLLocationCoordinate2D(latitude: 37.5750, longitude: 126.9900)
]
let polyline = MKPolyline(coordinates: &coordinates, count: coordinates.count)
mapView.addOverlay(polyline)
}
// ๋ค๊ฐํ ์ค๋ฒ๋ ์ด
func addPolygonOverlay() {
var coordinates = [
CLLocationCoordinate2D(latitude: 37.56, longitude: 126.97),
CLLocationCoordinate2D(latitude: 37.57, longitude: 126.97),
CLLocationCoordinate2D(latitude: 37.57, longitude: 126.99),
CLLocationCoordinate2D(latitude: 37.56, longitude: 126.99)
]
let polygon = MKPolygon(coordinates: &coordinates, count: coordinates.count)
mapView.addOverlay(polygon)
}
// ์ค๋ฒ๋ ์ด ๋ ๋๋ฌ ์ค์ (๋ธ๋ฆฌ๊ฒ์ดํธ)
func mapView(_ mapView: MKMapView,
rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let circle = overlay as? MKCircle {
let renderer = MKCircleRenderer(circle: circle)
renderer.fillColor = UIColor.blue.withAlphaComponent(0.1)
renderer.strokeColor = .blue
renderer.lineWidth = 2
return renderer
}
if let polyline = overlay as? MKPolyline {
let renderer = MKPolylineRenderer(polyline: polyline)
renderer.strokeColor = .systemBlue
renderer.lineWidth = 4
renderer.lineDashPattern = [8, 4] // ์ ์ ํจํด
return renderer
}
if let polygon = overlay as? MKPolygon {
let renderer = MKPolygonRenderer(polygon: polygon)
renderer.fillColor = UIColor.green.withAlphaComponent(0.15)
renderer.strokeColor = .green
renderer.lineWidth = 2
return renderer
}
return MKOverlayRenderer(overlay: overlay)
}
3-4. ์ญ์ง์ค์ฝ๋ฉ โ ์ขํ๋ฅผ ์ฃผ์๋ก!
์ญ์ง์ค์ฝ๋ฉ(Reverse Geocoding)์ ์๋/๊ฒฝ๋ ์ขํ๋ฅผ ์ฌ๋์ด ์ฝ์ ์ ์๋ ์ฃผ์๋ก ๋ณํํ๋ ๊ธฐ๋ฅ์ด์ผ. ๋ฐ๋๋ก ์ฃผ์๋ฅผ ์ขํ๋ก ๋ฐ๊พธ๋ ๊ฑด ์ง์ค์ฝ๋ฉ(Geocoding)์ด๋ผ๊ณ ํด:
// ์ญ์ง์ค์ฝ๋ฉ - ์ขํ โ ์ฃผ์
func reverseGeocode(location: CLLocation) {
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(location) { placemarks, error in
if let error = error {
print("์ญ์ง์ค์ฝ๋ฉ ์คํจ: \(error.localizedDescription)")
return
}
guard let placemark = placemarks?.first else { return }
// ์ฃผ์ ๊ตฌ์ฑ ์์
let country = placemark.country ?? "" // ๊ตญ๊ฐ: ๋ํ๋ฏผ๊ตญ
let administrativeArea = placemark.administrativeArea ?? "" // ์/๋: ์์ธํน๋ณ์
let locality = placemark.locality ?? "" // ์/๊ตฐ/๊ตฌ: ์ค๊ตฌ
let subLocality = placemark.subLocality ?? "" // ๋/์/๋ฉด: ๋ช
๋
let thoroughfare = placemark.thoroughfare ?? "" // ๋๋ก๋ช
let subThoroughfare = placemark.subThoroughfare ?? "" // ๋ฒ์ง
let address = "\(country) \(administrativeArea) \(locality) \(subLocality)"
print("์ฃผ์: \(address)")
}
}
// ์ง์ค์ฝ๋ฉ - ์ฃผ์ โ ์ขํ
func geocodeAddress(address: String) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(address) { placemarks, error in
if let error = error {
print("์ง์ค์ฝ๋ฉ ์คํจ: \(error.localizedDescription)")
return
}
guard let placemark = placemarks?.first,
let location = placemark.location else { return }
let coordinate = location.coordinate
print("์ขํ: \(coordinate.latitude), \(coordinate.longitude)")
// ์ง๋์ ์ด๋
ธํ
์ด์
์ถ๊ฐ
let annotation = MKPointAnnotation()
annotation.coordinate = coordinate
annotation.title = address
self.mapView.addAnnotation(annotation)
}
}
๐ 4. ์ค์ LBS ์ฑ ๊ตฌํ โ ๊ทผ์ฒ ์ฅ์ ์ฐพ๊ธฐ ์ฑ
์ด์ ์ค์ ๋ก ์ธ ์ ์๋ ์์ค์ ์ฝ๋๋ฅผ ๋ณด์! ํ์ฌ ์์น ๊ธฐ๋ฐ์ผ๋ก ๊ทผ์ฒ ์ฅ์๋ฅผ ๊ฒ์ํ๊ณ ์ง๋์ ํ์ํ๋ ์ฑ์ ๋ง๋ค์ด๋ณผ๊ฒใ ใ
4-1. MKLocalSearch โ ์ฃผ๋ณ ์ฅ์ ๊ฒ์
MapKit์๋ MKLocalSearch๋ผ๋ ๊ฐ๋ ฅํ ๊ฒ์ API๊ฐ ๋ด์ฅ๋ผ ์์ด. ๋ณ๋์ ์๋ฒ ์์ด๋ Apple Maps ๋ฐ์ดํฐ๋ก ์ฃผ๋ณ ์ฅ์๋ฅผ ๊ฒ์ํ ์ ์์ด:
// ํ์ฌ ์์น ๊ธฐ๋ฐ ์ฃผ๋ณ ์นดํ ๊ฒ์
func searchNearbyPlaces(coordinate: CLLocationCoordinate2D, query: String) {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query // "์นดํ", "ํธ์์ ", "์ฃผ์ ์" ๋ฑ
// ๊ฒ์ ๋ฐ๊ฒฝ ์ค์ (ํ์ฌ ์์น ์ค์ฌ 1km ๋ฐ๊ฒฝ)
let region = MKCoordinateRegion(
center: coordinate,
latitudinalMeters: 1000,
longitudinalMeters: 1000
)
request.region = region
// ๊ฒฐ๊ณผ ํ์
ํํฐ (iOS 13+)
request.resultTypes = [.pointOfInterest, .address]
let search = MKLocalSearch(request: request)
search.start { response, error in
if let error = error {
print("๊ฒ์ ์คํจ: \(error.localizedDescription)")
return
}
guard let response = response else { return }
// ๊ธฐ์กด ์ด๋
ธํ
์ด์
์ ๊ฑฐ
self.mapView.removeAnnotations(self.mapView.annotations)
// ๊ฒ์ ๊ฒฐ๊ณผ ์ฒ๋ฆฌ
for item in response.mapItems {
let annotation = MKPointAnnotation()
annotation.coordinate = item.placemark.coordinate
annotation.title = item.name
annotation.subtitle = item.placemark.thoroughfare
self.mapView.addAnnotation(annotation)
// ์ถ๊ฐ ์ ๋ณด
print("์ฅ์๋ช
: \(item.name ?? "")")
print("์ ํ๋ฒํธ: \(item.phoneNumber ?? "")")
print("URL: \(item.url?.absoluteString ?? "")")
print("์นดํ
๊ณ ๋ฆฌ: \(item.pointOfInterestCategory?.rawValue ?? "")")
}
// ๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ๋ณด์ด๋๋ก ์ง๋ ์์ญ ์กฐ์
self.mapView.showAnnotations(self.mapView.annotations, animated: true)
}
}
4-2. ๊ฒฝ๋ก ์๋ด โ MKDirections
์ถ๋ฐ์ง์์ ๋ชฉ์ ์ง๊น์ง์ ๊ฒฝ๋ก๋ฅผ ๊ณ์ฐํ๊ณ ์ง๋์ ํ์ํ๋ ๊ธฐ๋ฅ์ด์ผ. ๋ด๋น๊ฒ์ด์ ์ฑ์ ํต์ฌ ๊ธฐ๋ฅ์ด์งใ ใ
// ๊ฒฝ๋ก ๊ณ์ฐ ๋ฐ ํ์
func calculateRoute(from source: CLLocationCoordinate2D,
to destination: CLLocationCoordinate2D) {
// ์ถ๋ฐ์ง ์ค์
let sourcePlacemark = MKPlacemark(coordinate: source)
let sourceMapItem = MKMapItem(placemark: sourcePlacemark)
// ๋ชฉ์ ์ง ์ค์
let destinationPlacemark = MKPlacemark(coordinate: destination)
let destinationMapItem = MKMapItem(placemark: destinationPlacemark)
// ๊ฒฝ๋ก ์์ฒญ ์ค์
let request = MKDirections.Request()
request.source = sourceMapItem
request.destination = destinationMapItem
request.transportType = .automobile // ์๋์ฐจ ๊ฒฝ๋ก
// .walking โ ๋๋ณด
// .transit โ ๋์ค๊ตํต
// .any โ ๋ชจ๋ ์๋จ
request.requestsAlternateRoutes = true // ๋์ ๊ฒฝ๋ก๋ ์์ฒญ
let directions = MKDirections(request: request)
directions.calculate { response, error in
if let error = error {
print("๊ฒฝ๋ก ๊ณ์ฐ ์คํจ: \(error.localizedDescription)")
return
}
guard let response = response else { return }
// ๊ธฐ์กด ์ค๋ฒ๋ ์ด ์ ๊ฑฐ
self.mapView.removeOverlays(self.mapView.overlays)
// ์ฒซ ๋ฒ์งธ ๊ฒฝ๋ก (์ต์ ๊ฒฝ๋ก) ํ์
if let route = response.routes.first {
self.mapView.addOverlay(route.polyline, level: .aboveRoads)
// ๊ฒฝ๋ก ์ ๋ณด ์ถ๋ ฅ
let distance = route.distance / 1000 // km
let time = route.expectedTravelTime / 60 // ๋ถ
print("๊ฑฐ๋ฆฌ: \(String(format: "%.1f", distance))km")
print("์์ ์๊ฐ: \(Int(time))๋ถ")
// ๊ฒฝ๋ก๊ฐ ๋ณด์ด๋๋ก ์ง๋ ์กฐ์
let rect = route.polyline.boundingMapRect
self.mapView.setVisibleMapRect(
rect,
edgePadding: UIEdgeInsets(top: 50, left: 50, bottom: 50, right: 50),
animated: true
)
// ํด๋ฐ์ดํด ์๋ด ์ ๋ณด
for step in route.steps {
print("์๋ด: \(step.instructions)")
print("๊ฑฐ๋ฆฌ: \(step.distance)m")
}
}
}
}
// ๊ฒฝ๋ก ์ค๋ฒ๋ ์ด ๋ ๋๋ฌ (๋ธ๋ฆฌ๊ฒ์ดํธ์ ์ถ๊ฐ)
// func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
// if let polyline = overlay as? MKPolyline {
// let renderer = MKPolylineRenderer(polyline: polyline)
// renderer.strokeColor = .systemBlue
// renderer.lineWidth = 5
// return renderer
// }
// return MKOverlayRenderer(overlay: overlay)
// }
4-3. ์ฌ์ฉ์ ์์น ์ถ์ ๋ฐ ์ง๋ ์ค์ฌ ์ด๋
// ์ฌ์ฉ์ ์์น ์ถ์ ๋ชจ๋ ์ค์
func setupUserTracking() {
// ์ฌ์ฉ์ ์์น๋ฅผ ์ง๋ ์ค์ฌ์ผ๋ก ์๋ ์ถ์
mapView.userTrackingMode = .follow
// .none โ ์ถ์ ์ ํจ
// .follow โ ์์น ์ถ์ (๋ฐฉํฅ ๊ณ ์ )
// .followWithHeading โ ์์น + ๋ฐฉํฅ ์ถ์ (๋์นจ๋ฐ ๋ฐฉํฅ์ผ๋ก ํ์ )
}
// ์์น ์
๋ฐ์ดํธ ์ ์ง๋ ์ค์ฌ ์ด๋
func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last,
location.horizontalAccuracy > 0 else { return }
// ์ฒ์ ์์น ์์ ์์๋ง ์ง๋ ์ค์ฌ ์ด๋
if isFirstLocationUpdate {
let region = MKCoordinateRegion(
center: location.coordinate,
latitudinalMeters: 500,
longitudinalMeters: 500
)
mapView.setRegion(region, animated: true)
isFirstLocationUpdate = false
}
// ํ์ฌ ์์น ์ ์ฅ
currentLocation = location
// ์ด๋ ๊ฒฝ๋ก ๊ธฐ๋ก (๋ฌ๋ ์ฑ ๋ฑ์์ ํ์ฉ)
routeCoordinates.append(location.coordinate)
updateRouteOverlay()
}
// ์ด๋ ๊ฒฝ๋ก ์ค์๊ฐ ์
๋ฐ์ดํธ
private var routeCoordinates: [CLLocationCoordinate2D] = []
func updateRouteOverlay() {
// ๊ธฐ์กด ๊ฒฝ๋ก ์ค๋ฒ๋ ์ด ์ ๊ฑฐ
let existingOverlays = mapView.overlays.filter { $0 is MKPolyline }
mapView.removeOverlays(existingOverlays)
// ์ ๊ฒฝ๋ก ์ค๋ฒ๋ ์ด ์ถ๊ฐ
if routeCoordinates.count >= 2 {
var coords = routeCoordinates
let polyline = MKPolyline(coordinates: &coords, count: coords.count)
mapView.addOverlay(polyline, level: .aboveRoads)
}
}
โก 5. ๋ฐฑ๊ทธ๋ผ์ด๋ ์์น ์ ๋ฐ์ดํธ โ ์ฑ์ด ๊บผ์ ธ๋ ์ถ์ !
๋ฌ๋ ์ฑ์ด๋ ๋ฐฐ๋ฌ ์ฑ์ฒ๋ผ ์ฑ์ด ๋ฐฑ๊ทธ๋ผ์ด๋์ ์์ด๋ ์์น๋ฅผ ๊ณ์ ์ถ์ ํด์ผ ํ๋ ๊ฒฝ์ฐ๊ฐ ์์ด. ์ด๊ฑด ์ค์ ์ด ์ข ๋ณต์กํดใ ใ
๋ฐฑ๊ทธ๋ผ์ด๋ ์์น ์ ๋ฐ์ดํธ๋ ์ฑ์คํ ์ด ์ฌ์ฌ์์ ๋งค์ฐ ์๊ฒฉํ๊ฒ ๊ฒํ ๋ผ. ์ค์ ๋ก ํ์ํ ๊ฒฝ์ฐ์๋ง ์ฌ์ฉํ๊ณ , ์ฌ์ฉ ๋ชฉ์ ์ ๋ช ํํ ์ค๋ช ํด์ผ ํด. ๋ถํ์ํ๊ฒ ์ฌ์ฉํ๋ฉด ๋ฆฌ์ ๋นํ๊ฑฐ๋ ์ฑ์ด ์ญ์ ๋ ์ ์์ด!
// 1. Xcode ํ๋ก์ ํธ ์ค์
// Target โ Signing & Capabilities โ Background Modes
// โ
Location updates ์ฒดํฌ
// 2. Info.plist์ ์ถ๊ฐ
// NSLocationAlwaysAndWhenInUseUsageDescription
// NSLocationAlwaysUsageDescription
// 3. ์ฝ๋ ์ค์
func setupBackgroundLocationUpdates() {
// Always ๊ถํ ์์ฒญ (๋ฐฑ๊ทธ๋ผ์ด๋ ์์น์ ํ์)
locationManager.requestAlwaysAuthorization()
// ๋ฐฑ๊ทธ๋ผ์ด๋ ์์น ์
๋ฐ์ดํธ ํ์ฉ
locationManager.allowsBackgroundLocationUpdates = true
// ์ผ์ ์ ์ง ์๋ ํ์ฉ (๋ฐฐํฐ๋ฆฌ ์ ์ฝ)
// true: ์์คํ
์ด ๋ฐฐํฐ๋ฆฌ ์ ์ฝ์ ์ํด ์๋์ผ๋ก ์ผ์ ์ ์ง ๊ฐ๋ฅ
locationManager.pausesLocationUpdatesAutomatically = false
// ์ค์ ์์น ๋ณ๊ฒฝ ๋ชจ๋ํฐ๋ง (๋ฐฐํฐ๋ฆฌ ํจ์จ์ )
// ์
ํ์ ๋ณ๊ฒฝ ์์๋ง ์
๋ฐ์ดํธ (์ฝ 500m~์km ๋จ์)
locationManager.startMonitoringSignificantLocationChanges()
}
// ์ฑ์ด ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ข
๋ฃ๋ ํ ์ฌ์คํ๋ ๋
// AppDelegate์์ ์ฒ๋ฆฌ
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// ์์น ์
๋ฐ์ดํธ๋ก ์ธํด ์ฌ์คํ๋ ๊ฒฝ์ฐ
if let _ = launchOptions?[.location] {
// LocationManager ์ฌ์ด๊ธฐํ ๋ฐ ์
๋ฐ์ดํธ ์ฌ์์
locationManager.startMonitoringSignificantLocationChanges()
}
return true
}
startMonitoringSignificantLocationChanges() โ ์ ํ์ ๊ธฐ๋ฐ, ๋ฐฐํฐ๋ฆฌ ์๋ชจ ์ต์. ์ฝ 500m ์ด์ ์ด๋ ์ ์ ๋ฐ์ดํธ.
startUpdatingLocation() โ GPS ๊ธฐ๋ฐ, ๋ฐฐํฐ๋ฆฌ ์๋ชจ ๋์. ์ค์๊ฐ ์ถ์ ํ์ ์ ์ฌ์ฉ.
์ง์คํ์ฑ โ ํน์ ์์ญ ์ง์ /์ดํ๋ง ๊ฐ์ง. ๋ฐฐํฐ๋ฆฌ ํจ์จ ์ต๊ณ .
์ฑ์ ์ฉ๋์ ๋ง๊ฒ ์ ์ ํ ๋ฐฉ๋ฒ์ ์ ํํ๋ ๊ฒ ํต์ฌ์ด์ผ!
๐งช 6. ์๋ฎฌ๋ ์ดํฐ์์ ์์น ํ ์คํธํ๊ธฐ
์ค๊ธฐ๊ธฐ ์์ด๋ Xcode ์๋ฎฌ๋ ์ดํฐ์์ ์์น ์๋น์ค๋ฅผ ํ ์คํธํ ์ ์์ด! ์ด๊ฒ ๊ฐ๋ฐํ ๋ ์์ฒญ ํธํดใ ใ
๋ฐฉ๋ฒ 1 โ ๋ฉ๋ด์์ ์ค์ :
์๋ฎฌ๋ ์ดํฐ ์คํ โ Features โ Location โ ์ํ๋ ์์น ์ ํ
(Apple, Apple Store, City Bicycle Ride, City Run, Freeway Drive ๋ฑ ํ๋ฆฌ์ ์ ๊ณต)
๋ฐฉ๋ฒ 2 โ Xcode Debug ๋ฉ๋ด:
์ฑ ์คํ ์ค โ Debug โ Simulate Location โ ์์น ์ ํ
๋ฐฉ๋ฒ 3 โ GPX ํ์ผ๋ก ๊ฒฝ๋ก ์๋ฎฌ๋ ์ด์ :
์ค์ ์ด๋ ๊ฒฝ๋ก๋ฅผ GPX ํ์ผ๋ก ๋ง๋ค์ด์ ์๋ฎฌ๋ ์ดํฐ์ ๋ก๋ํ๋ฉด
์ค์ ๋ก ์ด๋ํ๋ ๊ฒ์ฒ๋ผ ์์น๊ฐ ๋ณํด!
<!-- GPX ํ์ผ ์์ (Seoul_Route.gpx) -->
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="Xcode">
<trk>
<name>์์ธ ํ
์คํธ ๊ฒฝ๋ก</name>
<trkseg>
<trkpt lat="37.5665" lon="126.9780">
<ele>38</ele>
<time>2024-01-01T09:00:00Z</time>
</trkpt>
<trkpt lat="37.5680" lon="126.9800">
<ele>40</ele>
<time>2024-01-01T09:00:30Z</time>
</trkpt>
<trkpt lat="37.5700" lon="126.9830">
<ele>42</ele>
<time>2024-01-01T09:01:00Z</time>
</trkpt>
</trkseg>
</trk>
</gpx>
// GPX ํ์ผ ์ฌ์ฉ๋ฒ:
// 1. Xcode ํ๋ก์ ํธ์ GPX ํ์ผ ์ถ๊ฐ
// 2. Edit Scheme โ Run โ Options โ Default Location โ GPX ํ์ผ ์ ํ
// 3. ์ฑ ์คํ ์ GPX ๊ฒฝ๋ก๋๋ก ์์น๊ฐ ๋ณํจ
๐ฏ 7. SwiftUI์์ MapKit ์ฌ์ฉํ๊ธฐ (iOS 17+)
iOS 17๋ถํฐ MapKit์ด SwiftUI์ ์์ ํ ํตํฉ๋์ด! ๊ธฐ์กด UIKit ๋ฐฉ์๋ณด๋ค ํจ์ฌ ๊ฐ๊ฒฐํ๊ฒ ์ง๋๋ฅผ ๊ตฌํํ ์ ์์ดใ ใ
import SwiftUI
import MapKit
// iOS 17+ SwiftUI Map
struct ContentView: View {
// ์ง๋ ์นด๋ฉ๋ผ ์์น ์ํ
@State private var cameraPosition: MapCameraPosition = .region(
MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 37.5665, longitude: 126.9780),
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
)
)
// ์ ํ๋ ์ฅ์
@State private var selectedPlace: MKMapItem?
// ๊ฒ์ ๊ฒฐ๊ณผ
@State private var searchResults: [MKMapItem] = []
var body: some View {
Map(position: $cameraPosition, selection: $selectedPlace) {
// ์ฌ์ฉ์ ์์น ํ์
UserAnnotation()
// ๊ฒ์ ๊ฒฐ๊ณผ ๋ง์ปค ํ์
ForEach(searchResults, id: \.self) { item in
Marker(item: item)
.tint(.blue)
}
// ์ปค์คํ
์ด๋
ธํ
์ด์
Annotation("์์ธ์์ฒญ", coordinate: CLLocationCoordinate2D(
latitude: 37.5665, longitude: 126.9780
)) {
ZStack {
Circle()
.fill(.blue)
.frame(width: 40, height: 40)
Image(systemName: "building.2")
.foregroundColor(.white)
}
}
}
.mapStyle(.standard(elevation: .realistic)) // 3D ์งํ
.mapControls {
MapUserLocationButton() // ๋ด ์์น ๋ฒํผ
MapCompass() // ๋์นจ๋ฐ
MapScaleView() // ์ถ์ฒ
}
.onChange(of: selectedPlace) { _, newValue in
if let place = newValue {
print("์ ํ๋ ์ฅ์: \(place.name ?? "")")
}
}
}
}
// iOS 17+ ์์น ๊ด๋ฆฌ (Observable)
@Observable
class LocationService: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
var currentLocation: CLLocation?
var authorizationStatus: CLAuthorizationStatus = .notDetermined
override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
}
func requestPermission() {
manager.requestWhenInUseAuthorization()
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
authorizationStatus = manager.authorizationStatus
if authorizationStatus == .authorizedWhenInUse {
manager.startUpdatingLocation()
}
}
func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
currentLocation = locations.last
}
}
iOS 17+์์ SwiftUI Map์ ์ ์ธํ ๋ฌธ๋ฒ์ผ๋ก ํจ์ฌ ๊ฐ๊ฒฐํ๊ฒ ์ง๋๋ฅผ ๊ตฌํํ ์ ์์ด. ํนํ
@Observable ๋งคํฌ๋ก์ ๊ฒฐํฉํ๋ฉด ์์น ์ํ ๊ด๋ฆฌ๊ฐ ์์ฒญ ๊น๋ํด์ ธ. ์ ๊ท ํ๋ก์ ํธ๋ผ๋ฉด SwiftUI Map์ ์ ๊ทน ํ์ฉํ๋ ๊ฑธ ์ถ์ฒํด!
๐ก๏ธ 8. ํ๋ผ์ด๋ฒ์์ ๋ฐฐํฐ๋ฆฌ ์ต์ ํ โ ์ง์ง ์ค์ํ ๋ถ๋ถ!
์์น ์๋น์ค ๊ฐ๋ฐ์์ ๊ธฐ์ ์ ์ธ ๊ตฌํ๋งํผ์ด๋ ์ค์ํ ๊ฒ ํ๋ผ์ด๋ฒ์ ๋ณดํธ์ ๋ฐฐํฐ๋ฆฌ ์ต์ ํ์ผ. ์ด๊ฑธ ์ํํ ํ๋ฉด ์ฑ์คํ ์ด ๋ฆฌ์ ์ ๋ฌผ๋ก ์ด๊ณ ์ฌ์ฉ์ ์ ๋ขฐ๋ ์์ดใ ใ
8-1. ํ๋ผ์ด๋ฒ์ ์ต์ ๊ดํ
- โ ์ต์ ๊ถํ ์์น โ Always ๊ถํ์ด ํ์ ์์ผ๋ฉด When In Use๋ง ์์ฒญ
- โ ์ฌ์ฉ ๋ชฉ์ ๋ช ํํ ๊ณ ์ง โ Info.plist ์ค๋ช ๋ฌธ์์ด์ ๊ตฌ์ฒด์ ์ผ๋ก ์์ฑ
- โ ์์น ๋ฐ์ดํฐ ์ต์ ์์ง โ ํ์ํ ์ ํ๋๋ง ์ฌ์ฉ
- โ ๋ฐ์ดํฐ ์ํธํ ์ ์ฅ โ ์์น ์ด๋ ฅ์ ๋ก์ปฌ์ ์ ์ฅํ ๋ ์ํธํ
- โ ์๋ฒ ์ ์ก ์ต์ํ โ ๊ผญ ํ์ํ ๊ฒฝ์ฐ์๋ง ์๋ฒ๋ก ์์น ์ ์ก
- โ ์ฌ์ฉ์ ์ ์ด๊ถ ์ ๊ณต โ ์ฑ ๋ด์์ ์์น ์ถ์ on/off ๊ธฐ๋ฅ
- โ ๋ฐ์ดํฐ ๋ณด์กด ๊ธฐ๊ฐ ๋ช ์ โ ์์น ๋ฐ์ดํฐ๋ฅผ ์ผ๋ง๋ ๋ณด๊ดํ๋์ง ๊ณ ์ง
- โ ๋ถํ์ ์ ์ฆ์ ์ค์ง โ ์์น๊ฐ ํ์ ์์ ๋ stopUpdatingLocation() ํธ์ถ
8-2. ๋ฐฐํฐ๋ฆฌ ์ต์ ํ ์ ๋ต
// ๋ฐฐํฐ๋ฆฌ ์ต์ ํ๋ฅผ ์ํ ์ ์ํ ์์น ์
๋ฐ์ดํธ
class AdaptiveLocationManager: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
private var isMoving = false
// ์ด๋ ์ค์ผ ๋์ ์ ์ง ์ค์ผ ๋ ๋ค๋ฅธ ์ค์ ์ ์ฉ
func updateAccuracyBasedOnMovement(speed: Double) {
if speed > 1.0 { // 1m/s ์ด์ = ์ด๋ ์ค
if !isMoving {
isMoving = true
// ์ด๋ ์ค: ๋์ ์ ํ๋
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.distanceFilter = 5 // 5m๋ง๋ค ์
๋ฐ์ดํธ
}
} else { // ์ ์ง ์ค
if isMoving {
isMoving = false
// ์ ์ง ์ค: ๋ฎ์ ์ ํ๋๋ก ๋ฐฐํฐ๋ฆฌ ์ ์ฝ
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
manager.distanceFilter = 50 // 50m๋ง๋ค ์
๋ฐ์ดํธ
}
}
}
func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
updateAccuracyBasedOnMovement(speed: location.speed)
}
// ์ฑ์ด ๋ฐฑ๊ทธ๋ผ์ด๋๋ก ๊ฐ ๋ ๋ฐฐํฐ๋ฆฌ ์ ์ฝ ๋ชจ๋
func applicationDidEnterBackground() {
manager.desiredAccuracy = kCLLocationAccuracyKilometer
manager.distanceFilter = 500
}
// ์ฑ์ด ํฌ๊ทธ๋ผ์ด๋๋ก ์ฌ ๋ ์ ํ๋ ๋ณต์
func applicationWillEnterForeground() {
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.distanceFilter = 10
}
}
์ฐธ๊ณ ๋ก ์ฌ๋ฅ๋ท ๊ฐ์ ์ฌ๋ฅ ๊ณต์ ํ๋ซํผ์์๋ "๋ด ๊ทผ์ฒ ์ฌ๋ฅ์ธ ์ฐพ๊ธฐ" ๊ฐ์ ๊ธฐ๋ฅ์ ๊ตฌํํ ๋ ์ด๋ฐ ์์น ์ต์ ํ ๊ธฐ๋ฒ์ด ํ์์ผ! ์ฌ์ฉ์ ๋ฐฐํฐ๋ฆฌ๋ฅผ ์๋ผ๋ฉด์๋ ์ ํํ ์์น ๊ธฐ๋ฐ ๋งค์นญ์ ์ ๊ณตํ๋ ๊ฒ ํต์ฌ์ด๊ฑฐ๋ ๐
๐ง 9. ์์ฃผ ๋ง๋๋ ๋ฌธ์ ์ ํด๊ฒฐ๋ฒ
9-1. ์์น๊ฐ ๊ฐ์๊ธฐ ํ๋ ๋ฌธ์ (Location Spike)
// ์์น ํ ํ์ ํํฐ๋ง
func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
// 1. ์ ํ๋ ํํฐ - ๋ถ์ ํํ ๋ฐ์ดํฐ ๋ฌด์
guard location.horizontalAccuracy >= 0,
location.horizontalAccuracy <= 65 else { return }
// 2. ํ์์คํฌํ ํํฐ - ์ค๋๋ ์บ์ ๋ฐ์ดํฐ ๋ฌด์
let age = -location.timestamp.timeIntervalSinceNow
guard age < 5 else { return } // 5์ด ์ด๋ด ๋ฐ์ดํฐ๋ง ์ฌ์ฉ
// 3. ์๋ ํํฐ - ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ถ๊ฐ๋ฅํ ์๋ ๋ฌด์
if let previousLocation = previousLocation {
let distance = location.distance(from: previousLocation)
let timeInterval = location.timestamp.timeIntervalSince(previousLocation.timestamp)
let calculatedSpeed = distance / timeInterval
// ์์ 300km ์ด์์ด๋ฉด ํ ํ์์ผ๋ก ํ๋จ
guard calculatedSpeed < 83.3 else { return } // 300km/h = 83.3m/s
}
previousLocation = location
// ์ ํจํ ์์น ์ฒ๋ฆฌ
processValidLocation(location)
}
9-2. ์ค๋ด์์ ์์น ์ ํ๋ ์ ํ
์ค๋ด์์๋ GPS ์ ํธ๊ฐ ์ฝํด์ Wi-Fi์ ๋ธ๋ฃจํฌ์ค ๊ธฐ๋ฐ์ผ๋ก ์์น๋ฅผ ๊ณ์ฐํด. ์ด๋
horizontalAccuracy๊ฐ ์์ญ~์๋ฐฑ ๋ฏธํฐ๋ก ์ปค์ง ์ ์์ด.ํด๊ฒฐ์ฑ :
1.
horizontalAccuracy๋ฅผ UI์ ํ์ํด์ ์ฌ์ฉ์์๊ฒ ์๋ ค์ฃผ๊ธฐ2. ์ ํ๋๊ฐ ๋ฎ์ ๋๋ ์์น ์ ๋ฐ์ดํธ๋ฅผ ๋ฌด์ํ๊ฑฐ๋ ๋ง์ง๋ง ์ ํจ ์์น ์ฌ์ฉ
3. iBeacon(๋ธ๋ฃจํฌ์ค ๋น์ฝ)์ ํ์ฉํ ์ค๋ด ์ธก์ ๊ตฌํ ๊ณ ๋ ค
4. Core Motion๊ณผ ๊ฒฐํฉํด์ ๋ณดํ์ ์ถ์ธก ํญ๋ฒ(PDR) ํ์ฉ
9-3. ๊ถํ ๊ฑฐ๋ถ ์ ์ฌ์ฉ์ ์๋ด
// ๊ถํ ๊ฑฐ๋ถ ์ ์ค์ ์ฑ์ผ๋ก ์ ๋
func showLocationPermissionAlert() {
let alert = UIAlertController(
title: "์์น ๊ถํ ํ์",
message: "์ด ๊ธฐ๋ฅ์ ์ฌ์ฉํ๋ ค๋ฉด ์์น ์ ๊ทผ ๊ถํ์ด ํ์ํฉ๋๋ค. ์ค์ ์์ ๊ถํ์ ํ์ฉํด์ฃผ์ธ์.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "์ทจ์", style: .cancel))
alert.addAction(UIAlertAction(title: "์ค์ ์ผ๋ก ์ด๋", style: .default) { _ in
// ์ฑ ์ค์ ํ๋ฉด์ผ๋ก ์ด๋
if let settingsURL = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(settingsURL)
}
})
present(alert, animated: true)
}
// ๊ถํ ์ํ ํ์ธ ํ ์ ์ ํ ์ฒ๋ฆฌ
func checkAndRequestPermission() {
switch locationManager.authorizationStatus {
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
case .denied, .restricted:
showLocationPermissionAlert()
case .authorizedWhenInUse, .authorizedAlways:
locationManager.startUpdatingLocation()
@unknown default:
break
}
}
๐ 10. Core Location vs MapKit ํต์ฌ ์ ๋ฆฌ
| ๊ตฌ๋ถ | Core Location | MapKit |
|---|---|---|
| ์ญํ | ์์น ๋ฐ์ดํฐ ์์ง/์ฒ๋ฆฌ | ์ง๋ ์๊ฐํ/UI |
| ํต์ฌ ํด๋์ค | CLLocationManager, CLLocation | MKMapView, MKAnnotation |
| ๊ถํ ํ์ | โ ํ์ | โ ๋ถํ์ (์ง๋ ํ์๋ง) |
| ๋ฐฐํฐ๋ฆฌ ์ํฅ | ๋์ (GPS ์ฌ์ฉ) | ๋ฎ์ (๋ ๋๋ง๋ง) |
| ์ฃผ์ ๊ธฐ๋ฅ | ์์น ์ถ์ , ์ง์คํ์ฑ, ์ง์ค์ฝ๋ฉ | ์ง๋ ํ์, ์ด๋ ธํ ์ด์ , ๊ฒฝ๋ก |
| ๋ ๋ฆฝ ์ฌ์ฉ | โ ๊ฐ๋ฅ (์ง๋ ์์ด ์์น๋ง) | โ ๊ฐ๋ฅ (์์น ์์ด ์ง๋๋ง) |
| SwiftUI ์ง์ | @Observable๋ก ํตํฉ | Map() ๋ทฐ๋ก ์์ ์ง์ |
LBS ์ฑ ๊ฐ๋ฐ์ ํต์ฌ์ ๊ธฐ์ ์ ๊ตฌํ๊ณผ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ท ํ์ด์ผ.
โ ๊ถํ์ ํ์ํ ์ต์ํ๋ง ์์ฒญ
โก ์ ํ๋๋ ์ฑ ์ฉ๋์ ๋ง๊ฒ ์ ํ
โข ๋ฐฐํฐ๋ฆฌ ์๋ชจ๋ฅผ ํญ์ ๊ณ ๋ ค
โฃ ์์น ๋ฐ์ดํฐ ์ค๋ฅ ์ฒ๋ฆฌ ์ฒ ์ ํ
โค ํ๋ผ์ด๋ฒ์ ์ ์ฑ ๋ช ํํ ๊ณ ์ง
์ด ๋ค์ฏ ๊ฐ์ง๋ง ์ ์ง์ผ๋ ํ๋ฅญํ LBS ์ฑ์ ๋ง๋ค ์ ์์ด! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.