Azimjon Abdurasulov

10. Real Loyiha #2: Weather App (3 modul + API)

🚀 Tuist — Mukammal Qo‘llanma & Amaliyot

🌤️ Real Loyiha #2: Weather App — 3 Modulli + API

"Modullar — bu qurilish bloklari. Ularni to'g'ri yig'sang, har qanday loyihani qura olasiz!"


Loyiha Haqida

Weather App — 3 modulli ilova + tashqi dependency (Alamofire). Siz:

  • ✅ External dependency qo'shishni o'rganasiz
  • ✅ Network modulni alohida qilishni ko'rasiz
  • ✅ Real API bilan ishlashni mashq qilasiz

Arxitektura

┌─────────────────┐
│   WeatherApp    │  ← Asosiy ilova (SwiftUI)
│    (.app)       │
└────┬───────┬────┘
     │       │
┌────▼──┐ ┌──▼──────────┐
│Weather│ │  NetworkKit  │  ← API calls (Alamofire)
│  UI   │ │ (.framework) │
└───────┘ └──────────────┘

1-Qadam: Loyiha Strukturasini Yaratish

mkdir WeatherTuist
cd WeatherTuist

# Tuist fayllar
touch Tuist.swift
touch Project.swift
mkdir -p Tuist

# Modul papkalar
mkdir -p NetworkKit/Sources
mkdir -p WeatherUI/Sources
mkdir -p WeatherApp/Sources
mkdir -p WeatherApp/Resources

2-Qadam: Tuist.swift

import ProjectDescription

let tuist = Tuist()

3-Qadam: Tuist/Package.swift — External Dependency

// Tuist/Package.swift
// swift-tools-version: 5.9
import PackageDescription

#if TUIST
import ProjectDescription

let packageSettings = PackageSettings(
    productTypes: [
        "Alamofire": .framework,
    ]
)
#endif

let package = Package(
    name: "WeatherTuist",
    dependencies: [
        .package(url: "https://github.com/Alamofire/Alamofire", from: "5.9.0"),
    ]
)

4-Qadam: Project.swift

// Project.swift
import ProjectDescription

let project = Project(
    name: "WeatherTuist",
    targets: [
        // 1. NetworkKit — API aloqa moduli
        .target(
            name: "NetworkKit",
            destinations: .iOS,
            product: .framework,
            bundleId: "com.example.NetworkKit",
            deploymentTargets: .iOS("16.0"),
            infoPlist: .default,
            sources: ["NetworkKit/Sources/**"],
            dependencies: [
                .external(name: "Alamofire"),   // ← External dependency
            ]
        ),
        
        // 2. WeatherUI — UI komponentlar moduli
        .target(
            name: "WeatherUI",
            destinations: .iOS,
            product: .framework,
            bundleId: "com.example.WeatherUI",
            deploymentTargets: .iOS("16.0"),
            infoPlist: .default,
            sources: ["WeatherUI/Sources/**"],
            dependencies: []    // Hech kimga bog'liq emas (mustaqil UI)
        ),
        
        // 3. WeatherApp — Asosiy ilova
        .target(
            name: "WeatherApp",
            destinations: .iOS,
            product: .app,
            bundleId: "com.example.WeatherApp",
            deploymentTargets: .iOS("16.0"),
            infoPlist: .extendingDefault(with: [
                "UILaunchScreen": [
                    "UIColorName": "",
                    "UIImageName": "",
                ],
            ]),
            sources: ["WeatherApp/Sources/**"],
            resources: ["WeatherApp/Resources/**"],
            dependencies: [
                .target(name: "NetworkKit"),    // API uchun
                .target(name: "WeatherUI"),     // UI uchun
            ]
        ),
    ]
)

5-Qadam: NetworkKit — API Service

NetworkKit/Sources/WeatherModel.swift

import Foundation

/// Ob-havo ma'lumotlari modeli (OpenWeatherMap API formatida)
public struct WeatherResponse: Codable {
    public let name: String          // Shahar nomi
    public let main: MainWeather
    public let weather: [WeatherInfo]
    public let wind: Wind
}

public struct MainWeather: Codable {
    public let temp: Double          // Harorat (Kelvin)
    public let humidity: Int         // Namlik (%)
    public let feelsLike: Double     // His qilinadigan harorat
    
