Swift codable
Swift codable

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

  1. Type SafetySwift Codable provides compile-time guarantees for data parsing
  2. Customization: Custom coding keys and initializers handle API variations
  3. Computed Properties: Models can include UI-ready computed properties
  4. Testing: Data models are easy to unit test with JSON fixtures
  5. Integration: Models work seamlessly with SwiftUI views and MVVM architecture

Swift Codable Tutorial Best Practices

  • 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 Apphttps://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

By admin

Leave a Reply

Your email address will not be published. Required fields are marked *