# Bazel 14: Loyiha #3: "Ob-havo" ilova — Networking, SPM dependency, resurslar va mock testlar

> **Holat:** 2026-yil oktabr. Ushbu darsda biz uchinchi amaliy loyihamizni quramiz: **"Ob-havo"** — real REST API (Open-Meteo) dan ma'lumot oluvchi, Asset Catalog (`.xcassets`) resurslari bilan bezatilgan, tashqi SPM kutubxonalarini Bzlmod orqali integratsiya qilgan va eng muhimi — **100% offline hermetik mock testlar** bilan himoyalangan zamonaviy iOS SwiftUI ilovasi. Kod misollari Bazel 9.3.0, rules_apple 5.3.0, rules_swift 4.2.1 va Xcode 27.1'da tekshirilgan.

> **Bu darsdan keyin siz:** Bazel loyihalarida rasmlar, ranglar va Asset Catalog'larni to'g'ri ulashni, asinxron tarmoq qatlamini (Networking) arxitekturaviy ajratishni va CI tizimlarida internet bo'lmasa ham muammosiz o'tadigan deterministik mock testlar yozishni o'rganasiz.

- Haqiqiy REST API chaqiruvlari uchun async/await asosidagi toza `NetworkKit` modulini yaratasiz.
- Resurslarni (`Assets.xcassets`, AppIcon, SVG/PNG ikonlar) Bazel'da `actool` orqali to'g'ri kompilyatsiya qilasiz.
- **Hermetik testlash qoidasi:** CI'da testlar tashqi internetga bog'lanmasligi uchun `URLProtocol` orqali tarmoq so'rovlarini tutib qolib (intercept), lokal JSON fixture'lar orqali sinaysiz.
- Modullarda resurslarga xavfsiz kirish (`Bundle.module`) qoidalarini amalda tushunasiz.
- Toshkent va boshqa shaharlar ob-havo ma'lumotlarini chiroyli SwiftUI interfeysida ko'rsatasiz.

## Mundarija

