SwiftUI - Weather Hourly Forecast example

Let’s see how can we build this component of Apple’s weather app in SwiftUI.

First, what do we need to display within the view?

  • The headline of the forecast
  • Hours
  • Rain percentage
  • Weather State

Let’s define that in a data model:

struct HourlyForecastModel {
    let headline: String
    let forecasts: [HourForecast]
    
    static private var staticRandomHeadlines = [
        "Lower temperatures expected tomorrow, with a high of 16°.",
        "Higher temperatures expected today, with a high of 20°",
    ]
    
    static func makeRandom() -> HourlyForecastModel {
        let randomHourForecasts = (0...24).map { HourForecast.makeRandom(
            hour: Calendar.current.component(
                .hour,
                from: Calendar.current.date(
                    byAdding: .hour,
                    value: $0,
                    to: Date()
                )!
            ))
        }
        
        return HourlyForecastModel(
            headline: self.staticRandomHeadlines.randomElement()!,
            forecasts: randomHourForecasts
        )
    }
}

struct HourForecast {
    let hour: Int
    let celsius: Int
    let weatherType: WeatherType
    
    static func makeRandom(hour: Int) -> HourForecast {
        .init(
            hour: hour,
            celsius: Int.random(in: -10...20),
            weatherType: WeatherType.allCases.randomElement()!
        )
    }
}

extension HourForecast: Hashable { }

enum WeatherType {
    case sunny
    case cloud
    case rain
    case snow
    case hail
    case lightning
    
    var systemNameIcon: String {
        switch self {
        case .sunny:
            return "sun.min.fill"
        case .cloud:
            return "cloud.fill"
        case .rain:
            return "cloud.rain.fill"
        case .snow:
            return "cloud.sleet.fill"
        case .hail:
            return "cloud.hail.fill"
        case .lightning:
            return "cloud.bolt.fill"
        }
    }
}

extension WeatherType: CaseIterable { }

To help us with displaying, we’ve also added a few static random data generators.


Then let’s create a data service provider and a ViewModel to allow a view to display the data to a UI.

protocol ForecastProviding {
    func getHourlyForecastData() -> HourlyForecastModel
}

final class MockForecastProvider: ForecastProviding {
    func getHourlyForecastData() -> HourlyForecastModel {
        return .makeRandom()
    }
}

@MainActor
final class ContentViewModel: ObservableObject {
    @Published var data: HourlyForecastModel
    
    private let forecastProvider: ForecastProviding
    
    init(forecastProvider: ForecastProviding = MockForecastProvider()) {
        self.forecastProvider = forecastProvider
        self.data = self.forecastProvider.getHourlyForecastData()
    }
}

Now, our ContentViewModel receives a provider that allows data to be fetched, and then once the data is retrieved, we publish it.


Lastly, let’s create a view similar to the one of the official Apple weather app, which subscribes to the provided data.

struct ContentView: View {
    @StateObject private var viewModel = ContentViewModel()
    
    var body: some View {
        hourForecastView
    }
    
    private var hourForecastView: some View {
        VStack(alignment: .leading) {
            Text(viewModel.data.headline)
                .padding(.horizontal)

            Divider()
            
            ScrollView(.horizontal, showsIndicators: false) {
                LazyHStack(alignment: .top, spacing: 24) {
                    ForEach(viewModel.data.forecasts, id: \.self) { forecast in
                        VStack {
                            Text("\(forecast.hour)")
                                .bold()
                            
                            Image(systemName: forecast.weatherType.systemNameIcon)
                                .imageScale(.large)
                                .frame(height: 40)
                            
                            Text("\(forecast.celsius)°")
                                .bold()
                        }
                    }
                }
                .padding([.horizontal])
            }
        }
        .padding(.vertical)
        .frame(height: 200)
        .foregroundColor(.white)
        .background(
            LinearGradient(gradient: Gradient(colors: [.accentColor.opacity(0.8), .accentColor.opacity(0.6)]), startPoint: .leading, endPoint: .trailing)
        )
        .cornerRadius(16)
    }
}

Let’s see it in action.

Although, not exactly like the official Apple weather app hourly forecast component, it’s a start!


Now hopefully, you have a better idea of how to create a similar component for your app.

The full source code is available here.

Happy coding!