    enum CodingKeys: String, CodingKey {
        case temp, humidity
        case feelsLike = "feels_like"
    }
    
    /// Kelvin → Celsius
    public var tempCelsius: Int {
        Int(temp - 273.15)
    }
    
    public var feelsLikeCelsius: Int {
        Int(feelsLike - 273.15)
    }
}

public struct WeatherInfo: Codable {
    public let main: String          // "Clear", "Clouds", "Rain"
    public let description: String   // Batafsil tavsif
    public let icon: String          // Icon kodi
    
    /// Icon URL
    public var iconURL: URL? {
        URL(string: "https://openweathermap.org/img/wn/\(icon)@2x.png")
    }
}

public struct Wind: Codable {
    public let speed: Double         // Shamol tezligi (m/s)
}

NetworkKit/Sources/WeatherService.swift

import Foundation
import Alamofire

/// Ob-havo API service
public class WeatherService {
    private let apiKey: String
    private let baseURL = "https://api.openweathermap.org/data/2.5"
    
    public init(apiKey: String = "YOUR_API_KEY") {
        self.apiKey = apiKey
    }
    
    /// Shahar nomi bo'yicha ob-havo olish
    public func fetchWeather(city: String) async throws -> WeatherResponse {
        let parameters: Parameters = [
            "q": city,
            "appid": apiKey,
            "lang": "uz",
        ]
        
        let response = try await AF.request(
            "\(baseURL)/weather",
            parameters: parameters
        )
        .validate()
        .serializingDecodable(WeatherResponse.self)
        .value
        
        return response
    }
}

6-Qadam: WeatherUI — UI Komponentlar

WeatherUI/Sources/WeatherCardView.swift

import SwiftUI

/// Ob-havo kartochkasi — qayta ishlatilishi mumkin bo'lgan UI komponent
public struct WeatherCardView: View {
    let title: String
    let value: String
    let icon: String
    let color: Color
    
    public init(title: String, value: String, icon: String, color: Color = .blue) {
        self.title = title
        self.value = value
        self.icon = icon
        self.color = color
    }
    
    public var body: some View {
        VStack(spacing: 8) {
            Image(systemName: icon)
                .font(.title)
                .foregroundStyle(color)
            
            Text(value)
                .font(.title2)
                .bold()
            
            Text(title)
                .font(.caption)
                .foregroundStyle(.secondary)
        }
        .frame(maxWidth: .infinity)
        .padding()
        .background(color.opacity(0.1))
        .cornerRadius(16)
    }
}

/// Loading shimmer effekti
public struct ShimmerView: View {
    @State private var isAnimating = false
    
    public init() {}
    
    public var body: some View {
        RoundedRectangle(cornerRadius: 16)
            .fill(Color.gray.opacity(0.3))
            .frame(height: 100)
            .overlay(
                RoundedRectangle(cornerRadius: 16)
                    .fill(
                        LinearGradient(
                            colors: [.clear, .white.opacity(0.3), .clear],
                            startPoint: .leading,
                            endPoint: .trailing
                        )
                    )
                    .offset(x: isAnimating ? 300 : -300)
            )
            .clipped()
            .onAppear {
                withAnimation(.linear(duration: 1.5).repeatForever(autoreverses: false)) {
                    isAnimating = true
                }
            }
    }
}

7-Qadam: WeatherApp — Asosiy Ilova

WeatherApp/Sources/WeatherTuistApp.swift

import SwiftUI

@main
struct WeatherTuistApp: App {
    var body: some Scene {
        WindowGroup {
            WeatherMainView()
        }
    }
}

WeatherApp/Sources/WeatherMainView.swift

import SwiftUI
import NetworkKit    // ← API service
import WeatherUI     // ← UI komponentlar

struct WeatherMainView: View {
    @State private var city = "Tashkent"
    @State private var weather: WeatherResponse?
    @State private var isLoading = false
    @State private var errorMessage: String?
    
    private let service = WeatherService(apiKey: "YOUR_API_KEY")
    