1. [Loyiha arxitekturasi: Tarmoq va Resurslar](#arxitektura)
2. [Loyihaning papkalar tuzilishi](#tuzilish)
3. [WeatherCore moduli: Ob-havo ma'lumotlar modellari](#core)
4. [NetworkKit moduli: Generic HTTP mijoz](#network)
5. [Hermetik Mock Testlar: Nega internetga so'rov jo'natilmaydi?](#mock-tests)
6. [Resurslar va Asset Catalog: WeatherUI moduli](#ui-assets)
7. [App qatlami va ilovani birlashtirish](#app)
8. [Simulyatorda ishga tushirish](#simulyator)
9. [Resurslar kompilyatsiyasi (actool) qanday keshlanadi?](#actool-cache)
10. [Amaliy mashqlar](#mashqlar)
11. [O'zingizni tekshiring](#checklist)
12. [Xulosa](#xulosa)
13. [Manbalar](#manbalar)

---

## 1. Loyiha arxitekturasi: Tarmoq va Resurslar {#arxitektura}

Mobil ilovalar odatda faqat koddan iborat emas. Unda katta hajmdagi grafik resurslar (ikonlar, rasmlar, ranglar palitrasi) va tashqi API bilan muloqot qiluvchi tarmoq drayveri bo'ladi.

Ko'p jamoalarda uchraydigan 2 ta xato:
1. **Resurslarni aralashtirib yuborish:** Rasmlar boshqa modulda bo'lsa-yu, `UIImage(named:)` ularni topolmay qolishi.
2. **CI'da tarmoqqa so'rov yuborish:** Unit testlar ichida real API serverga so'rov jo'natish. Natijada internet uzilsa yoki API sekinlashsa, CI qizilga aylanadi.

Biz bularni to'g'ri hal qilamiz:

![Loyiha #3: "Ob-havo" ilovasi arxitekturasi va Hermetik Mock Test oqimi](/lessons/images/bazel/bazel_14_obhavo_layers.svg)

- `//Core:WeatherCore` — sof modellar.
- `//Network:NetworkKit` — tarmoq mantiqi + XCTest mock'lar.
- `//UI:WeatherUI` — SwiftUI komponentlari + `Assets.xcassets`.
- `//App:ObHavo` — yakuniy ilova.

---

## 2. Loyihaning papkalar tuzilishi {#tuzilish}

```text
obhavo-app/
├── MODULE.bazel
├── .bazelrc
├── Core/
│   ├── BUILD.bazel
│   └── Sources/
│       ├── WeatherData.swift
│       └── City.swift
├── Network/
│   ├── BUILD.bazel
│   ├── Sources/
│   │   ├── APIClient.swift
│   │   └── WeatherEndpoint.swift
│   └── Tests/
│       ├── MockURLProtocol.swift
│       ├── NetworkTests.swift
│       └── Fixtures/
│           └── weather_tashkent.json
├── UI/
│   ├── BUILD.bazel
│   ├── Assets.xcassets/
│   │   ├── Contents.json
│   │   ├── sun.imageset/...
│   │   └── rain.imageset/...
│   └── Sources/
│       ├── WeatherCardView.swift
│       └── WeatherDashboardView.swift
└── App/
    ├── BUILD.bazel
    ├── Info.plist
    └── Sources/
        └── ObHavoApp.swift
```

---

## 3. WeatherCore moduli: Ob-havo ma'lumotlar modellari {#core}

Open-Meteo ochiq API (hech qanday API key talab qilmaydi) javobiga mos Swift modellari:

### `Core/Sources/WeatherData.swift`

```swift
import Foundation

public struct CurrentWeather: Codable, Equatable, Sendable {
    public let temperature: Double
    public let windSpeed: Double
    public let weatherCode: Int

    public init(temperature: Double, windSpeed: Double, weatherCode: Int) {
        self.temperature = temperature
        self.windSpeed = windSpeed
        self.weatherCode = weatherCode
    }

    public var conditionDescription: String {
        switch weatherCode {
        case 0: return "Ochiq havo"
        case 1, 2, 3: return "Bulutli"
        case 51...67: return "Yomg'ir"
        case 71...77: return "Qor"
        default: return "O'zgaruvchan"
        }
    }

    public var iconName: String {
        switch weatherCode {
        case 0: return "sun.max.fill"
        case 1...3: return "cloud.sun.fill"
        case 51...67: return "cloud.rain.fill"
        case 71...77: return "snowflake"
        default: return "cloud.fill"
        }
    }
}

public struct City: Identifiable, Sendable {
    public let id: String
    public let name: String
    public let latitude: Double
    public let longitude: Double

    public init(id: String, name: String, latitude: Double, longitude: Double) {
        self.id = id
        self.name = name
        self.latitude = latitude
        self.longitude = longitude
    }

    public static let tashkent = City(id: "TAS", name: "Toshkent", latitude: 41.2995, longitude: 69.2401)
    public static let samarkand = City(id: "SKD", name: "Samarqand", latitude: 39.6542, longitude: 66.9597)
    public static let bukhara = City(id: "BHK", name: "Buxoro", latitude: 39.7747, longitude: 64.4286)
}
```

### `Core/BUILD.bazel`

```python
load("@rules_swift//swift:swift.bzl", "swift_library")

swift_library(
    name = "WeatherCore",
    srcs = glob(["Sources/**/*.swift"]),
    module_name = "WeatherCore",
    visibility = ["//visibility:public"],
)
```

---

## 4. NetworkKit moduli: Generic HTTP mijoz {#network}

### `Network/Sources/APIClient.swift`

```swift
import Foundation
import WeatherCore

public enum NetworkError: Error {
    case invalidURL
    case invalidResponse
    case decodingError
}

public protocol WeatherServiceProtocol: Sendable {
    func fetchWeather(for city: City) async throws -> CurrentWeather
}

public final class WeatherAPIClient: WeatherServiceProtocol {
    private let session: URLSession

    public init(session: URLSession = .shared) {
        self.session = session
    }

    public func fetchWeather(for city: City) async throws -> CurrentWeather {
        let urlString = "https://api.open-meteo.com/v1/forecast?latitude=\(city.latitude)&longitude=\(city.longitude)&current_weather=true"
        guard let url = URL(string: urlString) else {
            throw NetworkError.invalidURL
        }

        let (data, response) = try await session.data(from: url)
        guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
            throw NetworkError.invalidResponse
        }

        struct ResponseWrapper: Decodable {
            let current_weather: CurrentWeatherPayload
        }
        struct CurrentWeatherPayload: Decodable {
            let temperature: Double
            let windspeed: Double
            let weathercode: Int
        }

        let decoded = try JSONDecoder().decode(ResponseWrapper.self, from: data)
        return CurrentWeather(
            temperature: decoded.current_weather.temperature,
            windSpeed: decoded.current_weather.windspeed,
            weatherCode: decoded.current_weather.weathercode
        )
    }
}
```

---

## 5. Hermetik Mock Testlar: Nega internetga so'rov jo'natilmaydi? {#mock-tests}

Bazel tamoyiliga ko'ra, har qanday test tashqi muhitdan ajratilgan (hermetik) bo'lishi kerak. Biz `URLProtocol` yordamida soxta (mock) tarmoq adapteri quramiz.

### `Network/Tests/MockURLProtocol.swift`

```swift
import Foundation

public final class MockURLProtocol: URLProtocol {
    public static var mockData: Data?
    public static var mockResponse: HTTPURLResponse?
    public static var mockError: Error?

    public override class func canInit(with request: URLRequest) -> Bool {
        return true // Barcha so'rovlarni o'zimiz ushlaymiz!
    }

    public override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }

    public override func startLoading() {
        if let error = MockURLProtocol.mockError {
            client?.urlProtocol(self, didFailWithError: error)
            return
        }

        if let response = MockURLProtocol.mockResponse {
            client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
        }

        if let data = MockURLProtocol.mockData {
            client?.urlProtocol(self, didLoad: data)
        }

        client?.urlProtocolDidFinishLoading(self)
    }

    public override func stopLoading() {}
}
```

### `Network/Tests/NetworkTests.swift`

```swift
import XCTest
@testable import NetworkKit
@testable import WeatherCore

final class NetworkTests: XCTestCase {
    var client: WeatherAPIClient!

    override func setUp() {
        super.setUp()
        let config = URLSessionConfiguration.ephemeral
        config.protocolClasses = [MockURLProtocol.self]
        let session = URLSession(configuration: config)
        client = WeatherAPIClient(session: session)
    }

    func testFetchWeatherSuccess() async throws {
        let fakeJSON = """
        {
            "current_weather": {
                "temperature": 26.5,
                "windspeed": 12.3,
                "weathercode": 0
            }
        }
        """.data(using: .utf8)!

        MockURLProtocol.mockData = fakeJSON
        MockURLProtocol.mockResponse = HTTPURLResponse(
            url: URL(string: "https://api.open-meteo.com")!,
            statusCode: 200,
            httpVersion: nil,
            headerFields: nil
        )

        let weather = try await client.fetchWeather(for: .tashkent)

        XCTAssertEqual(weather.temperature, 26.5)
        XCTAssertEqual(weather.windSpeed, 12.3)
        XCTAssertEqual(weather.conditionDescription, "Ochiq havo")
    }
}
```

### `Network/BUILD.bazel`

```python
load("@rules_swift//swift:swift.bzl", "swift_library")
load("@rules_apple//apple:ios.bzl", "ios_unit_test")

swift_library(
    name = "NetworkKit",
    srcs = glob(["Sources/**/*.swift"]),
    module_name = "NetworkKit",
    deps = ["//Core:WeatherCore"],
    visibility = ["//visibility:public"],
)

swift_library(
    name = "NetworkTestLib",
    srcs = glob(["Tests/**/*.swift"]),
    deps = [
        ":NetworkKit",
        "//Core:WeatherCore",
    ],
)

ios_unit_test(
    name = "NetworkTests",
    minimum_os_version = "17.0",
    deps = [":NetworkTestLib"],
    visibility = ["//visibility:public"],
)
```

Testni yugurtirish:

```bash
bazel test //Network:NetworkTests
```

Natija:
```text
//Network:NetworkTests                           PASSED in 0.08s
```
0.08 soniyada 100% ishonchli va offline!

---

## 6. Resurslar va Asset Catalog: WeatherUI moduli {#ui-assets}

Bazel'da Asset Catalog (`.xcassets`) resurslari `swift_library` yoki `apple_bundle_resources` orqali ulanadi.

### `UI/BUILD.bazel`

```python
load("@rules_swift//swift:swift.bzl", "swift_library")

filegroup(
    name = "WeatherAssets",
    srcs = glob(["Assets.xcassets/**"]),
)

swift_library(
    name = "WeatherUI",
    srcs = glob(["Sources/**/*.swift"]),
    data = [":WeatherAssets"],
    module_name = "WeatherUI",
    deps = ["//Core:WeatherCore"],
    visibility = ["//visibility:public"],
)
```

### `UI/Sources/WeatherCardView.swift`

```swift
import SwiftUI
import WeatherCore

public struct WeatherCardView: View {
    public let city: City
    public let weather: CurrentWeather?

    public init(city: City, weather: CurrentWeather?) {
        self.city = city
        self.weather = weather
    }

    public var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack {
                Text(city.name)
                    .font(.title2)
                    .fontWeight(.bold)
                    .foregroundColor(.white)
                Spacer()
                if let weather = weather {
                    Image(systemName: weather.iconName)
                        .font(.title)
                        .foregroundColor(.yellow)
                }
            }

            if let weather = weather {
                HStack(alignment: .firstTextBaseline) {
                    Text("\(Int(weather.temperature))°C")
                        .font(.system(size: 48, weight: .semibold, design: .rounded))
                        .foregroundColor(.white)
                    Spacer()
                    Text(weather.conditionDescription)
                        .font(.headline)
                        .foregroundColor(.white.opacity(0.85))
                }

                HStack {
                    Label("\(weather.windSpeed, specifier: "%.1f") km/soat", systemImage: "wind")
                        .font(.caption)
                        .foregroundColor(.white.opacity(0.8))
                }
            } else {
                ProgressView()
                    .progressViewStyle(CircularProgressViewStyle(tint: .white))
                    .frame(maxWidth: .infinity, alignment: .center)
            }
        }
        .padding(20)
        .background(
            LinearGradient(
                colors: [Color.blue.opacity(0.8), Color.indigo.opacity(0.8)],
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            )
        )
        .cornerRadius(18)
        .shadow(color: .black.opacity(0.15), radius: 8, x: 0, y: 4)
    }
}
```

---

## 7. App qatlami va ilovani birlashtirish {#app}

### `App/Sources/ObHavoApp.swift`

```swift
import SwiftUI
import WeatherCore
import NetworkKit
import WeatherUI

@main
struct ObHavoApp: App {
    private let client: WeatherServiceProtocol = WeatherAPIClient()
    @State private var weatherMap: [String: CurrentWeather] = [:]
    private let cities: [City] = [.tashkent, .samarkand, .bukhara]

    var body: some Scene {
        WindowGroup {
            NavigationStack {
                ScrollView {
                    VStack(spacing: 16) {
                        ForEach(cities) { city in
                            WeatherCardView(city: city, weather: weatherMap[city.id])
                        }
                    }
                    .padding()
                }
                .navigationTitle("O'zbekiston Ob-havosi")
                .task {
                    await loadAllWeather()
                }
                .refreshable {
                    await loadAllWeather()
                }
            }
        }
    }

    private func loadAllWeather() async {
        for city in cities {
            if let weather = try? await client.fetchWeather(for: city) {
                weatherMap[city.id] = weather
            }
        }
    }
}
```

### `App/BUILD.bazel`

```python
load("@rules_swift//swift:swift.bzl", "swift_library")
load("@rules_apple//apple:ios.bzl", "ios_application")

swift_library(
    name = "AppLib",
    srcs = ["Sources/ObHavoApp.swift"],
    deps = [
        "//Core:WeatherCore",
        "//Network:NetworkKit",
        "//UI:WeatherUI",
    ],
)

ios_application(
    name = "ObHavo",
    bundle_id = "uz.azimjondev.obhavo",
    families = ["iphone", "ipad"],
    infoplists = ["Info.plist"],
    minimum_os_version = "17.0",
    deps = [":AppLib"],
    visibility = ["//visibility:public"],
)
```

---

## 8. Simulyatorda ishga tushirish {#simulyator}

```bash
# Ilovani qurish va simulyatorga ochish
bazel run //App:ObHavo
```

Simulyatorda Toshkent, Samarqand va Buxoro uchun real vaqtdagi ob-havo kartalari paydo bo'ladi!

---

## 9. Resurslar kompilyatsiyasi (actool) qanday keshlanadi? {#actool-cache}

Xcode loyihalarida Asset Catalog keshlanishi ko'pincha nosoz ishlaydi va kichik rasm o'zgarganda butun loyiha clean qilinishi talab qilinadi.

Bazel'da esa `actool` alohida deterministik action sifatida ishlaydi:
- Kirish: `Assets.xcassets` ichidagi fayllarning SHA-256 xesh qiymatlari.
- Chiqish: `Assets.car` arxivi.
- Agar siz `.swift` kodini o'zgartirsangiz, `actool` qayta ishlamaydi (Action Cache Hit).
- Agar faqat bitta `.png` rasm qo'shilsa, faqat `actool` ishlaydi, `NetworkKit` yoki `Core` tegilmaydi.

---

## Amaliy mashqlar {#mashqlar}

1. **5 kunlik prognoz qo'shing.** `WeatherData.swift`ga kelgusi kunlar ro'yxatini saqlovchi model qo'shing va Open-Meteo'ning `daily=temperature_2m_max,temperature_2m_min` parametrini integratsiya qiling.
2. **Xato holati (Error State) mock testi.** `NetworkTests.swift`da 500 status kodi kelganda ilova to'g'ri `NetworkError.invalidResponse` otishini tekshiruvchi test yozing.
3. ⭐ **App Icon qo'shing.** `Assets.xcassets/AppIcon.appiconset` papkasini yaratib, `ios_application` qoidasida `app_icons = ["//UI:AppIcon"]` atributini bog'lang va ilovani simulyatorda yangi ikonka bilan ko'ring.

---

## O'zingizni tekshiring {#checklist}

- [ ] CI testlarida real tarmoqqa so'rov yuborish nega yomon ekanini va `URLProtocol` qanday qutqarishini bilaman.
- [ ] Asset Catalog (`.xcassets`) resurslarini Bazel'da `data = [":WeatherAssets"]` orqali ulashni tushunaman.
- [ ] `WeatherServiceProtocol` yordamida mock va real tarmoqni almashtirish mumkinligini bilaman.
- [ ] `actool` action keshlanishi tufayli resurslar har safar qayta yig'ilmasligini tushunaman.

---

## Xulosa {#xulosa}

- Biz tashqi tarmoq bilan ishlovchi, resurslarga boy haqiqiy iOS ilovani qurdik.
- Hermetik mock testlar yordamida testlarni bir soniyadan kam vaqtda va internetsiz yugurtirishga erishdik.
- Endi navbatdagi bosqich — enterprise darajadagi eng katta chaqiriq: **12 modulli SuperApp va jamoaviy Remote Cache!**
- **Keyingi dars:** [Bazel 15: Loyiha #4: 12 modulli "SuperApp" va jamoaviy remote cache](/ios-lessons/bazel-15-loyiha-superapp)

---

## Manbalar {#manbalar}

1. [Open-Meteo Documentation](https://open-meteo.com/en/docs) — Bepul ochiq ob-havo REST API.
2. [Apple Documentation — URLProtocol](https://developer.apple.com/documentation/foundation/urlprotocol) — Tarmoq so'rovlarini tutib qolish va testlash.
3. [rules_apple Asset Catalog Handling](https://github.com/bazelbuild/rules_apple) — Resurslarni bundle qilish qoidalari.
