Tüm Yazılar
KategoriSwiftUI
Okuma Süresi
20 dk okuma
Yayın Tarihi
...
Kelime Sayısı
1.463kelime

Kahveni hazırla - bu içerikli bir makale!

Layout protocol ile custom container layout olusturma. FlowLayout, MasonryLayout, circular layout ve responsive tasarim teknikleri.

SwiftUI Layout Protocol: Custom Container ve Adaptive Layout

iOS 16 ile gelen Layout protocol, SwiftUI'da tamamen custom container layout'lar olusturmayi mumkun kildi. HStack ve VStack yetmedigi noktalarda FlowLayout, MasonryLayout veya circular layout gibi ozel yerlesimler yazabilirsiniz. Bu rehberde Layout protocol'u derinlemesine inceleyecegiz.


Icindekiler


1. Layout Protocol Nedir?

Layout protocol, iki temel metod gerektirir:

Metod
Amac
Ne Zaman Cagrilir
`sizeThatFits`
Container'in boyutunu hesapla
Parent boyut sordugunda
`placeSubviews`
Child view'leri konumla
Layout uygulanirken

2. Temel Yapisi

swift
1import SwiftUI
2 
3struct SimpleHStack: Layout {
4 var spacing: CGFloat = 8
5 
6 func sizeThatFits(
7 proposal: ProposedViewSize,
8 subviews: Subviews,
9 cache: inout ()
10 ) -> CGSize {
11 let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
12 let totalWidth = sizes.reduce(0) { $0 + $1.width }
13 + CGFloat(max(0, subviews.count - 1)) * spacing
14 let maxHeight = sizes.map(\.height).max() ?? 0
15 return CGSize(width: totalWidth, height: maxHeight)
16 }
17 
18 func placeSubviews(
19 in bounds: CGRect,
20 proposal: ProposedViewSize,
21 subviews: Subviews,
22 cache: inout ()
23 ) {
24 var x = bounds.minX
25 for subview in subviews {
26 let size = subview.sizeThatFits(.unspecified)
27 subview.place(
28 at: CGPoint(x: x, y: bounds.midY),
29 anchor: .leading,
30 proposal: ProposedViewSize(size)
31 )
32 x += size.width + spacing
33 }
34 }
35}
36 
37// Kullanim
38struct ContentView: View {
39 var body: some View {
40 SimpleHStack(spacing: 12) {
41 ForEach(0..<5) { i in
42 Text("Item \(i)")
43 .padding(8)
44 .background(.blue.opacity(0.2))
45 .cornerRadius(8)
46 }
47 }
48 }
49}

3. FlowLayout Ornegi

Tag cloud veya chip listesi icin ideal:

swift
1struct FlowLayout: Layout {
2 var spacing: CGFloat = 8
3 var lineSpacing: CGFloat = 8
4 
5 func sizeThatFits(
6 proposal: ProposedViewSize,
7 subviews: Subviews,
8 cache: inout ()
9 ) -> CGSize {
10 let containerWidth = proposal.width ?? .infinity
11 var currentX: CGFloat = 0
12 var currentY: CGFloat = 0
13 var lineHeight: CGFloat = 0
14 var maxWidth: CGFloat = 0
15 
16 for subview in subviews {
17 let size = subview.sizeThatFits(.unspecified)
18 
19 if currentX + size.width > containerWidth && currentX > 0 {
20 currentX = 0
21 currentY += lineHeight + lineSpacing
22 lineHeight = 0
23 }
24 
25 currentX += size.width + spacing
26 lineHeight = max(lineHeight, size.height)
27 maxWidth = max(maxWidth, currentX - spacing)
28 }
29 
30 return CGSize(width: maxWidth, height: currentY + lineHeight)
31 }
32 
33 func placeSubviews(
34 in bounds: CGRect,
35 proposal: ProposedViewSize,
36 subviews: Subviews,
37 cache: inout ()
38 ) {
39 var currentX = bounds.minX
40 var currentY = bounds.minY
41 var lineHeight: CGFloat = 0
42 
43 for subview in subviews {
44 let size = subview.sizeThatFits(.unspecified)
45 
46 if currentX + size.width > bounds.maxX && currentX > bounds.minX {
47 currentX = bounds.minX
48 currentY += lineHeight + lineSpacing
49 lineHeight = 0
50 }
51 
52 subview.place(
53 at: CGPoint(x: currentX, y: currentY),
54 anchor: .topLeading,
55 proposal: ProposedViewSize(size)
56 )
57 
58 currentX += size.width + spacing
59 lineHeight = max(lineHeight, size.height)
60 }
61 }
62}
63 
64// Tag Cloud kullanimi
65struct TagCloudView: View {
66 let tags = ["Swift", "SwiftUI", "iOS", "Xcode", "UIKit",
67 "Combine", "Async/Await", "Core Data", "Metal",
68 "ARKit", "HealthKit", "CloudKit"]
69 
70 var body: some View {
71 FlowLayout(spacing: 8, lineSpacing: 8) {
72 ForEach(tags, id: \.self) { tag in
73 Text(tag)
74 .font(.caption)
75 .padding(.horizontal, 12)
76 .padding(.vertical, 6)
77 .background(.blue.opacity(0.1))
78 .foregroundColor(.blue)
79 .cornerRadius(16)
80 }
81 }
82 .padding()
83 }
84}

