İlk hücreye iki sütunluk yer vermek, ardından kalan hücreleri art arda gelen satırlara doldurmak için özel bir SwiftUI Layout kullanın. Bu macOS örneği, pencerenin genişliği değiştiğinde sütunlarını yeniden hesaplar ve her satırın yüksekliğini içeriğine göre ölçer.
Görev, masaüstü için küçük bir palettir: önce bir tanıtım kutucuğu, ardından numaralandırılmış on bir düğme gelir. En az iki sütun sığdığında ilk kutucuk daha geniş olur. Dar genişliklerde içerik dikey olarak devam edebilsin diye tek sütunlu bir kutucuğa dönüşür. Örnekteki tüm etiketler ve seçimler sentetiktir.
Bu düzeni hangi SwiftUI kapsayıcısı destekler?
Apple’ın Grid bileşeni, GridRow içinde gridCellColumns(_:) kullanarak bir hücrenin birden fazla sütuna yayılmasını destekler. Ancak satırları kendiniz sağlarsınız. Grid içine, bir GridRow dışında doğrudan yerleştirilen görünüm tüm ızgaraya yayılır. Bu düzenlerin hiçbiri düz bir koleksiyonu genişliğe bağlı satırlara otomatik olarak yerleştirmez. Apple’ın Grid belgelerine ve gridCellColumns(_:) belgelerine bakın.
LazyVGrid otomatik satır doldurma sağlar: öğeleri sütunlara yerleştirir ve sonraki satırlara devam eder. Belgelenen sütun yapılandırmasında, tek bir hücrenin sütunlara yayılması için GridRow mekanizması bulunmaz. İlk öğeyi görsel olarak genişletmek, belgelenmiş bir iki sütunluk yerleşim sağlamaz. Apple’ın LazyVGrid belgelerine bakın.
Kapsayıcıyı ihtiyaç duyduğunuz davranışa göre seçin:
| Gereksinim | Uygun yaklaşım |
|---|---|
| İki sütuna yayılan hücre içeren açıkça belirlenmiş satırlar | Grid, GridRow ve gridCellColumns(2) |
| Sıradan hücrelerden oluşan, otomatik doldurulan satırlar | LazyVGrid |
| Otomatik doldurulan satırlar ve iki yuvaya yayılan ilk hücre | Bu yerleştirme kuralını uygulayan özel bir Layout |
Bu son durum için Apple’ın desteklenen genişletme noktası Layout protokolüdür. Bu protokolün iki temel yöntemi, kapsayıcının boyutunu bildirir ve alt görünümlerini yerleştirir. Aşağıdaki uygulama, belgelenen bu yöntemler üzerine kurulmuş özgün bir yerleştirme algoritmasıdır. Layout — Apple
Düzeni yazmadan önce boyutlandırma kurallarını belirleyin
Bu örnekte sıradan bir sütunun minimum genişliği 96 nokta, sütunlar arasındaki boşluk ise 8 noktadır. Sütunlar, kullanılabilir genişliği eşit olarak paylaşacak şekilde genişler. İlk alt görünüm, aradaki boşluk da dahil olmak üzere iki yuva kullanır; diğer tüm alt görünümler bir yuva kullanır. Alt görünümler mevcut sıralarını koruyarak satırları doldurur ve her satır, en uzun alt görünümü kadar yüksek olur.
Kullanılabilir genişlik W, minimum sütun genişliği m ve boşluk g için sütun sayısı max(1, floor((W + g) / (m + g))) olur. Gerçek sıradan sütun genişliği (W - (columns - 1) × g) / columns değeridir. Dolayısıyla iki yuvalı bir kutucuğun genişliği 2 × columnWidth + g nokta olur.
Örneğin, 408 noktalık bir içerik alanına, aralarında üç adet 8 noktalık boşluk bulunan dört adet 96 noktalık sütun sığar. Tanıtım kutucuğu 200 nokta genişliğindedir. Yanına iki sıradan düğme sığar; sonraki düğmeler her satırda dört yuvayı doldurur. Bunlar çalışan bir uygulamadan alınan ölçümler değil, seçilen sabitlerle yapılan açıklayıcı hesaplamalardır.
SwiftUI, bir düzenin esnekliğini incelerken belirtilmemiş, sıfır veya sonsuz boyutta önerilerde bulunabilir. Bu önerilere bilinçli yanıtlar verilmesi gerekir. Bu uygulama, ideal genişlik için en fazla dört sütun, minimum genişlik için bir sütun ve sorulduğunda tüm alt görünümlerin tek satıra sığması için gereken sayıda sütun kullanır. Her zaman sonlu bir genişlik döndürür. Apple, bu önerilerin anlamlarını ProposedViewSize belgelerinde açıklar.
Düzeni macOS uygulamasında uygulayın
Xcode’da bir SwiftUI macOS uygulaması oluşturun ve Layout kullanılabildiği için dağıtım hedefini macOS 13 veya üstüne ayarlayın. Aşağıdaki eksiksiz düzen türünü içeren WideFirstGrid.swift adlı bir Swift dosyası ekleyin. Layout — Apple
import SwiftUI
struct WideFirstGrid: Layout {
let minimumColumnWidth: CGFloat = 96
let gap: CGFloat = 8
struct Plan {
var size: CGSize
var frames: [CGRect]
}
func makePlan(width proposedWidth: CGFloat?, subviews: Subviews) -> Plan {
guard !subviews.isEmpty else {
return Plan(size: .zero, frames: [])
}
let totalSlots = subviews.count + (subviews.count > 1 ? 1 : 0)
let idealColumns = min(4, totalSlots)
let idealWidth = CGFloat(idealColumns) * minimumColumnWidth
+ CGFloat(idealColumns - 1) * gap
let maximumWidth = CGFloat(totalSlots) * minimumColumnWidth
+ CGFloat(totalSlots - 1) * gap
let width: CGFloat
if let proposedWidth {
if proposedWidth.isFinite {
width = max(minimumColumnWidth, proposedWidth)
} else {
width = maximumWidth
}
} else {
width = idealWidth
}
let columns = max(1, Int(floor(
(width + gap) / (minimumColumnWidth + gap)
)))
let columnWidth = (width - CGFloat(columns - 1) * gap)
/ CGFloat(columns)
var frames: [CGRect] = []
var column = 0
var y: CGFloat = 0
var rowHeight: CGFloat = 0
for (index, subview) in subviews.enumerated() {
let span = index == 0 ? min(2, columns) : 1
if column + span > columns {
y += rowHeight + gap
column = 0
rowHeight = 0
}
let cellWidth = CGFloat(span) * columnWidth
+ CGFloat(span - 1) * gap
let measured = subview.sizeThatFits(
ProposedViewSize(width: cellWidth, height: nil)
)
frames.append(CGRect(
x: CGFloat(column) * (columnWidth + gap),
y: y,
width: cellWidth,
height: measured.height
))
rowHeight = max(rowHeight, measured.height)
column += span
}
return Plan(
size: CGSize(width: width, height: y + rowHeight),
frames: frames
)
}
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) -> CGSize {
makePlan(width: proposal.width, subviews: subviews).size
}
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) {
let plan = makePlan(width: bounds.width, subviews: subviews)
for (subview, frame) in zip(subviews, plan.frames) {
subview.place(
at: CGPoint(
x: bounds.minX + frame.minX,
y: bounds.minY + frame.minY
),
anchor: .topLeading,
proposal: ProposedViewSize(width: frame.width, height: nil)
)
}
}
}
Aynı planlama işlevi hem ölçüm hem yerleştirme için kullanılır. Her alt görünüm, kendisine ayrılan genişlikte ve belirtilmemiş yükseklikte ölçülür, ardından aynı alt görünüm boyutu önerisiyle yerleştirilir. Böylece metin kaydırma ve satır yüksekliği hesaplamaları tutarlı olur. Apple, boyutlandırma ve yerleştirme sırasında uyumlu hesaplamalar yapılmasını özellikle şart koşar. placeSubviews(in:proposal:subviews:cache:) — Apple
Yerleştirme, hesaplanan her konuma bounds.minX ve bounds.minY değerlerini ekler. Üst kapsayıcının ayrılan sınırları sıfırdan başlamayabilir. Her alt görünüm için bir kez yerleştirme çağrısı yapılır ve bu çağrılar yalnızca LayoutSubview yerleştirme sözleşmesinin gerektirdiği üzere placeSubviews içinde gerçekleşir.
Sentetik kutucukları ve düğme eylemlerini ekleyin
Oluşturulan uygulamanın giriş dosyasını aşağıdaki kodla değiştirin ve projede tam olarak bir @main bildirimi bırakın. WideFirstGrid.swift dosyasıyla birlikte bu, örneğin tamamını oluşturur. Tanıtım metni ilk düzen alt görünümüdür; her yineleme bir düğme sağlar.
import SwiftUI
@main
struct WideFirstGridDemoApp: App {
var body: some Scene {
WindowGroup {
PaletteView()
.frame(minWidth: 240, minHeight: 260)
}
}
}
struct PaletteView: View {
@State private var selectedNumber: Int? = nil
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Tile palette")
.font(.title2)
Text(selectedNumber.map { "Selected tile: \($0)" }
?? "Choose a numbered tile")
ScrollView {
WideFirstGrid {
Text("Choose a tile from this sample collection")
.font(.headline)
.padding(12)
.frame(
maxWidth: .infinity,
minHeight: 64,
alignment: .leading
)
.background(
Color.blue.opacity(0.12),
in: RoundedRectangle(cornerRadius: 10)
)
ForEach(1...11, id: \.self) { number in
Button {
selectedNumber = number
} label: {
Text("Tile \(number)")
.padding(12)
.frame(maxWidth: .infinity, minHeight: 64)
.background(
Color.gray.opacity(0.12),
in: RoundedRectangle(cornerRadius: 10)
)
}
.buttonStyle(.plain)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding(16)
}
}
Sabit sayısal koleksiyon, id: \.self aracılığıyla her düğmeye benzersiz ve kararlı bir tanımlayıcı verir. Düzen, geometrisini alt görünüm proxy’leri üzerinden hesaplar; seçim ise görünümün durumunda kalır ve yalnızca düğme eyleminde değişir. Apple, ForEach yapısının tanımlanabilir öğeler veya açık bir tanımlayıcı gerektirdiğini belgeler. ForEach — Apple
ScrollView ek satırları barındırır. Düzen, satırları önerilen yüksekliğe sığdırmak yerine doğal içerik yüksekliğini hesaplar. Daha yüksek bir metin etiketi satırının yüksekliğini artırır ve sonraki satırlar buna göre aşağı kayar. Daha kısa alt görünümler o satırın üst kısmına hizalı kalır.
Yeniden boyutlandırmayı, dar genişlikleri ve düzen yaşam döngüsünü kontrol edin
macOS hedefini derleyip çalıştırın, ardından pencerenin genişliğini değiştirin. Aşağıdaki beklenen sonuçlar, çevresindeki dolgu çıkarıldıktan sonra ızgaranın kullanılabilir içerik genişliğine dayanır. Toplam on iki alt görünüm, yani tanıtım kutucuğu ve on bir düğme olduğunu varsayar.
| Izgara içerik genişliği | Sütunlar | İlk hücrenin genişliği | Toplam satır |
|---|---|---|---|
| 96 nokta | 1 | 96 nokta | 12 |
| 200 nokta | 2 | 200 nokta | 7 |
| 304 nokta | 3 | 200 nokta | 5 |
| 408 nokta | 4 | 200 nokta | 4 |
Pencerenin minimum genişliği nedeniyle 96 noktalık durumu test etmek için ayrı bir dar test kapsayıcısı veya geçici olarak azaltılmış bir minimum pencere genişliği gerekir. Ayrıca boş bir düzeni, yalnızca tanıtım kutucuğunu içeren bir düzeni ve daha uzun bir tanıtım etiketini test edin. Boş içerik sıfır boyut döndürür; yalnızca ilk kutucuğu içeren düzen, başka bir alt görünüm gerektirmeden kullanılabilir iki yuvayı da kaplayabilir.
Yerleştirme aritmetiği, yukarıdaki dört genişlikte belirleyici bir Python hesaplamasıyla kontrol edildi; buna çakışan yuvalar ve sütun taşması denetimleri de dahildi. Araştırma ortamında Swift derleyicisi bulunmadığı için SwiftUI uygulaması incelendi ancak derlenip çalıştırılmadı. Örneği uyarlamadan önce görsel davranışı Xcode’da doğrulayın.
SwiftUI, boyuta karar vermeden önce sizeThatFits yöntemini farklı önerilerle tekrar tekrar çağırabilir. Ölçüm sırasında seçimi değiştirmeyin, alt görünümleri yerleştirmeyin veya başka uygulama eylemleri gerçekleştirmeyin. Bu örnek varsayılan boş önbelleği kullanıp planı yeniden hesaplar; böylece önceki bir genişlik eski satır konumlarını bırakamaz. Apple bu tekrarlı ölçüm davranışını sizeThatFits(proposal:subviews:cache:) belgelerinde açıklar.
Genişletmeden önce sınırları anlayın
96 noktalık minimum, bilinçli bir düzen politikasıdır. Üst kapsayıcı daha az genişlik sunarsa düzen yine de 96 nokta bildirir ve bu ayrılan alanın dışına taşabilir. Sağlanan pencere minimumu bu durumu önler. Daha dar kapsayıcıları desteklemek için minimum sütun genişliğini düşürün ve etiketleri bu genişlikte kontrol edin.
Boyut önerisi bir istektir: alt görünüm farklı bir boyut seçebilir. Sağlanan kutucuklar kendilerine ayrılan genişliği kabul edecek şekilde tasarlanmıştır. Bunları daha büyük sabit bir genişlikte ısrar eden içerikle değiştirmek çakışmaya neden olabilir; bu tür alt görünümleri eklemeden önce boyutlandırma politikasını gözden geçirin. place(at:anchor:proposal:) — Apple
Bu uygulama ayrıca soldan sağa fiziksel yerleştirme kullanır ve her alt görünümü ölçer. Yön duyarlı sıralama veya tembel oluşturma uygulamaz. Çok daha büyük koleksiyonlarda farklı bir kapsayıcı seçmeden önce sonucu profilleyin. Apple, profilleme kayda değer bir performans iyileşmesi gösterdiğinde normal ızgaradan tembel ızgaraya geçilmesini önerir; bu değişiklikte hücrelerin birden fazla sütuna yayılması gereksinimi yeniden değerlendirilmelidir. Grid — Apple
Küçük bir Mac paleti için ortak planlama işlevinin davranışı uyarlaması kolaydır: sütun minimumunu veya boşluğu değiştirin, ölçüm ve yerleştirme tutarlılığını koruyun ve tek sütunlu geri dönüşü sürdürün. Böylece ilk kutucuk, yer olduğu sürece geniş kalırken kalan tüm düğmeler sonraki kullanılabilir yuvaya akar.
Kaynaklar
- Grid — Apple
- gridCellColumns(_:) — Apple
- LazyVGrid — Apple
- Layout — Apple
- ProposedViewSize — Apple
- sizeThatFits(proposal:subviews:cache:) — Apple
- placeSubviews(in:proposal:subviews:cache:) — Apple
- place(at:anchor:proposal:) — Apple
- ForEach — Apple