Ana içeriğe geç
  1. 100 Günde SwiftUI Notları/

41.Gün - SwiftUI: Moonshot Uygulamasını Bitirelim

Bugün container relative frame ve ScrollView ile çalışarak MoonShot uygulamasını bitireceğiz.

ScrollView ve containerRelativeFrame() ile Mission Ayrıntılarını Gösterme #

Kullanıcı ana listemizden Apollo mission’larından birini seçtiğinde, mission hakkında bilgi göstermek istiyoruz: misson rozeti, mission açıklaması ve mürettebatta bulunan tüm astronotları rolleriyle birlikte. Bunlardan ilk ikisi çok zor değil, ancak üçüncüsü biraz daha fazla çalışma gerektiriyor çünkü mürettebat kimliklerini iki JSON dosyamızdaki mürettebat ayrıntıları ile eşleştirmemiz gerekiyor.

Basit bir şekilde başlayalım; MissionView.swift adında yeni bir SwiftUI view oluşturalım. Başlangıçta bu sadece bir mission property’ye sahip olacak, böylece mission rozetini ve açıklamasını gösterebileceğiz, ancak kısa süre sonra buna daha fazlasını ekleyeceğiz.

Layout ‘un mission rozeti için yeniden boyutlandırılabilir bir görüntü ve ardından bir text view içeren Scrolling bir VStack’e sahip olması gerekir. Mission View’in genişliğini ayarlamak için containerRelativeFrame() kullanacağız ve %50 - %70 arasında bir genişlik ayarlayacağız.

Aşağıdaki kodu MissionView.swift’e ekleyelim;

struct MissionView: View {
    let mission: Mission

    var body: some View {
        ScrollView {
            VStack {
                Image(mission.image)
                    .resizable()
                    .scaledToFit()
                    .containerRelativeFrame(.horizontal) { width, axis in
                        width * 0.6
                    }
                    .padding(.top)

                VStack(alignment: .leading) {
                    Text("Mission Highlights")
                        .font(.title.bold())
                        .padding(.bottom, 5)

                    Text(mission.description)
                }
                .padding(.horizontal)
            }
            .padding(.bottom)
        }
        .navigationTitle(mission.displayName)
        .navigationBarTitleDisplayMode(.inline)
        .background(.darkBackground)
    }
}

Bir VSTack’i başka bir VSTack’in içine yerleştirmek, view’ın belirli bir kısmı için hizalamayı kontrol etmemizi sağlar. Mission görseli ortalanabilirken, mission ayrıntıları ön kenera hizalanabilir.

Yeni oluşturduğumuz MissionView preview nedeniyle oluşturulmayacaktır. Bundle extension’ımız burada da çalıştığı için bu sorunu rahatlıkla halledebiliriz.

#Preview {
    let missions: [Mission] = Bundle.main.decode("missions.json")

    return MissionView(mission: missions[0])
        .preferredColorScheme(.dark)
}

İpucu: ContentView’deki NavigationStack’e koyu renk düzeni eklendiğinden, MissionView’da buna sahip olacaktır, ancak MissionView Preview bunu bilemez, bu yüzden elle etkinleştirmemiz gerekir.

Önizlemeye bakarsanız bunun iyi bir başlangıç olduğunu göreceksiniz, ancak bir sonraki kısım daha zor: göreve katılan astronotların listesini açıklamanın altında göstermek istiyoruz. Şimdi bunun üstesinden gelelim.

Codable Struct’ları Birleştirme #

Görev açıklamasının altında her bir mürettebat üyesinin resimlerini, isimlerini ve rollerini göstermek istiyoruz, bu da iki farklı JSONdosyasından gelen verileri eşleştirmek anlamına geliyor.

Hatırlarsanız, JSON verilerimiz missions.json ve astronauts.json dosyalarına bölünmüştü. Bu, bazı astronotlar birden fazla görevde yer aldığı için verilerimizdeki yinelemeyi ortadan kaldırır, ancak aynı zamanda verilerimizi birleştirmek için bazı kodlar yazmamız gerektiği anlamına gelir.

Yapmamız gereken şey, MissionView’ımızın astronaut dictionary ile birlikte, bir önceki ekranda dokunulan görevi kabul etmesini sağlamak ve ardından hangi astronotların fırlatmaya katıldığını bulmasını sağlamaktır.

Bu nested struct’ı şimdi MissionView içine ekleyelim.

struct CrewMember {
    let role: String
    let astronaut: Astronaut
}

MissionView’e CrewMember nesnelerinin array’ini saklayan bir property eklememiz gerekiyor.

let crew: [CrewMember]

Bu view’e görevi ve tüm astronotları verirsek, görev mürettebatı üzerinde döngü yapabilir, ardından her mürettebat üyesi için dictionary’de eşleşen kimliğe sahip olanı bulabiliriz. Birini bulduğumuzda onu ve rolünü bir CrewMember nesnesine dönüştürebiliriz, ancak bulamazsak bir şekilde geçersiz veya bilinmeyen bir isme sahip bir mürettebat rolümüz olduğu anlamına gelir.

Bu ikinci durum asla gerçekleşmemelidir. Eğer yanlış JSON verisinden kaynaklı olarak bu durum gerçekleşirse, fatalError() ile uygulamayı sonlandırıp bu durumu bildirmeliyiz.

Tüm bunları MissionView için custom Initializer kullanarak koda dökelim.

