Bazel 14: Loyiha #3: "Ob-havo" ilova — Networking, SPM dependency, resurslar va mock testlar
📦 Bazel iOS — Mukammal Qo‘llanma & Amaliyot
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
NetworkKitmodulini yaratasiz. - Resurslarni (
Assets.xcassets, AppIcon, SVG/PNG ikonlar) Bazel'daactoolorqali to'g'ri kompilyatsiya qilasiz. - Hermetik testlash qoidasi: CI'da testlar tashqi internetga bog'lanmasligi uchun
URLProtocolorqali 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
- Loyiha arxitekturasi: Tarmoq va Resurslar
- Loyihaning papkalar tuzilishi
- WeatherCore moduli: Ob-havo ma'lumotlar modellari
- NetworkKit moduli: Generic HTTP mijoz
- Hermetik Mock Testlar: Nega internetga so'rov jo'natilmaydi?
- Resurslar va Asset Catalog: WeatherUI moduli
- App qatlami va ilovani birlashtirish
- Simulyatorda ishga tushirish
- Resurslar kompilyatsiyasi (actool) qanday keshlanadi?
- Amaliy mashqlar
- O'zingizni tekshiring
- Xulosa
- 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:
- Resurslarni aralashtirib yuborish: Rasmlar boshqa modulda bo'lsa-yu,
UIImage(named:)ularni topolmay qolishi. - 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
//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}
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
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
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
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)¤t_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
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
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
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:
bazel test //Network:NetworkTests
Natija:
//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
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
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
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
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}
# 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.xcassetsichidagi fayllarning SHA-256 xesh qiymatlari. - Chiqish:
Assets.cararxivi. - Agar siz
.swiftkodini o'zgartirsangiz,actoolqayta ishlamaydi (Action Cache Hit). - Agar faqat bitta
.pngrasm qo'shilsa, faqatactoolishlaydi,NetworkKityokiCoretegilmaydi.
Amaliy mashqlar {#mashqlar}
- 5 kunlik prognoz qo'shing.
WeatherData.swiftga kelgusi kunlar ro'yxatini saqlovchi model qo'shing va Open-Meteo'ningdaily=temperature_2m_max,temperature_2m_minparametrini integratsiya qiling. - Xato holati (Error State) mock testi.
NetworkTests.swiftda 500 status kodi kelganda ilova to'g'riNetworkError.invalidResponseotishini tekshiruvchi test yozing. - ⭐ App Icon qo'shing.
Assets.xcassets/AppIcon.appiconsetpapkasini yaratib,ios_applicationqoidasidaapp_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
URLProtocolqanday qutqarishini bilaman. - [ ] Asset Catalog (
.xcassets) resurslarini Bazel'dadata = [":WeatherAssets"]orqali ulashni tushunaman. - [ ]
WeatherServiceProtocolyordamida mock va real tarmoqni almashtirish mumkinligini bilaman. - [ ]
actoolaction 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
Manbalar {#manbalar}
- Open-Meteo Documentation — Bepul ochiq ob-havo REST API.
- Apple Documentation — URLProtocol — Tarmoq so'rovlarini tutib qolish va testlash.
- rules_apple Asset Catalog Handling — Resurslarni bundle qilish qoidalari.