
From Architecture to Data: Swift Codable Tutorial for Weather Models
In [Part 3], we implemented the **Core Architecture** and **MVVM** pattern for **Simple Weather**. Now it’s time to build the foundation that powers our app – the data models. This **Swift Codable tutorial** will show you how to create robust, type-safe weather data structures that seamlessly integrate with JSON APIs and provide excellent developer experience.
Why Data Models Matter in Simple Weather
The Foundation of Our App
Data models are the backbone of Simple Weather. They define:
- Data Structure: How weather information is organized and stored
- Type Safety: Compile-time guarantees that prevent runtime errors
- API Integration: Seamless JSON parsing with Swift Codable
- Business Logic: Computed properties and data validation
- UI Integration: Properties that directly support SwiftUI views
Swift Codable Tutorial: The Power of Declarative Data
Swift Codable is Apple’s protocol-oriented approach to data serialization. In this Swift Codable tutorial, you’ll learn how to:
- Create models that automatically parse JSON from weather APIs
- Handle complex nested data structures
- Implement custom encoding/decoding logic
- Validate data and provide fallback values
- Create type-safe enums for weather conditions
Weather Data Structure Design
Understanding Weather API Responses
Before diving into the Swift Codable tutorial, let’s understand what weather data looks like:
{
"current": {
"temp": 72.5,
"feels_like": 74.2,
"humidity": 65,
"wind_speed": 8.5,
"wind_deg": 180,
"weather": [
{
"id": 800,
"main": "Clear",
"description": "clear sky",
"icon": "01d"
}
]
},
"location": {
"name": "San Francisco",
"lat": 37.7749,
"lon": -122.4194,
"country": "US"
}
}
Core Weather Models
Let’s start our Swift Codable tutorial with the fundamental weather models:
// MARK: - Main Weather Response
struct WeatherResponse: Codable {
let current: CurrentWeather
let location: Location
let hourly: [HourlyForecast]?
let daily: [DailyForecast]?
// Custom coding keys for API compatibility
enum CodingKeys: String, CodingKey {
case current
case location
case hourly
case daily
}
}
// MARK: - Current Weather Model
struct CurrentWeather: Codable, Identifiable {
let id = UUID()
let temperature: Double
let feelsLike: Double
let humidity: Int
let windSpeed: Double
let windDirection: Int
let weatherConditions: [WeatherCondition]
let timestamp: Date
// Swift Codable tutorial: Custom coding keys
enum CodingKeys: String, CodingKey {
case temperature = "temp"
case feelsLike = "feels_like"
case humidity
case windSpeed = "wind_speed"
case windDirection = "wind_deg"
case weatherConditions = "weather"
case timestamp = "dt"
}
// Swift Codable tutorial: Custom initializer for data transformation
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
temperature = try container.decode(Double.self, forKey: .temperature)
feelsLike = try container.decode(Double.self, forKey: .feelsLike)
humidity = try container.decode(Int.self, forKey: .humidity)
windSpeed = try container.decode(Double.self, forKey: .windSpeed)
windDirection = try container.decode(Int.self, forKey: .windDirection)
weatherConditions = try container.decode([WeatherCondition].self, forKey: .weatherConditions)
// Swift Codable tutorial: Date handling
let timestampInterval = try container.decode(TimeInterval.self, forKey: .timestamp)
timestamp = Date(timeIntervalSince1970: timestampInterval)
}
// Swift Codable tutorial: Computed properties for UI
var temperatureString: String {
return "(Int(temperature))°"
}
var feelsLikeString: String {
return "(Int(feelsLike))°"
}
var windSpeedString: String {
return "(Int(windSpeed)) mph"
}
var windDirectionString: String {
return windDirectionToCardinal(windDirection)
}
var primaryCondition: WeatherCondition? {
return weatherConditions.first
}
// Swift Codable tutorial: Helper methods
private func windDirectionToCardinal(_ degrees: Int) -> String {
let directions = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
"S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]
let index = Int(round(Double(degrees) / 22.5)) % 16
return directions[index]
}
}
Weather Condition Models
Type-Safe Weather Conditions
Our Swift Codable tutorial continues with weather condition models:
// MARK: - Weather Condition Model
struct WeatherCondition: Codable, Identifiable {
let id: Int
let main: String
let description: String
let icon: String
// Swift Codable tutorial: Computed properties
var conditionType: WeatherType {
return WeatherType.from(id: id)
}
var iconName: String {
return conditionType.iconName
}
var color: Color {
return conditionType.color
}
}
// MARK: - Weather Type Enum
enum WeatherType: Int, CaseIterable {
case clear = 800
case fewClouds = 801
case scatteredClouds = 802
case brokenClouds = 803
case overcastClouds = 804
case lightRain = 500
case moderateRain = 501
case heavyRain = 502
case lightSnow = 600
case moderateSnow = 601
case heavySnow = 602
case thunderstorm = 200
case mist = 701
case fog = 741
// Swift Codable tutorial: Static factory method
static func from(id: Int) -> WeatherType {
return WeatherType(rawValue: id) ?? .clear
}
// Swift Codable tutorial: Computed properties
var iconName: String {
switch self {
case .clear: return "sun.max.fill"
case .fewClouds, .scatteredClouds: return "cloud.sun.fill"
case .brokenClouds, .overcastClouds: return "cloud.fill"
case .lightRain: return "cloud.drizzle.fill"
case .moderateRain, .heavyRain: return "cloud.rain.fill"
case .lightSnow, .moderateSnow, .heavySnow: return "cloud.snow.fill"
case .thunderstorm: return "cloud.bolt.rain.fill"
case .mist, .fog: return "cloud.fog.fill"
}
}
var color: Color {
switch self {
case .clear: return .orange
case .fewClouds, .scatteredClouds: return .blue
case .brokenClouds, .overcastClouds: return .gray
case .lightRain, .moderateRain, .heavyRain: return .blue
case .lightSnow, .moderateSnow, .heavySnow: return .white
case .thunderstorm: return .purple
case .mist, .fog: return .gray
}
}
var description: String {
switch self {
case .clear: return "Clear"
case .fewClouds: return "Few Clouds"
case .scatteredClouds: return "Scattered Clouds"
case .brokenClouds: return "Broken Clouds"
case .overcastClouds: return "Overcast"
case .lightRain: return "Light Rain"
case .moderateRain: return "Moderate Rain"
case .heavyRain: return "Heavy Rain"
case .lightSnow: return "Light Snow"
case .moderateSnow: return "Moderate Snow"
case .heavySnow: return "Heavy Snow"
case .thunderstorm: return "Thunderstorm"
case .mist: return "Mist"
case .fog: return "Fog"
}
}
}
Location and Forecast Models
Location Data Structure
// MARK: - Location Model
struct Location: Codable, Identifiable, Equatable {
let id = UUID()
let name: String
let latitude: Double
let longitude: Double
let country: String
let state: String?
// Swift Codable tutorial: Custom coding keys
enum CodingKeys: String, CodingKey {
case name
case latitude = "lat"
case longitude = "lon"
case country
case state
}
// Swift Codable tutorial: Computed properties
var displayName: String {
if let state = state {
return "(name), (state)"
}
return "(name), (country)"
}
var coordinates: CLLocationCoordinate2D {
return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
// Swift Codable tutorial: Equatable conformance
static func == (lhs: Location, rhs: Location) -> Bool {
return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude
}
}
// MARK: - Hourly Forecast Model
struct HourlyForecast: Codable, Identifiable {
let id = UUID()
let time: Date
let temperature: Double
let feelsLike: Double
let humidity: Int
let windSpeed: Double
let weatherConditions: [WeatherCondition]
let precipitationProbability: Double
// Swift Codable tutorial: Custom coding keys
enum CodingKeys: String, CodingKey {
case time = "dt"
case temperature = "temp"
case feelsLike = "feels_like"
case humidity
case windSpeed = "wind_speed"
case weatherConditions = "weather"
case precipitationProbability = "pop"
}
// Swift Codable tutorial: Custom initializer
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let timeInterval = try container.decode(TimeInterval.self, forKey: .time)
time = Date(timeIntervalSince1970: timeInterval)
temperature = try container.decode(Double.self, forKey: .temperature)
feelsLike = try container.decode(Double.self, forKey: .feelsLike)
humidity = try container.decode(Int.self, forKey: .humidity)
windSpeed = try container.decode(Double.self, forKey: .windSpeed)
weatherConditions = try container.decode([WeatherCondition].self, forKey: .weatherConditions)
precipitationProbability = try container.decode(Double.self, forKey: .precipitationProbability)
}
// Swift Codable tutorial: Computed properties
var timeString: String {
let formatter = DateFormatter()
formatter.timeStyle = .short
return formatter.string(from: time)
}
var temperatureString: String {
return "(Int(temperature))°"
}
var precipitationString: String {
return "(Int(precipitationProbability * 100))%"
}
}
// MARK: - Daily Forecast Model
struct DailyForecast: Codable, Identifiable {
let id = UUID()
let date: Date
let highTemperature: Double
let lowTemperature: Double
let weatherConditions: [WeatherCondition]
let precipitationProbability: Double
let sunrise: Date
let sunset: Date
// Swift Codable tutorial: Custom coding keys
enum CodingKeys: String, CodingKey {
case date = "dt"
case temperature = "temp"
case weatherConditions = "weather"
case precipitationProbability = "pop"
case sunrise
case sunset
}
// Swift Codable tutorial: Custom initializer for nested temperature
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let dateInterval = try container.decode(TimeInterval.self, forKey: .date)
date = Date(timeIntervalSince1970: dateInterval)
// Swift Codable tutorial: Nested object decoding
let tempContainer = try container.nestedContainer(keyedBy: TemperatureKeys.self, forKey: .temperature)
highTemperature = try tempContainer.decode(Double.self, forKey: .max)
lowTemperature = try tempContainer.decode(Double.self, forKey: .min)
weatherConditions = try container.decode([WeatherCondition].self, forKey: .weatherConditions)
precipitationProbability = try container.decode(Double.self, forKey: .precipitationProbability)
let sunriseInterval = try container.decode(TimeInterval.self, forKey: .sunrise)
sunrise = Date(timeIntervalSince1970: sunriseInterval)
let sunsetInterval = try container.decode(TimeInterval.self, forKey: .sunset)
sunset = Date(timeIntervalSince1970: sunsetInterval)
}
// Swift Codable tutorial: Nested coding keys
private enum TemperatureKeys: String, CodingKey {
case max
case min
}
// Swift Codable tutorial: Computed properties
var dateString: String {
let formatter = DateFormatter()
formatter.dateFormat = "EEE"
return formatter.string(from: date)
}
var highTempString: String {
return "(Int(highTemperature))°"
}
var lowTempString: String {
return "(Int(lowTemperature))°"
}
var precipitationString: String {
return "(Int(precipitationProbability * 100))%"
}
}
Advanced Swift Codable Tutorial Features
Custom Encoding and Decoding
// MARK: - Weather Units Model
struct WeatherUnits: Codable {
let temperature: TemperatureUnit
let windSpeed: WindSpeedUnit
let pressure: PressureUnit
// Swift Codable tutorial: Default values
init(temperature: TemperatureUnit = .celsius,
windSpeed: WindSpeedUnit = .mph,
pressure: PressureUnit = .hpa) {
self.temperature = temperature
self.windSpeed = windSpeed
self.pressure = pressure
}
// Swift Codable tutorial: Custom encoding
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(temperature.rawValue, forKey: .temperature)
try container.encode(windSpeed.rawValue, forKey: .windSpeed)
try container.encode(pressure.rawValue, forKey: .pressure)
}
// Swift Codable tutorial: Custom decoding
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let tempRaw = try container.decode(String.self, forKey: .temperature)
temperature = TemperatureUnit(rawValue: tempRaw) ?? .celsius
let windRaw = try container.decode(String.self, forKey: .windSpeed)
windSpeed = WindSpeedUnit(rawValue: windRaw) ?? .mph
let pressureRaw = try container.decode(String.self, forKey: .pressure)
pressure = PressureUnit(rawValue: pressureRaw) ?? .hpa
}
enum CodingKeys: String, CodingKey {
case temperature = "temp"
case windSpeed = "wind"
case pressure
}
}
// MARK: - Unit Enums
enum TemperatureUnit: String, CaseIterable {
case celsius = "celsius"
case fahrenheit = "fahrenheit"
var symbol: String {
switch self {
case .celsius: return "°C"
case .fahrenheit: return "°F"
}
}
}
enum WindSpeedUnit: String, CaseIterable {
case mph = "mph"
case kmh = "kmh"
case ms = "ms"
var symbol: String {
switch self {
case .mph: return "mph"
case .kmh: return "km/h"
case .ms: return "m/s"
}
}
}
enum PressureUnit: String, CaseIterable {
case hpa = "hpa"
case inhg = "inhg"
var symbol: String {
switch self {
case .hpa: return "hPa"
case .inhg: return "inHg"
}
}
}
Testing Weather Models
Unit Tests for Data Models
// MARK: - Weather Model Tests
class WeatherModelTests: XCTestCase {
func testCurrentWeatherDecoding() throws {
// Swift Codable tutorial: Test JSON
let json = """
{
"temp": 72.5,
"feels_like": 74.2,
"humidity": 65,
"wind_speed": 8.5,
"wind_deg": 180,
"weather": [
{
"id": 800,
"main": "Clear",
"description": "clear sky",
"icon": "01d"
}
],
"dt": 1640995200
}
""".data(using: .utf8)!
// Swift Codable tutorial: Decode and verify
let weather = try JSONDecoder().decode(CurrentWeather.self, from: json)
XCTAssertEqual(weather.temperature, 72.5)
XCTAssertEqual(weather.feelsLike, 74.2)
XCTAssertEqual(weather.humidity, 65)
XCTAssertEqual(weather.windSpeed, 8.5)
XCTAssertEqual(weather.windDirection, 180)
XCTAssertEqual(weather.weatherConditions.count, 1)
XCTAssertEqual(weather.weatherConditions.first?.id, 800)
}
func testWeatherConditionTypeMapping() throws {
// Swift Codable tutorial: Test weather type mapping
let clearCondition = WeatherCondition(id: 800, main: "Clear", description: "clear sky", icon: "01d")
XCTAssertEqual(clearCondition.conditionType, .clear)
let rainCondition = WeatherCondition(id: 500, main: "Rain", description: "light rain", icon: "10d")
XCTAssertEqual(rainCondition.conditionType, .lightRain)
}
func testLocationEquality() throws {
// Swift Codable tutorial: Test location equality
let location1 = Location(name: "San Francisco", latitude: 37.7749, longitude: -122.4194, country: "US", state: "CA")
let location2 = Location(name: "San Francisco", latitude: 37.7749, longitude: -122.4194, country: "US", state: "CA")
let location3 = Location(name: "New York", latitude: 40.7128, longitude: -74.0060, country: "US", state: "NY")
XCTAssertEqual(location1, location2)
XCTAssertNotEqual(location1, location3)
}
}
Integration with SwiftUI Views
Using Models in Views
// MARK: - Weather Card View
struct WeatherCardView: View {
let weather: CurrentWeather
var body: some View {
VStack(spacing: 16) {
// Temperature
Text(weather.temperatureString)
.font(.system(size: 72, weight: .thin))
.foregroundColor(.primary)
// Weather condition
if let condition = weather.primaryCondition {
HStack {
Image(systemName: condition.iconName)
.font(.title2)
.foregroundColor(condition.color)
Text(condition.description)
.font(.title3)
}
.foregroundColor(.secondary)
}
// Details
WeatherDetailsView(weather: weather)
}
.padding()
.background(.ultraThinMaterial)
.cornerRadius(16)
}
}
// MARK: - Weather Details View
struct WeatherDetailsView: View {
let weather: CurrentWeather
var body: some View {
VStack(spacing: 8) {
DetailRow(title: "Feels Like", value: weather.feelsLikeString)
DetailRow(title: "Humidity", value: "(weather.humidity)%")
DetailRow(title: "Wind", value: "(weather.windSpeedString) (weather.windDirectionString)")
}
}
}
struct DetailRow: View {
let title: String
let value: String
var body: some View {
HStack {
Text(title)
.foregroundColor(.secondary)
Spacer()
Text(value)
.fontWeight(.medium)
}
}
}
Next Steps in Simple Weather Development
With our weather data models and Swift Codable tutorial complete, we’re ready to move forward with:
- Part 5: Implementing the Weather API Service
- Part 6: Creating the Main Weather View
- Part 7: Location Services & Permissions
Key Takeaways from Swift Codable Tutorial
- Type Safety: Swift Codable provides compile-time guarantees for data parsing
- Customization: Custom coding keys and initializers handle API variations
- Computed Properties: Models can include UI-ready computed properties
- Testing: Data models are easy to unit test with JSON fixtures
- Integration: Models work seamlessly with SwiftUI views and MVVM architecture
Swift Codable Tutorial Best Practices
Recommended Patterns
- Custom Coding Keys: Use for API compatibility
- Computed Properties: Add UI-ready properties to models
- Type-Safe Enums: Use enums for constrained values
- Default Values: Provide sensible defaults for optional properties
- Error Handling: Implement proper error handling for malformed data
Performance Tips
- Lazy Properties: Use lazy properties for expensive computations
- Memory Management: Be mindful of large data sets
- Caching: Cache parsed models when appropriate
- Validation: Validate data during parsing
Get Involved in Simple Weather Development
The weather data models and Swift Codable tutorial provide a solid foundation for Simple Weather. In the next part, we’ll implement the weather API service that uses these models.
Have questions about Swift Codable implementation or suggestions for improving the data models? Share your thoughts in the comments below!
Weather – Simple Weather iOS App: https://apps.apple.com/us/app/weather-simple-weather/id6747141875
Weather – Simple Weather support page and privacy page, please visit here: https://tools.regalrealm.com/ios-support/
Weather App Home Page: https://quietbookspace.com/weather-simple-weather-ios-app/
Next up: Part 5 – Implementing the Weather API Service for Simple Weather