4. Circular Layout

swift
1struct CircularLayout: Layout {
2 var radius: CGFloat = 100
3 var startAngle: Angle = .degrees(0)
4 
5 func sizeThatFits(
6 proposal: ProposedViewSize,
7 subviews: Subviews,
8 cache: inout ()
9 ) -> CGSize {
10 let diameter = radius * 2 + 50
11 return CGSize(width: diameter, height: diameter)
12 }
13 
14 func placeSubviews(
15 in bounds: CGRect,
16 proposal: ProposedViewSize,
17 subviews: Subviews,
18 cache: inout ()
19 ) {
20 let center = CGPoint(x: bounds.midX, y: bounds.midY)
21 let angleStep = 2 * .pi / Double(subviews.count)
22 
23 for (index, subview) in subviews.enumerated() {
24 let angle = startAngle.radians + angleStep * Double(index)
25 let x = center.x + radius * cos(angle)
26 let y = center.y + radius * sin(angle)
27 
28 subview.place(
29 at: CGPoint(x: x, y: y),
30 anchor: .center,
31 proposal: .unspecified
32 )
33 }
34 }
35}

5. Animated Layout Transitions

swift
1struct LayoutTransitionDemo: View {
2 @State private var isCircular = false
3 
4 var body: some View {
5 let layout = isCircular ?
6 AnyLayout(CircularLayout(radius: 120)) :
7 AnyLayout(FlowLayout())
8 
9 layout {
10 ForEach(0..<8) { index in
11 Circle()
12 .fill(Color(hue: Double(index) / 8, saturation: 0.8, brightness: 0.9))
13 .frame(width: 44, height: 44)
14 .overlay(Text("\(index + 1)").foregroundColor(.white))
15 }
16 }
17 .animation(.spring(duration: 0.6), value: isCircular)
18 
19 Button(isCircular ? "Flow Layout" : "Circular Layout") {
20 isCircular.toggle()
21 }
22 }
23}

6. ViewThatFits ile Adaptive Layout

swift
1struct AdaptiveStack<Content: View>: View {
2 @ViewBuilder var content: Content
3 
4 var body: some View {
5 ViewThatFits {
6 HStack { content }
7 VStack { content }
8 }
9 }
10}
11 
12// Kullanim — yatay sigmazsa dikeye gecer
13struct ButtonBar: View {
14 var body: some View {
15 AdaptiveStack {
16 Button("Kaydet") { }
17 .buttonStyle(.borderedProminent)
18 Button("Iptal") { }
19 .buttonStyle(.bordered)
20 Button("Sil") { }
21 .buttonStyle(.bordered)
22 .tint(.red)
23 }
24 }
25}

7. AnyLayout ile Dynamic Switching

swift
1struct DynamicLayoutDemo: View {
2 @Environment(\.horizontalSizeClass) var sizeClass
3 
4 var layout: AnyLayout {
5 sizeClass == .compact ?
6 AnyLayout(VStackLayout(spacing: 12)) :
7 AnyLayout(HStackLayout(spacing: 20))
8 }
9 
10 var body: some View {
11 layout {
12 InfoCard(title: "Kullanicilar", value: "1.2M")
13 InfoCard(title: "Indirme", value: "5.6M")
14 InfoCard(title: "Gelir", value: "$2.3M")
15 }
16 .animation(.easeInOut, value: sizeClass)
17 }
18}

