Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Sichere ProgrammierungThe Homelab Is the New Resume(21.09.2026 um 00:29 Uhr)
Sichere ProgrammierungPerl 🐪 Weekly #791 - The Dark Side is here!(21.09.2026 um 00:44 Uhr)
Sichere Programmierungbro.js v3.0.0 – What’s new(21.09.2026 um 00:44 Uhr)
Sichere ProgrammierungFour bugs my test suite couldn't catch(21.09.2026 um 00:49 Uhr)
Sichere ProgrammierungAutomating Deployment with Github Actions(21.09.2026 um 00:49 Uhr)
Linux Tipps & HardeningKernel prepatch 7.3-rc4(21.09.2026 um 00:52 Uhr)
IT NachrichtenHow to use Xbox mode on your Windows PC(21.09.2026 um 00:30 Uhr)
Sichere ProgrammierungThe Homelab Is the New Resume(21.09.2026 um 00:29 Uhr)
Sichere ProgrammierungPerl 🐪 Weekly #791 - The Dark Side is here!(21.09.2026 um 00:44 Uhr)
Sichere Programmierungbro.js v3.0.0 – What’s new(21.09.2026 um 00:44 Uhr)
Sichere ProgrammierungFour bugs my test suite couldn't catch(21.09.2026 um 00:49 Uhr)
Sichere ProgrammierungAutomating Deployment with Github Actions(21.09.2026 um 00:49 Uhr)
Linux Tipps & HardeningKernel prepatch 7.3-rc4(21.09.2026 um 00:52 Uhr)
IT NachrichtenHow to use Xbox mode on your Windows PC(21.09.2026 um 00:30 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Understanding MVVM by Building a Simple Weather App with SwiftUI

Reagiere als Erste:r — dein Feedback zählt!

MVVM with swiftUI

When learning SwiftUI, one of the first architectural patterns you'll encounter is MVVM (Model-View-ViewModel). In this tutorial, we'll build a simple weather application that consumes the OpenWeather API while applying MVVM, dependency injection, and protocol-oriented programming. By the end, you'll understand not only how to structure the project, but also why each layer exists.

This is the link for the OpenWeather API https://openweathermap.org/api.

What you'll learn

By the end of this tutorial you'll know how to:

  • Structure a SwiftUI project using MVVM.
  • Consume a REST API using async/await.
  • Apply dependency injection using protocols.
  • Display loading and error states.
  • Keep Views focused only on UI.

This is how the data flow looks.

        User taps "Search"
                │
                ▼
         ┌──────────────┐
         │ ContentView  │
         └──────┬───────┘
                │
        await fetchWeather()
                │
                ▼
      ┌────────────────────┐
      │ WeatherViewModel   │
      └─────────┬──────────┘
                │
                ▼
        WeatherServiceProtocol
                │
                ▼
          ┌─────────────────┐
          │ WeatherService  │
          └──────┬──────────┘
                 │
                 ▼
         OpenWeather API

Project Structure

WeatherApp
├── Configuration
│  └──AppConfig.swift
├── Models
│   ├── Main.swift
│   ├── Weather.swift
│   └── WeatherResponse.swift
├── Services
│   ├── WeatherService.swift
│   └── WeatherServiceProtocol.swift
├── ViewModels
│   └── WeatherViewModel.swift
└── Views
    └── ContentView.swift
  • Configuration contains application-wide constants such as the API key and base URLs.
  • Models contains the data structures used to decode the API response.
  • Services is responsible for networking and fetching data.
  • ViewModels contains the presentation logic and exposes data to the UI.
  • Views contains the SwiftUI interface.

On the AppConfig file, we are going to keep static info, just like the base URL, API key, etc

struct AppConfig {
    static let apiKey = "YOUR API KEY"
    static let baseGeoCodingAPIURL = "https://api.openweathermap.org/geo/1.0/direct?q="
    static let baseURL = "https://api.openweathermap.org/data/2.5/weather?&units=metric&lat="

}

Designing the UI

Before implementing the networking layer, we'll start by designing the UI. Defining the interface first helps us identify exactly which data we need from the API, allowing us to create only the models required by the application.

The app is going to look something like

┌───────────────────────────────┐
│ Enter city           Search   │
├───────────────────────────────┤
│                               │
│             Rain              │
│        Moderate Rain          │
│                               │
│             28.5°             │
│                               │
│  Feels Like     Min      Max  │
│     29.1°      25.0°    31.2° │
│                               │
└───────────────────────────────┘

After defining what data we need to show, we can create our models. The OpenWeather API JSON returns something like this

{
  "lat": 51.5,
  "lon": -0.1,
  "timezone": "Europe/London",
  "timezone_offset": 3600,
  "data": [
    {
      "dt": 1777449371,
      "sunrise": 1777437375,
      "sunset": 1777490344,
      "temp": 286.42,
      "feels_like": 285.32,
      "pressure": 1024,
      "humidity": 58,
      "dew_point": 278.34,
      "uvi": 1.55,
      "clouds": 0,
      "visibility": 10000,
      "wind_speed": 8.23,
      "wind_deg": 70,
      "weather": [
        {
          "id": 800,
          "main": "Clear",
          "description": "sky is clear",
          "icon": "01d"
        }
      ]
      "alerts": [
        "8B46C632-DCA7-44D7-8BDF-02445621BAFF",
        "29F58A35-BB91-4A73-9F46-9FC64BDF604F",
        ...
    ]
    }
  ]
}

The OpenWeather response contains much more information than we need. Rather than decoding the entire JSON, we'll only model the fields our application uses.

Creating the Models

First we have the Weather model

struct Weather: Codable{
    var id: Int
    var main: String
    var description: String
    var icon: String
}

also we have the Main model

struct Main: Codable{
    let temp: Double
    let feels_like: Double
    let temp_min: Double
    let temp_max: Double
}

Now we can combine the two of them to get the data we need from the OpenWeather API, this is the WeatherResponse object

struct WeatherResponse: Codable {
    var weather : [Weather]
    var main : Main
}

Building the Service Layer

After creating the model, we can work on the service, the service is part of the data layer, it handles API/NETWORK tasks.

fetchCity and fetchWeather are asynchronous throwing functions. They propagate any networking or decoding errors to the caller, allowing the ViewModel to decide how to handle them.

The WeatherService is responsible for communicating with the OpenWeather API. It converts the city name into coordinates using the Geocoding API, then requests the current weather using those coordinates.

We are going to create the protocol for WeatherService, in that way we can use dependency injection, we can pass the interface via the initializer so you can easily swap it with a mock service during SwiftUI Previews or Unit testing.

protocol WeatherServiceProtocol{
    static func convertCityToGeoCodingAPI(city: String) -> String
    static func getWeatherAPI(lat: Double, lon: Double) -> String
    static func fetchCity(for city: String ) async throws  -> CityCoords
    func fetchWeather(city: String) async throws -> WeatherResponse
}

We make these helper methods static because they don't depend on any instance-specific state. They simply build URLs based on the provided parameters, so there's no need to create a WeatherService object just to call them.

static func convertCityToGeoCodingAPI(city: String) -> String {
        return AppConfig.baseGeoCodingAPIURL + city + "&limit=1&appid=" + AppConfig.apiKey
    }

    static func getWeatherAPI(lat: Double, lon: Double) -> String {
        return AppConfig.baseURL + "\(lat)&lon=\(lon)&appid=" + AppConfig.apiKey
    }

now we have the fetchCity and fetchWeather, the fetchCity receives the city name and converts it to coords, the fetchWeather uses the fetchCity function to get the current weather data from the results from fetchCity.

    static func fetchCity(for city: String ) async throws  -> CityCoords {
        //create and validate url
        guard let url = URL(string : Self.convertCityToGeoCodingAPI(city: city)) else {
            throw NetworkError.invalidURL
        }

        //Fetch data from the network
            let (data, response) = try await URLSession.shared.data(from: url)

        //verify http status code
        guard let httpResponse = response as? HTTPURLResponse,httpResponse.statusCode == 200 else {
            throw NetworkError.invalidResponse
        }

        //Decode JSON payload
        let coordsList = try JSONDecoder().decode([CityCoords].self, from: data)

        // 5. Ensure the array isn't empty before picking the first result
        guard let coords = coordsList.first else {
            throw NetworkError.cityNotFound 
        }

        return coords;
    }

    func fetchWeather(city: String) async throws -> WeatherResponse {
        let coords = try await Self.fetchCity(for: city)

        guard let url = URL(string: Self.getWeatherAPI(lat: coords.lat, lon: coords.lon)) else {
            throw NetworkError.invalidURL
        }
        let (data, response) = try await URLSession.shared.data(from: url)
        guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
            throw NetworkError.invalidResponse
        }

        let weatherData = try JSONDecoder().decode(WeatherResponse.self, from: data)

        return weatherData
    }
}

