날씨 데이터 앱 구현 가이드. 이 문서를 읽고 WeatherKit 코드를 생성할 수 있습니다.
WeatherKit은 Apple의 날씨 서비스로, 현재 날씨, 시간별/일별 예보, 심각한 기상 경보 등을 제공합니다. 월 50만 회 무료 API 호출이 포함되며, Apple Developer 계정이 필요합니다.
import WeatherKit
import CoreLocationXcode > Signing & Capabilities > + WeatherKit
Apple Developer Console에서 WeatherKit 서비스 활성화
<!-- Info.plist (위치 권한) -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>현재 위치의 날씨를 확인하기 위해 필요합니다.</string>import WeatherKit
import CoreLocation
// 날씨 서비스 인스턴스
let weatherService = WeatherService.shared
// 위치 기반 날씨 요청
func getWeather(for location: CLLocation) async throws -> Weather {
try await weatherService.weather(for: location)
}// 현재 날씨
let current: CurrentWeather = weather.currentWeather
current.temperature // Measurement<UnitTemperature>
current.apparentTemperature // 체감 온도
current.humidity // 습도 (0.0 ~ 1.0)
current.condition // WeatherCondition (sunny, cloudy 등)
current.symbolName // SF Symbol 이름
// 시간별 예보
let hourly: Forecast<HourWeather> = weather.hourlyForecast
for hour in hourly {
hour.date
hour.temperature
hour.precipitationChance
}
// 일별 예보
let daily: Forecast<DayWeather> = weather.dailyForecast
for day in daily {
day.date
day.highTemperature
day.lowTemperature
day.precipitationChance
}// 심각한 기상 경보
let alerts: [WeatherAlert]? = weather.weatherAlerts
for alert in alerts ?? [] {
alert.summary
alert.severity // .minor, .moderate, .severe, .extreme
alert.region
}import SwiftUI
import WeatherKit
import CoreLocation
// MARK: - Weather ViewModel
@Observable
class WeatherViewModel {
var currentWeather: CurrentWeather?
var hourlyForecast: [HourWeather] = []
var dailyForecast: [DayWeather] = []
var isLoading = false
var errorMessage: String?
private let weatherService = WeatherService.shared
func fetchWeather(for location: CLLocation) async {
isLoading = true
errorMessage = nil
do {
let weather = try await weatherService.weather(for: location)
currentWeather = weather.currentWeather
hourlyForecast = Array(weather.hourlyForecast.prefix(24))
dailyForecast = Array(weather.dailyForecast.prefix(7))
} catch {
errorMessage = "날씨를 불러올 수 없습니다: \(error.localizedDescription)"
}
isLoading = false
}
}
// MARK: - Location Manager
@Observable
class LocationManager: NSObject, CLLocationManagerDelegate {
var location: CLLocation?
var authorizationStatus: CLAuthorizationStatus = .notDetermined
private let manager = CLLocationManager()
override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyKilometer
}
func requestLocation() {
manager.requestWhenInUseAuthorization()
manager.requestLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
location = locations.first
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Location error: \(error)")
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
authorizationStatus = manager.authorizationStatus
}
}
// MARK: - Main View
struct WeatherView: View {
@State private var viewModel = WeatherViewModel()
@State private var locationManager = LocationManager()
var body: some View {
NavigationStack {
ScrollView {
if viewModel.isLoading {
ProgressView("날씨 불러오는 중...")
.padding(.top, 100)
} else if let error = viewModel.errorMessage {
ContentUnavailableView(
"오류 발생",
systemImage: "exclamationmark.triangle",
description: Text(error)
)
} else if let current = viewModel.currentWeather {
VStack(spacing: 24) {
// 현재 날씨
CurrentWeatherCard(weather: current)
// 시간별 예보
HourlyForecastView(forecast: viewModel.hourlyForecast)
// 일별 예보
DailyForecastView(forecast: viewModel.dailyForecast)
}
.padding()
} else {
ContentUnavailableView(
"위치 권한 필요",
systemImage: "location.slash",
description: Text("날씨를 확인하려면 위치 권한이 필요합니다.")
)
.onTapGesture {
locationManager.requestLocation()
}
}
}
.navigationTitle("날씨")
.refreshable {
if let location = locationManager.location {
await viewModel.fetchWeather(for: location)
}
}
}
.task {
locationManager.requestLocation()
}
.onChange(of: locationManager.location) { _, newLocation in
if let location = newLocation {
Task {
await viewModel.fetchWeather(for: location)
}
}
}
}
}
// MARK: - Current Weather Card
struct CurrentWeatherCard: View {
let weather: CurrentWeather
var body: some View {
VStack(spacing: 12) {
Image(systemName: weather.symbolName)
.font(.system(size: 64))
.symbolRenderingMode(.multicolor)
Text(weather.temperature.formatted(.measurement(width: .abbreviated)))
.font(.system(size: 48, weight: .thin))
Text(weather.condition.description)
.font(.title3)
.foregroundStyle(.secondary)
HStack(spacing: 24) {
Label {
Text("체감 \(weather.apparentTemperature.formatted(.measurement(width: .abbreviated)))")
} icon: {
Image(systemName: "thermometer.medium")
}
Label {
Text("\(Int(weather.humidity * 100))%")
} icon: {
Image(systemName: "humidity")
}
}
.font(.subheadline)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 32)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 20))
}
}
// MARK: - Hourly Forecast
struct HourlyForecastView: View {
let forecast: [HourWeather]
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Label("시간별 예보", systemImage: "clock")
.font(.headline)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
ForEach(forecast, id: \.date) { hour in
VStack(spacing: 8) {
Text(hour.date.formatted(.dateTime.hour()))
.font(.caption)
Image(systemName: hour.symbolName)
.font(.title2)
.symbolRenderingMode(.multicolor)
Text(hour.temperature.formatted(.measurement(width: .narrow)))
.font(.subheadline)
}
.frame(width: 60)
}
}
.padding(.horizontal, 4)
}
}
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16))
}
}
// MARK: - Daily Forecast
struct DailyForecastView: View {
let forecast: [DayWeather]
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Label("7일 예보", systemImage: "calendar")
.font(.headline)
ForEach(forecast, id: \.date) { day in
HStack {
Text(day.date.formatted(.dateTime.weekday(.wide)))
.frame(width: 80, alignment: .leading)
Image(systemName: day.symbolName)
.symbolRenderingMode(.multicolor)
.frame(width: 32)
Spacer()
Text(day.lowTemperature.formatted(.measurement(width: .narrow)))
.foregroundStyle(.secondary)
TemperatureBar(
low: day.lowTemperature.value,
high: day.highTemperature.value
)
.frame(width: 80)
Text(day.highTemperature.formatted(.measurement(width: .narrow)))
}
.font(.subheadline)
if day.date != forecast.last?.date {
Divider()
}
}
}
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16))
}
}
// MARK: - Temperature Bar
struct TemperatureBar: View {
let low: Double
let high: Double
var body: some View {
GeometryReader { geometry in
Capsule()
.fill(
LinearGradient(
colors: [.blue, .yellow, .orange],
startPoint: .leading,
endPoint: .trailing
)
)
.frame(height: 4)
.frame(maxHeight: .infinity, alignment: .center)
}
}
}
#Preview {
WeatherView()
}// 필요한 데이터셋만 요청 (성능 최적화)
let (current, hourly) = try await weatherService.weather(
for: location,
including: .current, .hourly
)
// 일별 예보만 요청
let daily = try await weatherService.weather(
for: location,
including: .daily
)struct WeatherAttributionView: View {
var body: some View {
VStack {
// ... 날씨 UI
// Apple Weather 출처 표시 (필수)
AsyncImage(url: WeatherService.shared.attribution.combinedMarkDarkURL) { image in
image
.resizable()
.scaledToFit()
} placeholder: {
EmptyView()
}
.frame(height: 12)
Link("데이터 출처", destination: WeatherService.shared.attribution.legalPageURL)
.font(.caption2)
}
}
}func checkWeatherAlerts(for location: CLLocation) async {
do {
let weather = try await weatherService.weather(for: location)
if let alerts = weather.weatherAlerts, !alerts.isEmpty {
for alert in alerts {
switch alert.severity {
case .extreme, .severe:
// 긴급 알림 표시
showUrgentAlert(alert)
case .moderate:
// 일반 알림
showWarning(alert)
case .minor:
// 참고 정보
logInfo(alert)
default:
break
}
}
}
} catch {
print("Weather alert check failed: \(error)")
}
}// 현재 날씨 상세 정보
let current = weather.currentWeather
let uvIndex = current.uvIndex // UV 지수
let visibility = current.visibility // 가시거리
let pressure = current.pressure // 기압
let dewPoint = current.dewPoint // 이슬점
let windSpeed = current.wind.speed // 풍속
let windDirection = current.wind.direction // 풍향
let cloudCover = current.cloudCover // 구름량 (0.0 ~ 1.0)-
Attribution 필수
- WeatherKit 사용 시 Apple Weather 출처 표시 필수
WeatherService.shared.attribution사용
-
API 호출 제한
- 무료: 월 50만 회
- 초과 시 유료 (Apple Developer 대시보드에서 확인)
-
위치 권한
- WeatherKit 자체는 위치 권한 불필요
- 현재 위치 날씨를 위해선 CoreLocation 필요
-
오프라인 처리
- 네트워크 필요 (오프라인 캐싱 없음)
- 적절한 에러 처리 필수
-
지역 제한
- 일부 국가에서 기상 경보 미지원
- 데이터 가용성은 지역마다 다름