2026๋ MapKit SwiftUI ๋ฉด์ : ์ด๋ ธํ ์ด์ , ์ค๋ฒ๋ ์ด, ์ง์ค๋ก์ผ์ด์
iOS ๋ฉด์ ์ ์ํ SwiftUI์ MapKit ๋ง์คํฐํ๊ธฐ: ์ฌ์ฉ์ ์ ์ ์ด๋ ธํ ์ด์ , ์ค๋ฒ๋ ์ด, ์ง์ค๋ก์ผ์ด์ , ์ฅ์ ๊ฒ์, Maps ํตํฉ ํจํด.

์ง๋๋ ๋ง์ iOS ์ ํ๋ฆฌ์ผ์ด์ ์์ ํต์ฌ ๊ธฐ๋ฅ์ ๋๋ค: ๋ฐฐ๋ฌ, ํผํธ๋์ค, ๋ถ๋์ฐ, ์์น ๊ธฐ๋ฐ ์์ ๋คํธ์ํฌ ๋ฑ์ด ์์ต๋๋ค. iOS 17๋ถํฐ MapKit์ ์ง๋ ํตํฉ์ ํฌ๊ฒ ๋จ์ํํ๋ ๋ค์ดํฐ๋ธ SwiftUI API๋ฅผ ์ ๊ณตํฉ๋๋ค. ์ฑ์ฉ ๋ด๋น์๋ ๊ธฐ์ ๋ฉด์ ์์ ์ด๋ฌํ ๊ธฐ์ ์ ์ ๊ธฐ์ ์ผ๋ก ํ๊ฐํฉ๋๋ค.
๊ฐ ์ง๋ฌธ์ ์ค์ ๊ธฐ์ ๋ฉด์ ํ์์ ๋ฐ์ํ๋ฉฐ, ์์ธํ ๋ต๋ณ๊ณผ ์๋ํ๋ ์ฝ๋๋ฅผ ํฌํจํฉ๋๋ค. ๊ฐ๋ ์ ๊ธฐ์ด๋ถํฐ ๊ณ ๊ธ๊น์ง ์งํ๋ฉ๋๋ค.
SwiftUI์์์ ์ง๋ ๊ธฐ์ด
์ง๋ฌธ 1: SwiftUI๋ก ๊ธฐ๋ณธ ์ง๋๋ฅผ ํ์ํ๋ ค๋ฉด ์ด๋ป๊ฒ ํฉ๋๊น?
iOS 17๋ถํฐ SwiftUI๋ ์ด์ ์ UIViewRepresentable ์ ๊ทผ ๋ฐฉ์์ ๋์ฒดํ๋ ๋ค์ดํฐ๋ธ Map ์ปดํฌ๋ํธ๋ฅผ ์ ๊ณตํฉ๋๋ค. Map ๋ทฐ๋ ์นด๋ฉ๋ผ ์์น์ ํ์ํ ์ฝํ
์ธ ๋ฅผ ๋ฐ์ต๋๋ค.
import SwiftUI
import MapKit
struct BasicMapView: View {
// Camera position controlling center and zoom
@State private var cameraPosition: MapCameraPosition = .automatic
var body: some View {
// Basic map without additional content
Map(position: $cameraPosition)
}
}
// Map centered on a specific region
struct CenteredMapView: View {
// Defines a region centered on Paris
@State private var cameraPosition: MapCameraPosition = .region(
MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522),
// Span defines the zoom level (in degrees)
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
)
)
var body: some View {
Map(position: $cameraPosition) {
// Map content (markers, annotations, etc.)
}
.mapStyle(.standard) // Map style: standard, imagery, hybrid
}
}position ์์ฑ์ ๋ค์ํ ๋ชจ๋๋ฅผ ํ์ฉํ๋ ์ด๊ฑฐํ์ธ MapCameraPosition์ ์ฌ์ฉํฉ๋๋ค: .automatic(์๋ ์กฐ์ ), .region(๊ณ ์ ์์ญ), .camera(์์ ์ ์ด) ๋๋ .userLocation(์ฌ์ฉ์ ์ค์ฌ).
์ง๋ฌธ 2: ์ฌ์ฉ ๊ฐ๋ฅํ ์ง๋ ์คํ์ผ์๋ ์ด๋ค ๊ฒ๋ค์ด ์์ต๋๊น?
MapKit์ ๋ค์ํ ์ฌ์ฉ ์ฌ๋ก์ ์ ํฉํ ์ฌ๋ฌ ์ง๋ ์คํ์ผ์ ์ ๊ณตํฉ๋๋ค. .mapStyle() ์์ ์๋ ์ง๋์ ์๊ฐ์ ๋ชจ์ต์ ๊ตฌ์ฑํฉ๋๋ค.
import SwiftUI
import MapKit
struct MapStylesDemo: View {
@State private var position: MapCameraPosition = .automatic
@State private var selectedStyle: MapStyleOption = .standard
var body: some View {
VStack {
Map(position: $position)
.mapStyle(selectedStyle.style)
.frame(height: 400)
// Style selector
Picker("Style", selection: $selectedStyle) {
ForEach(MapStyleOption.allCases) { option in
Text(option.name).tag(option)
}
}
.pickerStyle(.segmented)
.padding()
}
}
}
enum MapStyleOption: String, CaseIterable, Identifiable {
case standard
case imagery
case hybrid
case standardElevated
case imageryElevated
var id: String { rawValue }
var name: String {
switch self {
case .standard: return "Standard"
case .imagery: return "Satellite"
case .hybrid: return "Hybrid"
case .standardElevated: return "Terrain"
case .imageryElevated: return "Satellite 3D"
}
}
var style: MapStyle {
switch self {
case .standard:
// Classic road map
return .standard
case .imagery:
// Satellite view without labels
return .imagery
case .hybrid:
// Satellite with roads and names
return .hybrid
case .standardElevated:
// Standard with 3D terrain
return .standard(elevation: .realistic)
case .imageryElevated:
// Satellite with 3D terrain
return .imagery(elevation: .realistic)
}
}
}.standard ์คํ์ผ์ ํ์๋๋ ์นดํ
๊ณ ๋ฆฌ๋ฅผ ํํฐ๋งํ๋ pointsOfInterest ๋งค๊ฐ๋ณ์๋ฅผ ๋ฐ์ต๋๋ค: .including([.restaurant, .cafe]) ๋๋ .excluding([.nightlife]).
์ง๋ฌธ 3: ์ง๋์ ๋ง์ปค์ ์ด๋ ธํ ์ด์ ์ ์ถ๊ฐํ๋ ค๋ฉด ์ด๋ป๊ฒ ํฉ๋๊น?
๋ง์ปค๋ ์ง๋์์ ๊ด์ฌ ์ง์ ์ ๋ํ๋
๋๋ค. SwiftUI MapKit์ ์ฌ๋ฌ ์ ํ์ ์ ๊ณตํฉ๋๋ค: Marker(ํ์ค ํ), Annotation(์ฌ์ฉ์ ์ ์ ๋ทฐ), MapCircle/MapPolygon(๊ธฐํํ์ ๋ํ).
import SwiftUI
import MapKit
// Place model conforming to Identifiable
struct Place: Identifiable {
let id = UUID()
let name: String
let coordinate: CLLocationCoordinate2D
let category: PlaceCategory
}
enum PlaceCategory {
case restaurant, hotel, attraction
var icon: String {
switch self {
case .restaurant: return "fork.knife"
case .hotel: return "bed.double"
case .attraction: return "star"
}
}
var tint: Color {
switch self {
case .restaurant: return .orange
case .hotel: return .blue
case .attraction: return .purple
}
}
}
struct MarkersMapView: View {
@State private var position: MapCameraPosition = .region(
MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522),
span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
)
)
let places: [Place] = [
Place(name: "Eiffel Tower", coordinate: CLLocationCoordinate2D(latitude: 48.8584, longitude: 2.2945), category: .attraction),
Place(name: "Le Meurice", coordinate: CLLocationCoordinate2D(latitude: 48.8651, longitude: 2.3281), category: .hotel),
Place(name: "Le Jules Verne", coordinate: CLLocationCoordinate2D(latitude: 48.8583, longitude: 2.2944), category: .restaurant)
]
var body: some View {
Map(position: $position) {
// Markers with system icon
ForEach(places) { place in
Marker(place.name, systemImage: place.category.icon, coordinate: place.coordinate)
.tint(place.category.tint)
}
}
}
}
// Custom annotations with SwiftUI view
struct CustomAnnotationsView: View {
@State private var position: MapCameraPosition = .automatic
@State private var selectedPlace: Place?
let places: [Place] = [] // Data
var body: some View {
Map(position: $position, selection: $selectedPlace) {
ForEach(places) { place in
// Annotation with custom view
Annotation(place.name, coordinate: place.coordinate) {
PlaceAnnotationView(place: place, isSelected: selectedPlace?.id == place.id)
}
}
}
}
}
// Custom view for annotation
struct PlaceAnnotationView: View {
let place: Place
let isSelected: Bool
var body: some View {
VStack(spacing: 4) {
// Icon with colored background
Image(systemName: place.category.icon)
.font(.system(size: isSelected ? 20 : 16))
.foregroundStyle(.white)
.padding(8)
.background(place.category.tint)
.clipShape(Circle())
.shadow(radius: 4)
// Name displayed when selected
if isSelected {
Text(place.name)
.font(.caption)
.fontWeight(.medium)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(.ultraThinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
.animation(.spring(duration: 0.3), value: isSelected)
}
}ํต์ฌ ์ฐจ์ด์ : Marker๋ Apple Maps์ ๋ค์ดํฐ๋ธ ๋ ๋๋ง(ํ์ค ํ)์ ์ฌ์ฉํ๋ ๋ฐ๋ฉด, Annotation์ ๋ชจ๋ SwiftUI ๋ทฐ๋ก ์์ ํ ์ฌ์ฉ์ ์ ์๋ฅผ ํ์ฉํฉ๋๋ค.
์ง์ค๋ก์ผ์ด์ ๋ฐ ๊ถํ
์ง๋ฌธ 4: ์์น ๊ถํ์ ์ฒ๋ฆฌํ๊ณ ์ฌ์ฉ์ ์์น๋ฅผ ํ์ํ๋ ค๋ฉด ์ด๋ป๊ฒ ํฉ๋๊น?
์ง์ค๋ก์ผ์ด์
์ ๋ช
์์ ์ธ ๊ถํ๊ณผ ๋ค์ํ ๊ถํ ๋ถ์ฌ ์ํ์ ์ ์ ํ ์ฒ๋ฆฌ๊ฐ ํ์ํฉ๋๋ค. CLLocationManager๊ฐ ๊ถํ ์์ฒญ์ ์ฒ๋ฆฌํฉ๋๋ค.
import SwiftUI
import MapKit
import CoreLocation
@Observable
class LocationManager: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
// Location state
var location: CLLocation?
var authorizationStatus: CLAuthorizationStatus = .notDetermined
var isAuthorized: Bool {
authorizationStatus == .authorizedWhenInUse || authorizationStatus == .authorizedAlways
}
override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
// Get current status
authorizationStatus = manager.authorizationStatus
}
// Request location permission
func requestAuthorization() {
manager.requestWhenInUseAuthorization()
}
// Start location tracking
func startUpdatingLocation() {
guard isAuthorized else { return }
manager.startUpdatingLocation()
}
// Stop tracking to save battery
func stopUpdatingLocation() {
manager.stopUpdatingLocation()
}
// MARK: - CLLocationManagerDelegate
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
authorizationStatus = manager.authorizationStatus
if isAuthorized {
startUpdatingLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// Take the most recent position
location = locations.last
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Location error: \(error.localizedDescription)")
}
}
// View with map and user location
struct UserLocationMapView: View {
@State private var locationManager = LocationManager()
@State private var cameraPosition: MapCameraPosition = .userLocation(fallback: .automatic)
var body: some View {
Map(position: $cameraPosition) {
// Display the blue user location dot
UserAnnotation()
}
.mapControls {
// Button to recenter on position
MapUserLocationButton()
// Compass
MapCompass()
// Scale
MapScaleView()
}
.onAppear {
locationManager.requestAuthorization()
}
.overlay(alignment: .top) {
if !locationManager.isAuthorized {
PermissionBanner()
}
}
}
}
// Banner requesting authorization
struct PermissionBanner: View {
var body: some View {
HStack {
Image(systemName: "location.slash")
Text("Enable location in Settings")
Spacer()
Button("Open") {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}
.buttonStyle(.bordered)
}
.padding()
.background(.ultraThinMaterial)
}
}NSLocationWhenInUseUsageDescription ๋ฐ/๋๋ NSLocationAlwaysUsageDescription ํค๋ ์์น ์ฌ์ฉ์ ๋ํ ๋ช
ํํ ์ค๋ช
๊ณผ ํจ๊ป Info.plist์ ์ ์๋์ด์ผ ํฉ๋๋ค.
์ง๋ฌธ 5: ์ค์๊ฐ์ผ๋ก ์์น๋ฅผ ์ถ์ ํ๊ณ ๊ฒฝ๋ก๋ฅผ ๊ทธ๋ฆฌ๋ ค๋ฉด ์ด๋ป๊ฒ ํฉ๋๊น?
์ค์๊ฐ ์ถ์ ์ ํผํธ๋์ค ๋๋ ๋ด๋น๊ฒ์ด์ ์ฑ์ ํ์์ ์ ๋๋ค. ์ด ์ง๋ฌธ์ ์์น ๋ฐ์ดํฐ์ ์ฐ์์ ์ธ ์คํธ๋ฆผ์ ์ฒ๋ฆฌํ๋ ๋ฅ๋ ฅ์ ์ํํฉ๋๋ค.
import SwiftUI
import MapKit
import CoreLocation
@Observable
class RouteTracker: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
// Position history for the trace
var routeCoordinates: [CLLocationCoordinate2D] = []
var currentLocation: CLLocation?
var isTracking = false
// Route statistics
var totalDistance: CLLocationDistance = 0
private var lastLocation: CLLocation?
override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
// Update even in background
manager.allowsBackgroundLocationUpdates = true
manager.pausesLocationUpdatesAutomatically = false
}
func startTracking() {
manager.requestWhenInUseAuthorization()
manager.startUpdatingLocation()
isTracking = true
routeCoordinates = []
totalDistance = 0
lastLocation = nil
}
func stopTracking() {
manager.stopUpdatingLocation()
isTracking = false
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let newLocation = locations.last else { return }
// Filter inaccurate positions
guard newLocation.horizontalAccuracy < 20 else { return }
currentLocation = newLocation
routeCoordinates.append(newLocation.coordinate)
// Calculate distance traveled
if let last = lastLocation {
totalDistance += newLocation.distance(from: last)
}
lastLocation = newLocation
}
}
struct RouteTrackingView: View {
@State private var tracker = RouteTracker()
@State private var cameraPosition: MapCameraPosition = .userLocation(fallback: .automatic)
var body: some View {
ZStack {
Map(position: $cameraPosition) {
// Current position
UserAnnotation()
// Route trace as polyline
if tracker.routeCoordinates.count > 1 {
MapPolyline(coordinates: tracker.routeCoordinates)
.stroke(.blue, lineWidth: 4)
}
}
.mapStyle(.standard)
// Control interface
VStack {
Spacer()
// Statistics
HStack {
VStack(alignment: .leading) {
Text("Distance")
.font(.caption)
.foregroundStyle(.secondary)
Text(formatDistance(tracker.totalDistance))
.font(.title2)
.fontWeight(.bold)
}
Spacer()
}
.padding()
.background(.ultraThinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
.padding()
// Start/stop button
Button {
if tracker.isTracking {
tracker.stopTracking()
} else {
tracker.startTracking()
}
} label: {
Label(
tracker.isTracking ? "Stop" : "Start",
systemImage: tracker.isTracking ? "stop.fill" : "play.fill"
)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.tint(tracker.isTracking ? .red : .green)
.padding(.horizontal)
.padding(.bottom)
}
}
}
private func formatDistance(_ meters: CLLocationDistance) -> String {
if meters < 1000 {
return String(format: "%.0f m", meters)
} else {
return String(format: "%.2f km", meters / 1000)
}
}
}iOS ๋ฉด์ ์ค๋น๊ฐ ๋์ จ๋์?
์ธํฐ๋ํฐ๋ธ ์๋ฎฌ๋ ์ดํฐ, flashcards, ๊ธฐ์ ํ ์คํธ๋ก ์ฐ์ตํ์ธ์.
์ค๋ฒ๋ ์ด์ ๊ธฐํํ์ ๋ํ
์ง๋ฌธ 6: ์ง๋์ ์ฌ์ฉ์ ์ ์ ์ค๋ฒ๋ ์ด๋ฅผ ๊ทธ๋ฆฌ๋ ค๋ฉด ์ด๋ป๊ฒ ํฉ๋๊น?
์ค๋ฒ๋ ์ด๋ ์ง๋์ ์์ญ, ๊ฒฝ๋ก ๋๋ ์ง์ญ์ ํ์ํ ์ ์๊ฒ ํฉ๋๋ค. SwiftUI MapKit์ MapCircle, MapPolygon, MapPolyline์ ์ ๊ณตํฉ๋๋ค.
import SwiftUI
import MapKit
struct DeliveryZoneMapView: View {
@State private var position: MapCameraPosition = .region(
MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522),
span: MKCoordinateSpan(latitudeDelta: 0.15, longitudeDelta: 0.15)
)
)
// Delivery zones with different times
let deliveryZones: [DeliveryZone] = [
DeliveryZone(
name: "Express Zone",
center: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522),
radius: 2000,
deliveryTime: "15 min",
color: .green
),
DeliveryZone(
name: "Standard Zone",
center: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522),
radius: 5000,
deliveryTime: "30 min",
color: .orange
)
]
var body: some View {
Map(position: $position) {
// Circles for delivery zones
ForEach(deliveryZones) { zone in
MapCircle(center: zone.center, radius: zone.radius)
.foregroundStyle(zone.color.opacity(0.2))
.stroke(zone.color, lineWidth: 2)
}
// Restaurant marker
Marker("Restaurant", systemImage: "storefront", coordinate: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522))
.tint(.red)
}
.overlay(alignment: .bottomLeading) {
// Legend
VStack(alignment: .leading, spacing: 8) {
ForEach(deliveryZones) { zone in
HStack {
Circle()
.fill(zone.color)
.frame(width: 12, height: 12)
Text("\(zone.name) - \(zone.deliveryTime)")
.font(.caption)
}
}
}
.padding()
.background(.ultraThinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 8))
.padding()
}
}
}
struct DeliveryZone: Identifiable {
let id = UUID()
let name: String
let center: CLLocationCoordinate2D
let radius: CLLocationDistance
let deliveryTime: String
let color: Color
}
// Custom polygon (complex geographic zone)
struct PolygonOverlayView: View {
@State private var position: MapCameraPosition = .automatic
// Neighborhood coordinates
let neighborhoodCoordinates: [CLLocationCoordinate2D] = [
CLLocationCoordinate2D(latitude: 48.853, longitude: 2.347),
CLLocationCoordinate2D(latitude: 48.858, longitude: 2.352),
CLLocationCoordinate2D(latitude: 48.862, longitude: 2.348),
CLLocationCoordinate2D(latitude: 48.860, longitude: 2.340),
CLLocationCoordinate2D(latitude: 48.855, longitude: 2.342)
]
var body: some View {
Map(position: $position) {
// Polygon automatically closed
MapPolygon(coordinates: neighborhoodCoordinates)
.foregroundStyle(.blue.opacity(0.3))
.stroke(.blue, lineWidth: 3)
}
}
}์ง๋ฌธ 7: ๋ ์ง์ ์ฌ์ด์ ๊ฒฝ๋ก๋ฅผ ํ์ํ๋ ค๋ฉด ์ด๋ป๊ฒ ํฉ๋๊น?
๊ฒฝ๋ก ๊ณ์ฐ์ Apple ์๋ฒ์์ ๊ฒฝ๋ก๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ์ํด MKDirections๋ฅผ ์ฌ์ฉํฉ๋๋ค. ํ์๋ ๊ทธ ํ MapPolyline์ผ๋ก ์ํ๋ฉ๋๋ค.
import SwiftUI
import MapKit
@Observable
class RouteCalculator {
var route: MKRoute?
var isCalculating = false
var error: String?
// Calculate route between two points
func calculateRoute(from source: CLLocationCoordinate2D, to destination: CLLocationCoordinate2D) async {
isCalculating = true
error = nil
// Create source and destination placemarks
let sourcePlacemark = MKPlacemark(coordinate: source)
let destinationPlacemark = MKPlacemark(coordinate: destination)
// Configure request
let request = MKDirections.Request()
request.source = MKMapItem(placemark: sourcePlacemark)
request.destination = MKMapItem(placemark: destinationPlacemark)
request.transportType = .automobile // .walking, .transit available
request.requestsAlternateRoutes = false
let directions = MKDirections(request: request)
do {
let response = try await directions.calculate()
// Take the first route
route = response.routes.first
} catch {
self.error = error.localizedDescription
}
isCalculating = false
}
}
struct DirectionsMapView: View {
@State private var calculator = RouteCalculator()
@State private var position: MapCameraPosition = .automatic
let startPoint = CLLocationCoordinate2D(latitude: 48.8738, longitude: 2.2950) // Arc de Triomphe
let endPoint = CLLocationCoordinate2D(latitude: 48.8530, longitude: 2.3499) // Notre-Dame
var body: some View {
ZStack {
Map(position: $position) {
// Start and end markers
Marker("Start", systemImage: "car.fill", coordinate: startPoint)
.tint(.green)
Marker("End", systemImage: "flag.fill", coordinate: endPoint)
.tint(.red)
// Route trace
if let route = calculator.route {
MapPolyline(route.polyline)
.stroke(.blue, lineWidth: 5)
}
}
// Route information
if let route = calculator.route {
VStack {
Spacer()
RouteInfoCard(route: route)
}
}
if calculator.isCalculating {
ProgressView("Calculating route...")
.padding()
.background(.ultraThinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
.task {
await calculator.calculateRoute(from: startPoint, to: endPoint)
}
}
}
struct RouteInfoCard: View {
let route: MKRoute
var body: some View {
HStack(spacing: 20) {
VStack(alignment: .leading) {
Text("Distance")
.font(.caption)
.foregroundStyle(.secondary)
Text(formatDistance(route.distance))
.font(.headline)
}
Divider()
.frame(height: 30)
VStack(alignment: .leading) {
Text("Estimated Time")
.font(.caption)
.foregroundStyle(.secondary)
Text(formatDuration(route.expectedTravelTime))
.font(.headline)
}
Spacer()
}
.padding()
.background(.ultraThinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
.padding()
}
private func formatDistance(_ meters: CLLocationDistance) -> String {
let km = meters / 1000
return String(format: "%.1f km", km)
}
private func formatDuration(_ seconds: TimeInterval) -> String {
let minutes = Int(seconds) / 60
if minutes < 60 {
return "\(minutes) min"
} else {
let hours = minutes / 60
let remainingMinutes = minutes % 60
return "\(hours)h \(remainingMinutes)min"
}
}
}์ฅ์ ๊ฒ์ ๋ฐ ์ง์ค์ฝ๋ฉ
์ง๋ฌธ 8: MKLocalSearch๋ก ์ฅ์ ๊ฒ์์ ๊ตฌํํ๋ ค๋ฉด ์ด๋ป๊ฒ ํฉ๋๊น?
MKLocalSearch๋ ๊ด์ฌ ์ง์ , ์ฃผ์ ๋๋ ๋น์ฆ๋์ค๋ฅผ ๊ฒ์ํ ์ ์๊ฒ ํฉ๋๋ค. ์ด ๊ธฐ๋ฅ์ ์์น ๊ธฐ๋ฐ ๊ฒ์ ๋ฐ๊ฐ ์๋ ์ ํ๋ฆฌ์ผ์ด์
์ ํ์์ ์
๋๋ค.
import SwiftUI
import MapKit
@Observable
class PlaceSearchManager {
var searchResults: [MKMapItem] = []
var isSearching = false
var searchText = ""
private var searchTask: Task<Void, Never>?
// Search with debounce
func search(query: String, in region: MKCoordinateRegion) {
searchTask?.cancel()
guard !query.isEmpty else {
searchResults = []
return
}
searchTask = Task {
// 300ms debounce
try? await Task.sleep(for: .milliseconds(300))
guard !Task.isCancelled else { return }
isSearching = true
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.region = region
request.resultTypes = [.pointOfInterest, .address]
let search = MKLocalSearch(request: request)
do {
let response = try await search.start()
if !Task.isCancelled {
searchResults = response.mapItems
}
} catch {
print("Search error: \(error.localizedDescription)")
searchResults = []
}
isSearching = false
}
}
func clearResults() {
searchResults = []
searchText = ""
}
}
struct SearchableMapView: View {
@State private var searchManager = PlaceSearchManager()
@State private var position: MapCameraPosition = .region(
MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522),
span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
)
)
@State private var selectedItem: MKMapItem?
@State private var visibleRegion: MKCoordinateRegion?
var body: some View {
NavigationStack {
Map(position: $position, selection: $selectedItem) {
// Display search results
ForEach(searchManager.searchResults, id: \.self) { item in
Marker(item: item)
}
}
.onMapCameraChange { context in
// Capture visible region for search
visibleRegion = context.region
}
.searchable(text: $searchManager.searchText, prompt: "Search for a place")
.onChange(of: searchManager.searchText) { _, newValue in
if let region = visibleRegion {
searchManager.search(query: newValue, in: region)
}
}
.overlay(alignment: .bottom) {
if let item = selectedItem {
PlaceDetailCard(item: item)
}
}
.navigationTitle("Map")
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct PlaceDetailCard: View {
let item: MKMapItem
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(item.name ?? "Unknown Place")
.font(.headline)
if let address = item.placemark.title {
Text(address)
.font(.subheadline)
.foregroundStyle(.secondary)
}
HStack {
if let phone = item.phoneNumber {
Button {
if let url = URL(string: "tel:\(phone)") {
UIApplication.shared.open(url)
}
} label: {
Label("Call", systemImage: "phone")
}
.buttonStyle(.bordered)
}
Button {
item.openInMaps()
} label: {
Label("Directions", systemImage: "arrow.triangle.turn.up.right.diamond")
}
.buttonStyle(.borderedProminent)
}
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(.ultraThickMaterial)
.clipShape(RoundedRectangle(cornerRadius: 16))
.padding()
}
}์ง๋ฌธ 9: ์ฃผ์๋ฅผ ์ขํ๋ก ๋ณํํ๋ ค๋ฉด ์ด๋ป๊ฒ ํฉ๋๊น(์ง์ค์ฝ๋ฉ)?
์ง์ค์ฝ๋ฉ์ ํ ์คํธ ์ฃผ์๋ฅผ GPS ์ขํ๋ก ๋ณํํฉ๋๋ค. ์ญ์ง์ค์ฝ๋ฉ์ ๋ฐ๋ ์์ ์ ์ํํฉ๋๋ค.
import SwiftUI
import MapKit
import CoreLocation
@Observable
class GeocodingManager {
private let geocoder = CLGeocoder()
var coordinate: CLLocationCoordinate2D?
var address: String?
var isLoading = false
var error: String?
// Geocoding: address โ coordinates
func geocode(address: String) async {
isLoading = true
error = nil
do {
let placemarks = try await geocoder.geocodeAddressString(address)
if let placemark = placemarks.first,
let location = placemark.location {
coordinate = location.coordinate
} else {
error = "Address not found"
}
} catch {
self.error = error.localizedDescription
}
isLoading = false
}
// Reverse geocoding: coordinates โ address
func reverseGeocode(coordinate: CLLocationCoordinate2D) async {
isLoading = true
error = nil
let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
do {
let placemarks = try await geocoder.reverseGeocodeLocation(location)
if let placemark = placemarks.first {
address = formatAddress(placemark)
} else {
error = "Address not found"
}
} catch {
self.error = error.localizedDescription
}
isLoading = false
}
// Format readable address
private func formatAddress(_ placemark: CLPlacemark) -> String {
var components: [String] = []
if let street = placemark.thoroughfare {
if let number = placemark.subThoroughfare {
components.append("\(number) \(street)")
} else {
components.append(street)
}
}
if let city = placemark.locality {
components.append(city)
}
if let country = placemark.country {
components.append(country)
}
return components.joined(separator: ", ")
}
}
struct GeocodingDemoView: View {
@State private var manager = GeocodingManager()
@State private var addressInput = ""
@State private var position: MapCameraPosition = .automatic
var body: some View {
VStack {
// Address input field
HStack {
TextField("Enter an address", text: $addressInput)
.textFieldStyle(.roundedBorder)
Button("Search") {
Task {
await manager.geocode(address: addressInput)
if let coord = manager.coordinate {
position = .region(MKCoordinateRegion(
center: coord,
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
))
}
}
}
.buttonStyle(.borderedProminent)
.disabled(addressInput.isEmpty || manager.isLoading)
}
.padding()
// Map with marker
Map(position: $position) {
if let coord = manager.coordinate {
Marker("Result", coordinate: coord)
.tint(.red)
}
}
if let error = manager.error {
Text(error)
.foregroundStyle(.red)
.padding()
}
}
}
}Apple์ ์ง์ค์ฝ๋๋ ์์ฒญ ์ ํ์ ๋ถ๊ณผํฉ๋๋ค. ๋๋ ์์ฒญ์ ๊ฒฝ์ฐ ๋ก์ปฌ ์บ์ ๋๋ Mapbox๋ Google Maps์ ๊ฐ์ ํ์ฌ ์๋น์ค๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ๊ถ์ฅ๋ฉ๋๋ค.
๊ณ ๊ธ ํจํด ๋ฐ ๋ชจ๋ฒ ์ฌ๋ก
์ง๋ฌธ 10: ๋ง์ ๋ง์ปค๋ก ์ฑ๋ฅ์ ์ต์ ํํ๋ ค๋ฉด ์ด๋ป๊ฒ ํฉ๋๊น?
์๋ฐฑ ๊ฐ์ ๋ง์ปค๋ฅผ ํ์ํ๋ฉด ์ฑ๋ฅ์ ์ํฅ์ ๋ฏธ์น ์ ์์ต๋๋ค. ํด๋ฌ์คํฐ๋ง๊ณผ ์ ์ง์ ๋ก๋ฉ์ ํจ๊ณผ์ ์ธ ํด๊ฒฐ์ฑ ์ ๋๋ค.
import SwiftUI
import MapKit
struct ClusteringMapView: View {
@State private var position: MapCameraPosition = .automatic
@State private var visiblePlaces: [Place] = []
@State private var visibleRegion: MKCoordinateRegion?
// All data (potentially thousands)
let allPlaces: [Place]
var body: some View {
Map(position: $position) {
// Display only visible places
ForEach(visiblePlaces) { place in
Marker(place.name, coordinate: place.coordinate)
}
}
.onMapCameraChange(frequency: .onEnd) { context in
// Update when user stops moving
visibleRegion = context.region
updateVisiblePlaces(in: context.region)
}
}
// Filter places in visible region
private func updateVisiblePlaces(in region: MKCoordinateRegion) {
let minLat = region.center.latitude - region.span.latitudeDelta / 2
let maxLat = region.center.latitude + region.span.latitudeDelta / 2
let minLon = region.center.longitude - region.span.longitudeDelta / 2
let maxLon = region.center.longitude + region.span.longitudeDelta / 2
// Spatial filtering
var filtered = allPlaces.filter { place in
place.coordinate.latitude >= minLat &&
place.coordinate.latitude <= maxLat &&
place.coordinate.longitude >= minLon &&
place.coordinate.longitude <= maxLon
}
// Limit number of displayed markers
if filtered.count > 100 {
// Sampling or clustering
filtered = Array(filtered.prefix(100))
}
visiblePlaces = filtered
}
}
// Simplified manual clustering
struct ClusteredPlace: Identifiable {
let id = UUID()
let coordinate: CLLocationCoordinate2D
let count: Int
let places: [Place]
var isCluster: Bool { count > 1 }
}
@Observable
class ClusterManager {
var clusters: [ClusteredPlace] = []
// Group nearby places based on zoom level
func cluster(places: [Place], in region: MKCoordinateRegion) {
let gridSize = region.span.latitudeDelta / 10
var grid: [String: [Place]] = [:]
for place in places {
// Grid key based on position
let gridX = Int(place.coordinate.longitude / gridSize)
let gridY = Int(place.coordinate.latitude / gridSize)
let key = "\(gridX),\(gridY)"
grid[key, default: []].append(place)
}
// Convert to clusters
clusters = grid.map { (_, places) in
let centerLat = places.map(\.coordinate.latitude).reduce(0, +) / Double(places.count)
let centerLon = places.map(\.coordinate.longitude).reduce(0, +) / Double(places.count)
return ClusteredPlace(
coordinate: CLLocationCoordinate2D(latitude: centerLat, longitude: centerLon),
count: places.count,
places: places
)
}
}
}
struct ClusterAnnotationView: View {
let cluster: ClusteredPlace
var body: some View {
if cluster.isCluster {
// Display a circle with count
ZStack {
Circle()
.fill(.blue)
.frame(width: 40, height: 40)
Text("\(cluster.count)")
.font(.system(size: 14, weight: .bold))
.foregroundStyle(.white)
}
} else {
// Simple marker
Image(systemName: "mappin.circle.fill")
.font(.title)
.foregroundStyle(.red)
}
}
}์ง๋ฌธ 11: MapKit์ ๋ค๋ฅธ Apple ์๋น์ค์ ํตํฉํ๋ ค๋ฉด ์ด๋ป๊ฒ ํฉ๋๊น?
MapKit์ ํ๋ถํ ๊ฒฝํ์ ์ ๊ณตํ๊ธฐ ์ํด ์ฌ๋ฌ Apple ํ๋ ์์ํฌ์ ํตํฉ๋ฉ๋๋ค: Look Around, CarPlay, WidgetKit.
import SwiftUI
import MapKit
struct LookAroundMapView: View {
@State private var position: MapCameraPosition = .automatic
@State private var selectedPlace: Place?
@State private var lookAroundScene: MKLookAroundScene?
@State private var showLookAround = false
let places: [Place]
var body: some View {
Map(position: $position, selection: $selectedPlace) {
ForEach(places) { place in
Marker(place.name, coordinate: place.coordinate)
}
}
.onChange(of: selectedPlace) { _, newPlace in
if let place = newPlace {
Task {
await loadLookAroundScene(for: place.coordinate)
}
}
}
.sheet(isPresented: $showLookAround) {
if let scene = lookAroundScene {
LookAroundPreview(scene: scene)
.frame(height: 300)
}
}
.safeAreaInset(edge: .bottom) {
if selectedPlace != nil && lookAroundScene != nil {
Button("View in Look Around") {
showLookAround = true
}
.buttonStyle(.borderedProminent)
.padding()
}
}
}
// Load Look Around scene for a position
private func loadLookAroundScene(for coordinate: CLLocationCoordinate2D) async {
let request = MKLookAroundSceneRequest(coordinate: coordinate)
do {
lookAroundScene = try await request.scene
} catch {
lookAroundScene = nil
print("Look Around not available: \(error.localizedDescription)")
}
}
}
// Interactive Look Around component
struct LookAroundPreview: View {
let scene: MKLookAroundScene
@State private var isNavigating = false
var body: some View {
LookAroundPreviewRepresentable(scene: scene, isNavigating: $isNavigating)
.overlay(alignment: .topTrailing) {
Button {
isNavigating.toggle()
} label: {
Image(systemName: isNavigating ? "stop.fill" : "play.fill")
.padding()
.background(.ultraThinMaterial)
.clipShape(Circle())
}
.padding()
}
}
}
// UIViewRepresentable for Look Around
struct LookAroundPreviewRepresentable: UIViewRepresentable {
let scene: MKLookAroundScene
@Binding var isNavigating: Bool
func makeUIView(context: Context) -> MKLookAroundViewController {
let controller = MKLookAroundViewController(scene: scene)
controller.isNavigationEnabled = isNavigating
return controller
}
func updateUIView(_ uiView: MKLookAroundViewController, context: Context) {
uiView.isNavigationEnabled = isNavigating
}
}iOS ๋ฉด์ ์ค๋น๊ฐ ๋์ จ๋์?
์ธํฐ๋ํฐ๋ธ ์๋ฎฌ๋ ์ดํฐ, flashcards, ๊ธฐ์ ํ ์คํธ๋ก ์ฐ์ตํ์ธ์.
๊ฒฐ๋ก
SwiftUI์ ํจ๊ปํ๋ MapKit์ iOS ์ ํ๋ฆฌ์ผ์ด์ ์ ์ง๋๋ฅผ ํตํฉํ๊ธฐ ์ํ ํ๋์ ์ด๊ณ ๊ฐ๋ ฅํ API๋ฅผ ์ ๊ณตํฉ๋๋ค. ์ด๋ ธํ ์ด์ , ์ค๋ฒ๋ ์ด, ์ง์ค๋ก์ผ์ด์ ๋ฐ ์ฅ์ ๊ฒ์์ ๋ํ ์๋ฌ์ ๊ธฐ์ ๋ฉด์ ์์ ๊ฒฝํ ๋ง์ iOS ๊ฐ๋ฐ์๋ฅผ ๋๋ณด์ด๊ฒ ํฉ๋๋ค.
๊ฒํ ์ฒดํฌ๋ฆฌ์คํธ
- โ
Map์ผ๋ก ์ง๋๋ฅผ ํ์ํ๊ณMapCameraPosition๊ตฌ์ฑํ๊ธฐ - โ
๋ค์ํ ์ง๋ ์คํ์ผ ์ฌ์ฉํ๊ธฐ(
standard,imagery,hybrid) - โ
์ฌ์ฉ์ ์ ์
Marker๋ฐAnnotation์ถ๊ฐํ๊ธฐ - โ
CLLocationManager๋ก ์์น ๊ถํ ์ฒ๋ฆฌํ๊ธฐ - โ ์ค์๊ฐ์ผ๋ก ์์น ์ถ์ ํ๊ณ ๊ฒฝ๋ก ๊ทธ๋ฆฌ๊ธฐ
- โ
์ค๋ฒ๋ ์ด ๊ทธ๋ฆฌ๊ธฐ(
MapCircle,MapPolygon,MapPolyline) - โ
MKDirections๋ก ๊ฒฝ๋ก ๊ณ์ฐํ๊ธฐ - โ
MKLocalSearch๋ก ์ฅ์ ๊ฒ์ ๊ตฌํํ๊ธฐ - โ ์ง์ค์ฝ๋ฉ ๋ฐ ์ญ์ง์ค์ฝ๋ฉ ์ํํ๊ธฐ
- โ ๋ง์ปค ํด๋ฌ์คํฐ๋ง์ผ๋ก ์ฑ๋ฅ ์ต์ ํํ๊ธฐ
- โ ๋ชฐ์ ํ ๋ณด๊ธฐ๋ฅผ ์ํ Look Around ํตํฉํ๊ธฐ
์ฐ์ต์ ์์ํ์ธ์!
๋ฉด์ ์๋ฎฌ๋ ์ดํฐ์ ๊ธฐ์ ํ ์คํธ๋ก ์ง์์ ํ ์คํธํ์ธ์.
ํ๊ทธ
๊ณต์
๊ด๋ จ ๊ธฐ์ฌ

SwiftUI NavigationStack ๋ฉด์ ์ง๋ฌธ: 2026 ๋ด๋น๊ฒ์ด์ ํจํด
NavigationStack, NavigationPath, ๊ทธ๋ฆฌ๊ณ ์ต์ SwiftUI ๋ด๋น๊ฒ์ด์ ํจํด์ ๊ดํ ํต์ฌ ์ง๋ฌธ์ผ๋ก iOS ๋ฉด์ ์ ์ค๋นํฉ๋๋ค.

StoreKit 2 ์ธํฐ๋ทฐ: ๊ตฌ๋ ๊ด๋ฆฌ ๋ฐ ์์์ฆ ๊ฒ์ฆ
StoreKit 2, ๊ตฌ๋ ๊ด๋ฆฌ, ์์์ฆ ๊ฒ์ฆ, ์ธ์ฑ ๊ตฌ๋งค ๊ตฌํ์ ๊ดํ iOS ์ธํฐ๋ทฐ ์ง๋ฌธ์ ์ค์ฉ์ ์ธ Swift ์ฝ๋ ์์ ์ ํจ๊ป ๋ง์คํฐํ์ญ์์ค.

Vision Framework์ CoreML: ์จ๋๋ฐ์ด์ค ML iOS ๋ฉด์ ์ง๋ฌธ
Vision Framework์ CoreML์ ํต์ฌ ๋ฉด์ ์ง๋ฌธ์ผ๋ก iOS ๋ฉด์ ์ ์ค๋นํ ์ ์์ต๋๋ค. ์ด๋ฏธ์ง ์ธ์, ๊ฐ์ฒด ๊ฐ์ง, ์จ๋๋ฐ์ด์ค ML์ ๋ค๋ฃน๋๋ค.