Implementing the ViewModel

Now we can work on the ViewModel; The ViewModel responds to user actions, requests data from the service, stores the screen's state, and exposes formatted data for the View to display.

We use computed properties to send the data formatted to the view so we can keep the view dumb.

The WeatherViewModel conforms to ObservableObject, allowing SwiftUI to observe it for changes. Properties marked with @Published automatically notify the View whenever their values change, causing the UI to refresh.

The @MainActor is used to guarantee the code will run in the main thread, which is mandatory when we are updating the user interface.

import Foundation
import Combine


@MainActor
final class WeatherViewModel: ObservableObject {
    //Store state
    @Published var weatherResponse : WeatherResponse?
    @Published var isLoading = false
    @Published var errorMessage: Error?

    //Dependencies
    private let weatherService : WeatherServiceProtocol

    init(weatherService: WeatherServiceProtocol){
        self.weatherService = weatherService
    }

    //private derived data
    private var currentWeather: Weather? {
        weatherResponse?.weather.first
    }

    //UI computed properties
    var weatherDescription: String {
        currentWeather?.description ?? "No Data"
    }
    var weatherCondition: String {
        currentWeather?.main ?? "No Data"
    }
    var temperatureText : String {
        formattedTemperature(weatherResponse?.main.temp)
    }

