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?
| Mavzu | Tushuntirish |
|---|---|
| External dependency | Tuist/Package.swift + .external(name:) |
tuist install | Dependency'larni yuklab olish |
| 3 modul arxitektura | NetworkKit (API) + WeatherUI (UI) + App |
| API integration | Alamofire bilan network call |
| Reusable UI | WeatherCardView — 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!" 📈