8. Performans Ipuclari

Teknik
Aciklama
**Cache kullanin**
makeCache/updateCache ile hesaplamalari onbellekleyin
**Layout priority**
Onemli view'lere oncelik verin
**Spacing**
subviews.spacing ile sistem spacing'ini kullanin
**ProposedViewSize**
.zero, .unspecified, .infinity farklarini bilin
swift
1struct CachedFlowLayout: Layout {
2 struct CacheData {
3 var sizes: [CGSize] = []
4 }
5 
6 func makeCache(subviews: Subviews) -> CacheData {
7 CacheData(sizes: subviews.map { $0.sizeThatFits(.unspecified) })
8 }
9 
10 func sizeThatFits(
11 proposal: ProposedViewSize,
12 subviews: Subviews,
13 cache: inout CacheData
14 ) -> CGSize {
15 // cache.sizes kullanarak hesapla — her seferinde yeniden olcme yapma
16 let totalHeight = cache.sizes.reduce(0) { $0 + $1.height }
17 let maxWidth = cache.sizes.map(\.width).max() ?? 0
18 return CGSize(width: maxWidth, height: totalHeight)
19 }
20 
21 func placeSubviews(
22 in bounds: CGRect,
23 proposal: ProposedViewSize,
24 subviews: Subviews,
25 cache: inout CacheData
26 ) {
27 // cache.sizes kullan
28 }
29}

9. Best Practices

  1. Once ViewThatFits deneyin — basit adaptive durumlar icin yeterli
  2. AnyLayout ile gecis yapin — animasyonlu layout degisimi icin
  3. Cache kullanin — tekrarlanan hesaplamalari onbelleege alin
  4. Spacing API'sini kullanin — subviews.spacing ile tutarli aralıklar
  5. Accessibility'i unutmayin — dynamic type ile test edin

ALTIN İPUCU

Bu yazının en değerli bilgisi

Bu ipucu, yazının en önemli çıkarımını içeriyor.

Easter Egg

Gizli bir bilgi buldun!

Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?

Okuyucu Ödülü

Tebrikler! Bu yaziyi sonuna kadar okudugun icin sana ozel bir hediyem var:

10. Sonuc

Layout protocol, SwiftUI'da custom yerlesim mantigi yazmanin en guclu yoludur. FlowLayout, masonry grid ve circular layout gibi yapilar ile standart HStack/VStack'in otesine gecebilirsiniz. ViewThatFits ve AnyLayout ile adaptive tasarimlar olusturarak her ekran boyutuna uyum saglayabilirsiniz.


Sonuc ve Oneriler

Custom Layout protocol, SwiftUI'nin yerlesim sistemini tamamen ozellestirmenize olanak tanir. Apple'in kendi HStack ve VStack implementasyonlarinin da ayni protocol uzerine insa edildigi dusunuldugunde, bu API'nin ne kadar guclu oldugu anlasilir. FlowLayout ile etiket sistemleri, masonry grid ile Pinterest tarzinda gorunum ve circular layout ile dashboard benzeri arayuzler olusturabilirsiniz.

Layout gelistirirken sizePlaceChildren dongu mantigini iyi kavramak kritik oneme sahiptir. Her child view'in ideal, minimum ve maximum boyutlarini sorgulayarak esnek ve responsive yerlesimler olusturun. LayoutValueKey ile child view'lardan ozel degerler alarak daha sofistike yerlesim algoritmalari implement edebilirsiniz.

Performans acsindan Layout protocol'u SwiftUI'nin render pipeline'ina dogal olarak entegre oldugu icin ek maliyet minimamaldir. Ancak cok sayida child view iceren layout'larda cache mekanizmasi kullanmayi ve gereksiz yeniden hesaplamalari onlemeyi ihmal etmeyin. ViewThatFits ve AnyLayout ile farkli ekran boyutlari icin otomatik layout gecisleri yaparak kullanici deneyimini en ust seviyeye tasiyabilirsiniz.

Etiketler

#SwiftUI#Layout#Custom Layout#UI Design#Responsive#iOS
Muhittin Çamdalı

Muhittin Çamdalı

Senior iOS Developer

12+ yıllık deneyime sahip iOS Developer. Swift, SwiftUI ve modern iOS mimarileri konusunda uzman. Apple platformlarında performanslı ve kullanıcı dostu uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.

Paylaş

Bunu da begenebilirsiniz