    var feelsLikeText : String {
        formattedTemperature(weatherResponse?.main.feels_like)
    }
    var minTemperatureText: String {
        formattedTemperature(weatherResponse?.main.temp_min)
    }
    var maxTemperatureText: String {
        formattedTemperature(weatherResponse?.main.temp_max)
    } 

We use this function to format the temperature and send it to the view; in this way, the view does not have to do anything but display the data.

private func formattedTemperature(_ value: Double?) -> String {
        guard let value else {
            return "No Data"
        }

        return String(format: "%.2f°C", value)
    }

the last function is the fetchWeather, this one receives a city name as String and invokes the function from the weatherService, this function also controls the different states such as isLoading, errorMessage and weatherResponse.

func fetchWeather(city : String) async {

        isLoading = true
        errorMessage = nil

        defer {
            isLoading = false
        }
        do{
           weatherResponse = try await weatherService.fetchWeather(city: city)
        } catch {
            errorMessage = error
        }
    }
}

Building the View

Finally, we can build the View.

We create the @State var for getting the user input, As the user types, SwiftUI automatically updates the city state.

We create the weatherService object in the constructor, also the weather ViewModel is created inside the constructor because it needs the reference to the service object.

    //
//  ContentView.swift
//  WeatherApp
//
//  Created by Gerald on 23/6/26.
//

import SwiftData
import SwiftUI

struct ContentView: View {
    @State private var city = ""
    let service : WeatherService
    @StateObject private var weatherVM : WeatherViewModel

    init(service: WeatherService = WeatherService()) {
        self.service = service
        self._weatherVM = StateObject(wrappedValue: WeatherViewModel(weatherService: service))
    }

    var body: some View {
        VStack(spacing: 24) {

            HStack {
                TextField("Enter city", text: $city)
                    .textFieldStyle(.roundedBorder)
                    .onSubmit {
                        fetchWeather()
                    }

                Button("Search") {
                    fetchWeather()
                }
                .buttonStyle(.borderedProminent)
            }

            if weatherVM.temperatureText != "No Data" {
                if weatherVM.isLoading {
                    ProgressView()
                }

                VStack(spacing: 16) {

                    Text(weatherVM.weatherCondition)
                        .font(.largeTitle)
                        .fontWeight(.bold)

                    Text(weatherVM.weatherDescription.capitalized)
                        .font(.title3)
                        .foregroundStyle(.secondary)

                    Text(weatherVM.temperatureText)
                        .font(.system(size: 72, weight: .thin))

                    HStack(spacing: 40) {

                        WeatherInfoView(
                            title: "Feels Like",
                            value: weatherVM.feelsLikeText
                        )

                        WeatherInfoView(
                            title: "Min",
                            value: weatherVM.minTemperatureText
                        )

                        WeatherInfoView(
                            title: "Max",
                            value: weatherVM.maxTemperatureText
                        )
                    }
                }
                .padding()
            } else {
                ContentUnavailableView(     
                    "No Weather Data",
                    systemImage: "cloud.sun",
                    description: Text("Search for a city to see the weather")
                )
            }

            Spacer()
        }.alert(
            "Error",
            isPresented: .constant(weatherVM.errorMessage != nil)
        ) {
            Button("OK") {
                weatherVM.errorMessage = nil
            }
        } message: {
            Text(weatherVM.errorMessage?.localizedDescription ?? "")
        }
        .padding()
    }

    private func fetchWeather() {
        Task {
            await weatherVM.fetchWeather(city: city)
        }
    }
}

struct WeatherInfoView: View {
    let title: String
    let value: String

    var body: some View {
        VStack(spacing: 6) {
            Text(title)
                .font(.caption)
                .foregroundStyle(.secondary)

            Text(value)
                .font(.headline)
        }
    }
}

#Preview {
    ContentView()
}

In this tutorial we built a weather application using SwiftUI and the MVVM architectural pattern.
Along the way we learned how to:

  • Separate presentation logic from networking.

  • Use dependency injection through protocols.

  • Consume a REST API using async/await.

  • Manage loading and error states.

  • Keep the View focused only on displaying data.

Although this is a small application, these same architectural principles can be applied to much larger iOS projects.

Source Code

The complete source code for this project is available on GitHub:

https://github.com/GeraldCO/weather-app-swift

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-94084 | Suricata before 8.0.7 has an Http2ThreadMultiBuf use-after-free when a t…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick