Published on

SwiftUI-da Sheet orqali Location Detail View yaratish

Authors

LocationDetailView β€” yangi fayl

Navigator-da o'ng tugmani bosib, Views papkasida yangi SwiftUI View fayl yaratamiz β€” unga LocationDetailView deb nom beramiz.

Bu view β€” ma'lum bir location haqidagi barcha tafsilotlarni ko'rsatadigan ekran. Shuning uchun, bu view-ni yaratganda, unga location uzatilishi kerak:

struct LocationDetailView: View {
    let location: Location

    var body: some View {
        Text(location.name)
    }
}

Preview-ni to'g'rilash uchun, LocationsDataService-dan birinchi locationni olamiz (preview uchun ! bilan explicit unwrap qilamiz β€” haqiqiy kodda bunday qilmang):

#Preview {
    LocationDetailView(location: LocationsDataService.locations.first!)
        .environmentObject(LocationsViewModel())
}

ScrollView va asosiy tuzilma

Bu ekranning hammasi ScrollView ichida bo'ladi β€” chunki ko'p kontent bor va ekranga sig'masligi mumkin:

var body: some View {
    ScrollView {
        VStack {
            imageSection
                .shadow(color: .black.opacity(0.3), radius: 20, x: 0, y: 10)

            VStack(alignment: .leading, spacing: 16) {
                titleSection
                Divider()
                descriptionSection
                Divider()
                mapLayer
            }
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding()
        }
    }
    .ignoresSafeArea()
    .overlay(alignment: .topLeading) {
        backButton
    }
    .background(.ultraThinMaterial)
}

imageSection β€” rasm karuseli

Bir nechta rasmni swipe qilib ko'rish uchun TabView va PageTabViewStyle-dan foydalanamiz:

private var imageSection: some View {
    TabView {
        ForEach(location.imageNames, id: \.self) { name in
            Image(name)
                .resizable()
                .scaledToFill()
                .frame(width: UIScreen.main.bounds.width)
                .clipped()
        }
    }
    .frame(height: 500)
    .tabViewStyle(.page)
}

Muhim: .frame(width: UIScreen.main.bounds.width) va .clipped() β€” keyingi rasm oldingisinining ustiga chiqib kelishining oldini oladi. Agar bu bo'lmasa, swiping paytida g'alati vizual effekt paydo bo'ladi.

Eslatma: iPad uchun moslashtirilganda, bu qatirni keyinroq o'zgartirishimiz kerak bo'ladi, chunki iPad-da bu ekran to'liq kenglikda bo'lmaydi.


titleSection

private var titleSection: some View {
    VStack(alignment: .leading, spacing: 8) {
        Text(location.name)
            .font(.largeTitle)
            .fontWeight(.semibold)

        Text(location.cityName)
            .font(.title3)
            .foregroundColor(.secondary)
    }
}

Muhim: VStack-ni to'liq kenglikka yoyish uchun .frame(maxWidth: .infinity, alignment: .leading) tashqi VStack-ga qo'llaniladi β€” aks holda VStack faqat kontent kengligida qoladi.


descriptionSection

private var descriptionSection: some View {
    VStack(alignment: .leading, spacing: 16) {
        Text(location.description)
            .font(.subheadline)
            .foregroundColor(.secondary)

        if let url = URL(string: location.link) {
            Link("Read more on Wikipedia", destination: url)
                .font(.headline)
                .tint(.blue)
        }
    }
}

Muhim: URL(string:) optional qaytaradi, shuning uchun if let bilan xavfsiz unwrap qilish kerak. tint(.blue) β€” havolalar an'anaviy ko'k rangda ko'rinishi uchun (accent color-ga bog'liq bo'lmasligi uchun).


mapLayer β€” kichik xarita

Location detail ekranida xuddi shu locationning ustida kichik, interaktiv bo'lmagan xarita ko'rsatiladi:

@EnvironmentObject private var vm: LocationsViewModel

private var mapLayer: some View {
    Map(coordinateRegion: .constant(MKCoordinateRegion(
        center: location.coordinates,
        span: vm.mapSpan
    )),
        annotationItems: [location]) { location in
        MapAnnotation(coordinate: location.coordinates) {
            LocationMapAnnotationView()
                .shadow(radius: 10)
        }
    }
    .aspectRatio(1, contentMode: .fit)
    .cornerRadius(30)
    .allowsHitTesting(false)
}