    var body: some View {
        NavigationStack {
            ScrollView {
                VStack(spacing: 20) {
                    // Shahar qidirish
                    searchSection
                    
                    // Ob-havo ma'lumotlari
                    if isLoading {
                        loadingSection
                    } else if let weather = weather {
                        weatherSection(weather)
                    } else if let error = errorMessage {
                        errorSection(error)
                    }
                }
                .padding()
            }
            .navigationTitle("🌤️ Ob-havo")
        }
        .task {
            await loadWeather()
        }
    }
    
    // MARK: - Sections
    
    private var searchSection: some View {
        HStack {
            TextField("Shahar nomi...", text: $city)
                .textFieldStyle(.roundedBorder)
                .onSubmit { Task { await loadWeather() } }
            
            Button(action: { Task { await loadWeather() } }) {
                Image(systemName: "magnifyingglass")
                    .padding(10)
                    .background(.blue)
                    .foregroundStyle(.white)
                    .cornerRadius(8)
            }
        }
    }
    
    private var loadingSection: some View {
        VStack(spacing: 12) {
            ShimmerView()    // ← WeatherUI dan
            ShimmerView()
        }
    }
    
    private func weatherSection(_ data: WeatherResponse) -> some View {
        VStack(spacing: 16) {
            // Shahar va harorat
            VStack(spacing: 4) {
                Text(data.name)
                    .font(.largeTitle)
                    .bold()
                
                Text("\(data.main.tempCelsius)°C")
                    .font(.system(size: 60, weight: .thin))
                
                Text(data.weather.first?.description.capitalized ?? "")
                    .font(.title3)
                    .foregroundStyle(.secondary)
            }
            
            // Kartochkalar (WeatherUI komponentlari)
            LazyVGrid(columns: [
                GridItem(.flexible()),
                GridItem(.flexible()),
            ], spacing: 12) {
                WeatherCardView(
                    title: "His qilinadi",
                    value: "\(data.main.feelsLikeCelsius)°C",
                    icon: "thermometer",
                    color: .orange
                )
                WeatherCardView(
                    title: "Namlik",
                    value: "\(data.main.humidity)%",
                    icon: "humidity",
                    color: .blue
                )
                WeatherCardView(
                    title: "Shamol",
                    value: "\(String(format: "%.1f", data.wind.speed)) m/s",
                    icon: "wind",
                    color: .teal
                )
                WeatherCardView(
                    title: "Holat",
                    value: data.weather.first?.main ?? "-",
                    icon: "cloud.sun",
                    color: .purple
                )
            }
        }
    }
    
    private func errorSection(_ message: String) -> some View {
        VStack(spacing: 8) {
            Image(systemName: "exclamationmark.triangle")
                .font(.largeTitle)
                .foregroundStyle(.red)
            Text(message)
                .foregroundStyle(.secondary)
        }
    }
    
    // MARK: - Actions
    
    private func loadWeather() async {
        isLoading = true
        errorMessage = nil
        
        do {
            weather = try await service.fetchWeather(city: city)
        } catch {
            errorMessage = "Ma'lumot yuklanmadi: \(error.localizedDescription)"
        }
        
        isLoading = false
    }
}

8-Qadam: Generate va Run

# 1. External dependency yuklash
tuist install

# 2. Xcode loyiha yaratish
tuist generate

# 3. Xcode da Cmd+R — run!

Dependency Graph

tuist graph
┌──────────────┐
│  WeatherApp  │
└──┬────────┬──┘
   │        │
┌──▼────┐ ┌─▼──────────┐
│Weather│ │ NetworkKit  │
│  UI   │ └──────┬──────┘
└───────┘        │
          ┌──────▼──────┐
          │  Alamofire  │  ← External
          └─────────────┘

Nimani o'rgandik?

MavzuTushuntirish
External dependencyTuist/Package.swift + .external(name:)
tuist installDependency'larni yuklab olish
3 modul arxitekturaNetworkKit (API) + WeatherUI (UI) + App
API integrationAlamofire bilan network call
Reusable UIWeatherCardView — har joyda ishlatish mumkin

💡 Keyingi loyiha: Real Loyiha #3: E-Commerce App — 5+ modulli TMA arxitektura, micro-features.

"Modullar soni oshgan sari — professional darajangiz ham oshadi!" 📈

Markdown manba

iOS Lessons