Published on

MVVM arxitekturasi va boshqa xususiyatlarni yakuniy ko'rib chiqish

Authors

Ilovaga umumiy nazar

Ushbu video β€” kodlash emas, balki biz qurib yakunlagan ilovaning yakuniy sharhi.


Map layer

LocationsView-dagi xarita nisbatan murakkab tuzilmaga ega:

Map(coordinateRegion: $vm.mapRegion,
    annotationItems: vm.locations) { location in
    MapAnnotation(coordinate: location.coordinates) {
        LocationMapAnnotationView()
            .scaleEffect(vm.mapLocation == location ? 1 : 0.7)
            .shadow(radius: 10)
            .onTapGesture {
                vm.showNextLocation(location: location)
            }
    }
}
  • $vm.mapRegion β€” binding orqali ulangan: ViewModel-da mapRegion o'zgarganda, xarita avtomatik yangilanadi
  • Custom annotation β€” standart pin o'rniga LocationMapAnnotationView ishlatiladi β€” ilovaning o'ziga xos stil pin-i
  • .scaleEffect β€” tanlangan location-ning pin-i kattaroq ko'rinadi, qolganlarniki kichikroq
  • .onTapGesture β€” xaritadagi boshqa pin-larga bosib, pastki preview va xaritani o'sha location-ga o'tkazish imkoni

Location almashganda bir vaqtda bir nechta narsa sodir bo'ladi: pastki preview animatsiya bilan o'zgaradi, sarlavha yangilanadi, xarita siljiydi, pin kattayyadi β€” bu birgalikda juda yaxshi UI beradi.


Joylashuvlar ro'yxati

Header-dagi ro'yxat oddiy if ifodasi orqali ko'rsatiladi:

if vm.showLocationsList {
    LocationsListView()
        .transition(.move(edge: .top).animation(.easeInOut))
}
  • List komponenti ishlatilgani uchun, itemlar orasida ajratuvchi chiziq (divider) avtomatik paydo bo'ladi
  • List β€” scrollable: 5 ta location ham, 300 ta location ham ishlaydi
  • Yangi location qo'shish uchun faqat LocationsDataService-dagi massivga element qo'shish kifoya

Yangi location qo'shish

LocationsDataService.swift-dagi locations massiviga yangi element qo'shasiz:

static let locations: [Location] = [
    Location(
        name: "Colosseum",
        cityName: "Rome",
        coordinates: CLLocationCoordinate2D(latitude: 41.8902, longitude: 12.4922),
        description: "...",
        imageNames: ["rome-colosseum-1", "rome-colosseum-2"],
        link: "https://en.wikipedia.org/wiki/Colosseum"
    ),
    // ... yangi location
]

Muhim: imageNames qiymatidagi rasm nomlari Assets papkasidagi rasm nomlari bilan aynan mos kelishi kerak.


LocationPreviewStack β€” ZStack hiylasi

ZStack {
    ForEach(vm.locations) { location in
        if vm.mapLocation == location {
            LocationPreviewView(location: location)
                .transition(
                    .asymmetric(
                        insertion: .move(edge: .trailing),
                        removal: .move(edge: .leading)
                    )
                )
        }
    }
}

Bu kod β€” ko'rinishidan oddiy, lekin aslida aqlli:

  • ZStack barcha locationlar uchun LocationPreviewView yaratishga tayyor, lekin if sharti tufayli faqat joriy location-niki ko'rsatiladi
  • Asymmetric transition β€” kirish o'ngdan, chiqish chapdan: "keyingi" tuyg'usini beradi
  • Bitta qator transition kodi β€” katta vizual ta'sir

Transition kuchini ko'rsatish uchun (tajriba sifatida):

// Scale transition
.transition(.scale.animation(.easeInOut))

// Opacity transition
.transition(.opacity.animation(.easeInOut))

Bu misollar transition-lar qanchalik kuchli ekanligini ko'rsatadi β€” bitta qator kodni o'zgartirish butun animatsiya xulqini o'zgartiradi.


View body-ni o'qilishi oson saqlash

LocationsView-ning body-si:

var body: some View {
    ZStack {
        mapLayer
        contentLayer
    }
    .sheet(item: $vm.sheetLocation) { location in
        LocationDetailView(location: location)
    }
}

private var contentLayer: some View {
    VStack(spacing: 0) {
        header
        Spacer()
        locationsPreviewStack
    }
}