Muhim jihatlar:

  • .constant(...) β€” bu xarita hech qachon o'zgarmaydi, shuning uchun @State yoki @Binding shart emas. MKCoordinateRegion doimiy qiymat sifatida uzatiladi.
  • annotationItems: [location] β€” butun locationlar ro'yxati emas, faqat joriy location β€” bitta pin ko'rsatiladi.
  • .aspectRatio(1, contentMode: .fit) β€” xaritani kvadrat shaklida ko'rsatish uchun.
  • .allowsHitTesting(false) β€” xaritani touch-dan himoya qiladi. Sheet scroll bo'ladi, ScrollView scroll bo'ladi, xarita ham scroll bo'lsa β€” foydalanuvchi uchun chalkash bo'lar edi. Shuning uchun xaritani "qotib qo'yamiz".
  • import MapKit β€” fayl boshida bo'lishi shart.

backButton va material background

Sheet-ni yopish uchun back tugmasi kerak. U scroll view-ning ustida, yuqori chap qismida joylashadi:

private var backButton: some View {
    Button {
        vm.sheetLocation = nil
    } label: {
        Image(systemName: "xmark")
            .font(.headline)
            .padding(16)
            .foregroundColor(.primary)
            .background(.thickMaterial)
            .cornerRadius(10)
            .shadow(radius: 4)
            .padding()
    }
}
  • vm.sheetLocation = nil β€” qiymatni nil-ga o'rnatish sheet-ni avtomatik yopadi.
  • .thickMaterial β€” oq fonga o'xshash, ammo ortidagi kontentni biroz ko'rsatuvchi material.

Butun scroll view-ga ham biroz fon kerak:

.background(.ultraThinMaterial)

ViewModel-ga sheetLocation qo'shish

LocationsViewModel-ga location detail sheet-ini boshqaradigan o'zgaruvchi qo'shamiz:

// Show location detail via sheet
@Published var sheetLocation: Location? = nil
  • Location? β€” optional, chunki nil = sheet yopiq, qiymat bor = sheet ochiq.
  • Boshlang'ich qiymat β€” nil (ilova ochilganda sheet ko'rinmaydi).

LocationsView-da sheet qo'shish

LocationsView-dagi asosiy ZStack-ga .sheet modifikatorini qo'shamiz:

.sheet(item: $vm.sheetLocation) { location in
    LocationDetailView(location: location)
}
  • .sheet(item:) β€” item optional identifiable ob'ektga bog'lanadi. Qiymat nil bo'lmagan zahoti, sheet avtomatik ochiladi. Sheet yopilganda qiymat avtomatik nil bo'ladi.

LocationPreviewView-da "Learn More" tugmasi

LocationPreviewView-dagi "Learn more" tugmasida:

Button("Learn more") {
    vm.sheetLocation = location
}

Bu qiymatni o'rnatish β€” sheet-ni ochish uchun yetarli.


Xaritadagi MKCoordinateSpan

Location detail view-dagi xarita biroz ko'proq zoom qilingan bo'lishi kerak. Buning uchun LocationsViewModel-dagi mapSpan-ni yangilaymiz yoki alohida span yaratamiz:

// LocationDetailView ichida mapLayer-da:
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)

0.01 β€” kichik span, ya'ni xarita locationga yaqinroq zoom qilingan bo'ladi.


Natija

Endi ilova to'liq ishlaydi:

  1. Xaritada location-ni bosamiz β†’ LocationPreviewView paydo bo'ladi
  2. "Learn more" tugmasini bosamiz β†’ LocationDetailView sheet sifatida pastdan yuqoriga chiqadi
  3. Sheet ichida:
    • Rasmlar karuseli (swipe qilib o'tish mumkin)
    • Location nomi va shahri
    • Tavsif matni
    • Wikipedia havolasi
    • Kichik, interaktiv bo'lmagan xarita (bitta pin bilan)
  4. X tugmasi yoki pastga swipe qilish β†’ sheet yopiladi

Barcha ma'lumot LocationsViewModel orqali boshqariladi β€” yangi ekran ham xuddi shu @EnvironmentObject-dan foydalanadi, shuning uchun hamma narsa doim sinxron.

Buy mea coffee