init(mission: Mission, astronauts: [String: Astronaut]) {
    self.mission = mission

    self.crew = mission.crew.map { member in
        if let astronaut = astronauts[member.name] {
            return CrewMember(role: member.role, astronaut: astronaut)
        } else {
            fatalError("Missing \(member.name)")
        }
    }
}

Bu kodu yazdıktan sonra, preview kodu daha fazla bilgiye ihtiyaç duyacağından çalışmayı durduracaktır. Bu yüzden decode() methoduna ikinci bir çağrı ekleyelim.

#Preview {
    let missions: [Mission] = Bundle.main.decode("missions.json")
    let astronauts: [String: Astronaut] = Bundle.main.decode("astronauts.json")

    return MissionView(mission: missions[0], astronauts: astronauts)
        .preferredColorScheme(.dark)
}

Artık tüm astronot verilerimize sahip olduğumuza göre, bunları horizontal scroll view kullanarak doğrudan görev açıklamasının altında gösterebiliriz. Ayrıca astronot resimlerinin daha iyi görünmesini sağlamak için bir capsule clip shape ve overlay kullnarak resimlere daha fazla stil ekleyeceğiz.

Bu kodu VStack(alignment: .leading) ‘ten hemen sonra ekleyin.

ScrollView(.horizontal, showsIndicators: false) {
    HStack {
        ForEach(crew, id: \.role) { crewMember in
            NavigationLink {
                Text("Astronaut details")
            } label: {
                HStack {
                    Image(crewMember.astronaut.id)
                        .resizable()
                        .frame(width: 104, height: 72)
                        .clipShape(.capsule)
                        .overlay(
                            Capsule()
                                .strokeBorder(.white, lineWidth: 1)
                        )

                    VStack(alignment: .leading) {
                        Text(crewMember.astronaut.name)
                            .foregroundStyle(.white)
                            .font(.headline)
                        Text(crewMember.role)
                            .foregroundStyle(.white.opacity(0.5))
                    }
                }
                .padding(.horizontal)
            }
        }
    }
}

ScrollView neden VStack’in içinde değil de sonrasında? Çünkü scroll view’ler mevcut ekran alanından tam olarak yararlandıklarında en iyi şekilde çalışırlar, bu da kenardan kenara kaydırılmaları gerektiği anlamına gelir. Bunu VStack’imizin içine koysaydık, metnimizin geri kalanıyla aynı padding’e sahip olurdu, bu da garip bir şekilde kaydırılacağı anlamına gelirdi.

ContentView’de içindeki NavigationLink içindeki Text("Detail View") kodunu aşağıdaki ile değiştirin.

MissionView(mission: mission, astronauts: astronauts)

Uygulamayı çalıştırdığınızda kullanışlı olmaya başladığını göreceksiniz.

SwiftUI layout’da görsel bir ayrım oluşturmak için özel bir Divider view sağlar, ancak özelleştirilebilir değildir, her zaman sadece ince bir çizgidir. Bu sebeple, biraz daha kullanışlı bir şey elde etmek için, özel bir Divider çizeceğiz.

İlk olarak bunu “Mission Highlights” metninden hemen önce yerleştirin;

Rectangle()
    .frame(height: 2)
    .foregroundStyle(.lightBackground)
    .padding(.vertical)

Aynı kodu doğrudan mission.description‘dan sonra yerleştirin.

Bu view’ı tamamlamak için ekibimizden önce bir başlık ekleyeceğim, ancak bunun dikkatli bir şekilde yapılması gerekiyor. Gördüğünüz gibi, bu scroll view ile ilgili olsa da, metnimizin geri kalanıyla aynı padding’e sahip olması gerekiyor. Dolayısıyla, bunun için en iyi yer VStack’in içinde, önceki Rectangle()‘dan hemen sonradır;

Text("Crew")
    .font(.title.bold())
    .padding(.bottom, 5)

Son Dokunuş #

Bu uygulamayı bitirmek için astronot ayrıntılarını görüntülemek üzere üçüncü ve son bir view oluşturacağız ve view’e görev görünümündeki astronotlardan birine dokunarak ulaşacağız.

AsronautView adında yeni bir SwiftUI view oluşturarak başlayalım. Bunun tek bir Astronaut property’si olacak, böylece neyi göstereceğini bilecek, ardından MissionView’de olduğu gibi benzer bir ScrollView / VStack kombinasyonu kullanarak bunu düzenleyeceğiz.

struct AstronautView: View {
    let astronaut: Astronaut

    var body: some View {
        ScrollView {
            VStack {
                Image(astronaut.id)
                    .resizable()
                    .scaledToFit()

                Text(astronaut.description)
                    .padding()
            }
        }
        .background(.darkBackground)
        .navigationTitle(astronaut.name)
        .navigationBarTitleDisplayMode(.inline)
    }
}

Bir kez daha önizlemeyi güncellememiz gerekir, böylece view’ı veriler ile oluşturabilir.

#Preview {
    let astronauts: [String: Astronaut] = Bundle.main.decode("astronauts.json")

    return AstronautView(astronaut: astronauts["aldrin"]!)
        .preferredColorScheme(.dark)
}

Şimdi bunu MissionView içindeki NavigationLink’ten sunabiliriz. Bu şu anda Text(”Astronaut details”) öğesini işaret ediyor, ancak bunun yerine yeni AstronautView öğemize işaret edecek şekilde güncelleyebiliriz.

AstronautView(astronaut: crewMember.astronaut)

Finished MoonShot App


Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.

Bu yazı, SwiftUI Day 41 adresinde bulunan yazılardan kendim için aldığım notları içermektedir. Orjinal dersi takip etmek için lütfen bağlantıya tıklayın.