Bu muhim amaliyot: murakkab view-ni alohida computed variable-larga bo'lish β€” mapLayer, contentLayer, header, locationsPreviewStack va boshqalar. Natijada:

  • body qisqa va tushunarli
  • Boshqa dasturchi (yoki bir hafta o'tib o'zingiz) kodni tezda tushunadi
  • Xato topish osonlashadi

MVVM yakuniy sharhi

Model β€” Location

struct Location: Identifiable, Equatable {
    let id: String
    let name: String
    let cityName: String
    let coordinates: CLLocationCoordinate2D
    let description: String
    let imageNames: [String]
    let link: String

    // Equatable β€” id orqali tenglikni belgilaydi
    static func == (lhs: Location, rhs: Location) -> Bool {
        lhs.id == rhs.id
    }
}
  • Identifiable β€” ForEach-da ishlatish uchun
  • Equatable β€” if vm.mapLocation == location kabi tekshirishlar uchun, id asosida solishtirish

ViewModel β€” LocationsViewModel

class LocationsViewModel: ObservableObject {
    @Published var locations: [Location]
    @Published var mapLocation: Location
    @Published var mapRegion: MKCoordinateRegion
    @Published var showLocationsList: Bool = false
    @Published var sheetLocation: Location? = nil

    let mapSpan = MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)

    init() {
        let locations = LocationsDataService.locations
        self.locations = locations
        self.mapLocation = locations.first!
        self.mapRegion = MKCoordinateRegion(
            center: locations.first!.coordinates,
            span: mapSpan
        )
    }

    func showNextLocation(_ location: Location) { ... }
    func nextButtonPressed() { ... }
}

Ushbu ViewModel β€” ilovadagi barcha ekranlarning yagona manba:

  • locations massivi
  • joriy map location
  • joriy map region
  • ro'yxat ko'rinishi holati
  • sheet holati

Views β€” EnvironmentObject orqali ulash

// App.swift
@main
struct MapApp: App {
    @StateObject private var vm = LocationsViewModel()

    var body: some Scene {
        WindowGroup {
            LocationsView()
                .environmentObject(vm)
        }
    }
}
// Har bir view-da
@EnvironmentObject private var vm: LocationsViewModel

Ilovaning bosh nuqtasida LocationsViewModel yaratiladi va .environmentObject(vm) orqali muhitga joylashtiriladi. Bu yerdan, LocationsView-ning barcha avlod view-lari (LocationPreviewView, LocationsListView, LocationDetailView) β€” hech qaysi birida ViewModel-ni qo'lda uzatmasdan ham, @EnvironmentObject orqali shu ViewModel-ga murojaat qiladi.

Bu β€” SwiftUI-ning eng kuchli mexanizmlaridan biri: bir ob'ektni yaratib, butun ilova bo'ylab muammosiz ulashish imkoniyati.


Backend bilan ishlashga tayyorgarlik

Hozir locations LocationsDataService-dan yuklanadi. Agar haqiqiy backend bo'lsa, faqat init() qismini o'zgartirish kifoya:

init() {
    // Hozirgi holat:
    self.locations = LocationsDataService.locations

    // Backend bilan:
    self.locations = []
    downloadLocations()
}

func downloadLocations() {
    // API so'rov...
    // self.locations.append(contentsOf: downloadedLocations)
}

Ilovaning qolgan hech qanday kodi o'zgarmaydi β€” chunki barcha view-lar faqat vm.locations massiviga qaraydi. Massiv qayerdan to'ldirilishi β€” view-lar uchun farq qilmaydi.


Accent color β€” bir qadamda ilovaning ko'rinishini o'zgartirish

Assets.xcassets β†’ AccentColor-ni o'zgartirish β€” ilovaning barcha tugmalari, havolalar, pin-lar va boshqa elementlar bir vaqtda yangi rangga o'tadi. Hech qanday kod o'zgarmaydi.

Bu β€” AccentColor-ni ilovaning barcha joylarida aniq rang yozish o'rniga Color.accentColor yoki standart holatda qoldirish sababidir.


Ilova qo'llab-quvvatlanishi

QurilmaPortraitLandscape
iPhoneβœ…βŒ (o'chirilgan)
iPadβœ…βœ…
RejimQo'llab-quvvatlanadi
Light modeβœ…
Dark modeβœ… (avtomatik)

Dark mode avtomatik ishlagan sabab β€” barcha matnlarda .primary/.secondary, barcha fonlarda material ishlatilgan. Hech qanday aniq rang (Color.black, Color.white) ishlatilmagan.

Buy mea coffee