Tüm Yazılar
KategoriSwiftUI
Okuma Süresi
13 dk
Yayın Tarihi
2026-07-01
Kelime Sayısı
2.714kelime

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

iOS 27 SwiftUI: Her Yerde Swipe ve Sürükle-Sırala

Özet

iOS 27 SwiftUI swipe actions sürükle sırala: swipeActionsContainer() ile List dışında swipe, reorderable() ve reorderContainer(for:) ile LazyVGrid ve watchOS'ta sıralama nasıl kurulur.

  • swipeActionsContainer() artık herhangi bir ScrollView'da swipe aksiyonlarını etkinleştiriyor, List zorunluluğu kalktı.
  • .reorderable() (ForEach'e) + .reorderContainer(for:) (container'a) ile List, LazyVGrid, LazyVStack ve custom layout'larda aynı kodla sürükle-sırala geliyor.
  • watchOS'ta sıralama ilk kez destekleniyor; tvOS ise hem swipe hem sıralama için kapsam dışı.
  • Bölümlü (sectioned) sıralama için reorderable(collectionID:) + reorderContainer(for:in:) varyantı kullanılıyor.
iOS 27 SwiftUI: Her Yerde Swipe ve Sürükle-Sırala

iOS 27 ile SwiftUI'da iki eski kısıtlama birden kalkıyor: swipe aksiyonları artık List şartına bağlı değil, sürükle-sırala ise List'ten çıkıp LazyVGrid, LazyVStack ve custom layout'lara kadar genişliyor. WWDC26'nın SwiftUI rehberinde bu iki yetenek aynı "Presentation and interaction" bölümünde, bilinçli olarak yan yana duyuruldu. Bu yazıda swipeActionsContainer(), .reorderable() ve .reorderContainer(for:) API'lerini gerçek kod örnekleriyle, hangi container'da ne zaman çalıştığını ve veri modelini sıralamayla nasıl senkron tutacağını göreceksin.

💡 Pro Tip: swipeActionsContainer() ve reorderContainer(for:) iki ayrı modifier — birini uyguladığın için diğerinin de otomatik geleceğini düşünme; her ikisini de kendi container'ına ayrı ayrı eklemen gerekiyor.

İçindekiler

Eski yol: List zorunluluğu neden vardı

iOS 27 öncesinde swipeActions(edge:allowsFullSwipe:content:) modifier'ı yalnızca List satırlarında bir etki üretiyordu. Apple'ın resmi API referansındaki platform-uyumluluk tablosunda bu modifier iOS 15'ten beri var, ama davranışı hep List'in satır mekanizmasına bağlıydı — başka bir ScrollView içine aynı modifier'ı koyduğunda hiçbir şey olmuyordu.

Bu yüzden geliştiriciler ScrollView + LazyVStack ile özel liste görünümleri kurduklarında swipe davranışını DragGesture ve manuel offset hesabıyla elle yazıyordu — her uygulamada tekrar eden, kırılgan bir kod parçasıydı. onmyway133 da aynı kısıtlamayı doğruluyor: "Without swipeActionsContainer() on the container, the row level modifier still has no effect outside a List." Yani kısıtlama gerçekti ve iOS 27'ye kadar sürdü.

Aynı şey sıralama için de geçerliydi: List dışında sürükle-bırak sıralama istediğinde ya üçüncü parti kütüphanelere ya da düşük seviyeli DragGesture + koordinat matematiğine başvuruyordun. onMove(perform:) yalnızca List'in EditMode akışına bağlıydı, LazyVGrid gibi grid düzenlerinde derlenirdi ama hiçbir etki üretmiyordu.

swipeActionsContainer() ile ScrollView'da swipe

iOS 27 bu kısıtlamayı swipeActionsContainer() modifier'ıyla kaldırıyor. WWDC26 SwiftUI rehberinin kendi cümlesi net: "Add swipeActionsContainer to any ScrollView to enable them across your layout." Modifier'ı saran ScrollView'a bir kez uyguluyorsun, içindeki her satırdaki normal swipeActions() çağrıları da aktif oluyor.

Pratikte şöyle görünüyor:

swift
1struct InboxView: View {
2 @State private var messages: [Message] = Message.samples
3 
4 var body: some View {
5 ScrollView {
6 LazyVStack(spacing: 8) {
7 ForEach(messages) { message in
8 MessageRow(message: message)
9 .swipeActions(edge: .trailing, allowsFullSwipe: true) {
10 Button(role: .destructive) {
11 delete(message)
12 } label: {
13 Label("Sil", systemImage: "trash")
14 }
15 Button {
16 archive(message)
17 } label: {
18 Label("Arşivle", systemImage: "archivebox")
19 }
20 }
21 }
22 }
23 }
24 .swipeActionsContainer()
25 }
26}

Burada dikkat edilecek nokta: .swipeActionsContainer() en dıştaki ScrollView'a uygulanıyor, .swipeActions() ise her satıra ayrı ayrı. Container modifier'ı olmadan bu satır-seviyesi swipeActions() çağrıları List dışında sessizce hiçbir şey yapmaz — swiftwithmajid'in de doğruladığı gibi davranış aynı anda yalnızca bir satırın swipe menüsünü açık tutuyor; scroll ettiğinde veya container dışına dokunduğunda otomatik kapanıyor.

Grid ve stack'te reorder: List sınırının kalkışı

Sıralama tarafında değişiklik daha köklü. WWDC26 rehberi şöyle diyor: "New reorderable container APIs let people drag to rearrange items in any container — not just List — using the same code across List, LazyVGrid, and more." Yani aynı iki modifier'ı List, LazyVGrid, LazyVStack veya kendi yazdığın custom Layout protokolü üzerinde birebir aynı şekilde kullanabiliyorsun.

Bunun pratik anlamı: bir fotoğraf galerisini LazyVGrid ile gösteriyorsan, artık kullanıcıların hücreleri sürükleyip yeniden sıralaması için List'e geri dönüp kendi grid tasarımından vazgeçmen gerekmiyor. Aynı .reorderable() + .reorderContainer(for:) ikilisi grid hücrelerinde de çalışıyor.

swift
1struct PhotoGridView: View {
2 @State private var photos: [Photo] = Photo.samples
3 let columns = [GridItem(.adaptive(minimum: 100))]
4 
5 var body: some View {
6 ScrollView {
7 LazyVGrid(columns: columns, spacing: 8) {
8 ForEach(photos) { photo in
9 PhotoThumbnail(photo: photo)
10 }
11 .reorderable()
12 }
13 .reorderContainer(for: Photo.self) { difference in
14 move(difference: difference)
15 }
16 }
17 }
18}

LazyVGrid'i List ile değiştirsen bu iki modifier satırı aynı kalırdı — bu, Apple'ın "aynı kod her container'da" vaadinin somut karşılığı.

.reorderable() ve .reorderContainer(for:) API'si

İki modifier'ın işbölümü net: .reorderable(), hangi view'ların sürüklenebilir olduğunu işaretlemek için ForEach deklarasyonuna eklenir. .reorderContainer(for:isEnabled:move:) ise sıralamanın hangi alanda geçerli olacağını tanımlamak için kapsayan List, stack, grid veya custom layout container'ına eklenir.

Apple'ın resmi makalesindeki tanım şöyle: "indicate which views you want people to reorder by adding the reorderable() modifier to the ForEach declaration that generates those views" ve "define the area in your interface where people can reorder views by adding the reorderContainer(for:isEnabled:move:) modifier to the enclosing list, stack, grid, or custom layout container."

Apple'ın resmi makalesi bir ön koşul da koyuyor: veri öğenin identifier'ı Hashable ve Sendable'a uymalı (Identifiable uyumunu bu tipte bir identifier ile kurman gerekiyor). isEnabled parametresi ise sıralamayı çalışma zamanında açıp kapatmanı sağlıyor — örneğin bir "düzenleme modu" anahtarına bağlayabilirsin:

swift
1LazyVStack {
2 ForEach(tasks) { task in
3 TaskRow(task: task)
4 }
5 .reorderable()
6}
7.reorderContainer(for: TaskItem.self, isEnabled: isEditing) { difference in
8 move(difference: difference)
9}

isEnabled: false olduğunda sürükleme jestleri tamamen devre dışı kalıyor, view hiyerarşisini yeniden kurmana gerek kalmıyor.

Veri modelini ReorderDifference ile senkron tutmak

Sürükleme bittiğinde SwiftUI, reorderContainer'ın closure'ına bir ReorderDifference değeri geçiriyor. Bu değer taşınan öğe(ler)i (sources) ve hedefi (destination) tanımlıyor; hedef konum .before(id) (belirli bir öğeden önce) veya .end (listenin sonu) olabiliyor. Apple'ın kendi örneği bu closure'ı şöyle bağlıyor:

swift
1LazyVGrid(columns: columns) {
2 ForEach(photos) { photo in
3 PhotoThumbnail(photo: photo)
4 }
5 .reorderable()
6}
7.reorderContainer(for: Photo.self) { difference in
8 move(difference: difference)
9}

move(difference:) fonksiyonunun gövdesi sana ait — SwiftUI yalnızca taşınan öğeleri (sources) ve hedefi (destination) .before(id) / .end biçiminde sağlıyor, geri kalanı (diziden çıkar, hedef konuma ekle) standart Swift Array manipülasyonu. Yani asıl iş [Photo] dizisini bu iki bilgiye göre yeniden sıralamak; SwiftUI kendisi diziyi değiştirmiyor, sadece kullanıcının jestini bu iki parçaya (taşınan kimlik + hedef) çeviriyor. Bu closure çağrılmadan, yani move(difference:) içi boş bırakılırsa, sürükleme yalnızca görsel bir animasyon olarak kalır; diziyi güncellemediğin sürece görünüm veri modelinin sırasını yansıtmaya devam eder.

Bölümlü (sectioned) listelerde sıralama

Öğeler bölümlere ayrılmışsa (ör. bir Kanban panosunda "Yapılacak" / "Devam Ediyor" / "Bitti" sütunları), düz .reorderable() + .reorderContainer(for:) ikilisi yetmiyor — SwiftUI bu durum için bölüm kimliğini de taşıyan bir varyant sunuyor: .reorderable(collectionID:) (ForEach'e, hangi bölüme ait olduğunu belirtmek için) ve reorderContainer(for:in:) (container'a, bölümler arası taşımayı etkinleştirmek için).

swift
1ScrollView {
2 LazyVStack {
3 ForEach(sections) { section in
4 Section(section.title) {
5 ForEach(section.items) { item in
6 TaskCard(item: item)
7 }
8 .reorderable(collectionID: section.id)
9 }
10 }
11 }
12 .reorderContainer(for: TaskItem.self, in: TaskSection.ID.self) { difference in
13 move(difference: difference)
14 }
15}

Bu varyant sayesinde bir öğeyi yalnızca kendi bölümü içinde değil, bir bölümden diğerine de sürükleyebiliyorsun — Kanban panosu, checklist grupları gibi gerçek uygulama senaryolarında karşına çıkan tipik ihtiyaç bu.

watchOS'ta ilk kez sıralama

iOS 27'nin sıralama API'sindeki en dikkat çekici satır watchOS ile ilgili. WWDC26 rehberi bunu doğrudan söylüyor: "Reordering comes to watchOS for the first time." Bu, .reorderable() / .reorderContainer(for:) ikilisinin yalnızca "List dışında da çalışsın" diye değil, "daha önce hiç var olmayan bir platforma sıralama getirsin" diye tasarlandığını gösteriyor.

Pratikte bu, bir watchOS uygulamasında kullanıcının küçük ekranda bile bir liste veya grid'i parmağıyla sürükleyip yeniden sıralayabileceği anlamına geliyor — önceden watchOS'ta bu tür bir etkileşim için resmi bir SwiftUI API'si yoktu. Aynı reorderContainer(for:) çağrısı, List yerine watchOS'a özgü daha kompakt bir stack içinde de kullanılabiliyor; kod tarafında ekstra bir platform kontrolüne gerek kalmıyor, çünkü API zaten aynı.

'İlk kez' olan sıralama; swipe aksiyonları watchOS'ta List içinde zaten vardı.

Platform kapsamı: tvOS istisnası

Her iki özellik de her platformda gelmiyor. onmyway133'ün özetine göre sıralama "iOS, macOS, watchOS, and visionOS 27" üzerinde çalışıyor ve "unavailable on tvOS" — aynı kaynak swipeActionsContainer için de "remains unavailable on tvOS" diyor. Kısacası tvOS her iki yeni API'nin de dışında kalıyor; bu, tvOS'un uzaktan kumanda tabanlı etkileşim modeliyle (dokunma/sürükleme jestleri olmadan) tutarlı bir tasarım kararı.

Özellik
iOS 27
macOS 27
watchOS 27
visionOS 27
tvOS 27
swipeActionsContainer() (herhangi ScrollView'da swipe)
Var
Var
Var
Var
Yok
.reorderable() / .reorderContainer(for:) (List-dışı sıralama)
Var
Var
Var (ilk kez)
Var
Yok

Tabloyu okurken dikkat: watchOS satırındaki "swipe" zaten List içinde önceden de vardı (iOS 15 / watchOS 8 kuşağından beri); yeni olan kısım watchOS'ta genel sıralama desteğinin gelmesi, swipe'ın kendisi değil.

Karar tablosu: onMove mu reorderable mı

onMove(perform:) hâlâ SwiftUI'da duruyor ve kayboldu değil. Ama artık iki farklı sürükle-sırala yolun var, hangisini seçeceğini bilmek önemli:

Durum
Önerilen API
Neden
Basit bir List içinde, EditMode ile birlikte sıralama
onMove(perform:)
Zaten List'in düzenleme akışına gömülü, ek kod gerektirmez
LazyVGrid, LazyVStack veya custom Layout içinde sıralama
.reorderable() + .reorderContainer(for:)
onMove bu container'larda derlenir ama etki üretmez; List'e özel
watchOS'ta sıralama desteği gerekiyor
.reorderable() + .reorderContainer(for:)
WWDC26 rehberine göre watchOS'ta sıralama ilk kez bu API ile geliyor
Bölümler arası (Kanban tipi) taşıma
.reorderable(collectionID:) + reorderContainer(for:in:)
Bölüm kimliğini taşıyan tek varyant bu

Kısacası: List içinde kalıyorsan ve EditMode akışını değiştirmek istemiyorsan onMove hâlâ en az kod yazdıran seçenek. Ama List dışına çıktığın an, artık tek seçenek yeni API — eski yöntemin bu container'larda karşılığı yoktu.

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ü

Bu yazıdaki dört API'yi (swipeActionsContainer, reorderable, reorderContainer, reorderContainer(for:in:)) doğru sırayla uygulamak için aşağıdaki kontrol listesini kullanabilirsin. Her adımı işaretlemeden bir sonrakine geçme; sıralama önemli çünkü container modifier'ı olmadan satır modifier'ları sessizce hiçbir şey yapmıyor.

SSS

LazyVGrid'e swipe action nasıl eklenir?

LazyVGrid'in kendisi swipeActions'ı desteklemiyor; List-dışı davranış WWDC26'da eklendi. Grid'i saran ScrollView'a swipeActionsContainer() uyguluyorsun, ardından her grid hücresine normal swipeActions(edge:allowsFullSwipe:content:) modifier'ını ekliyorsun. Container modifier'ı olmadan hücre seviyesindeki swipeActions() hiçbir etki yapmaz — onmyway133'ün ifadesiyle, "Without swipeActionsContainer() on the container, the row level modifier still has no effect outside a List."

swipeActionsContainer nasıl kullanılır?

swipeActionsContainer() bir ScrollView (veya onu saran üst view) üzerine uygulanır ve içindeki tüm alt view'lerin swipeActions() çağrılarını aktif hale getirir. WWDC26 rehberinin kendi ifadesiyle: "Add swipeActionsContainer to any ScrollView to enable them across your layout." Aynı anda yalnız bir satırın swipe menüsü açık kalır; scroll edildiğinde veya container dışına dokunulduğunda otomatik kapanır.

SwiftUI'da List dışında sürükle-sırala nasıl yapılır?

ForEach'e .reorderable(), onu saran container'a (LazyVStack, LazyVGrid, HStack/VStack veya custom layout) .reorderContainer(for:) ekliyorsun. Sürükleme bittiğinde SwiftUI bir ReorderDifference üretir; bu değer taşınan öğeyi ve hedef konumu (.before(id) veya .end) tanımlar, sen de bu bilgiyi move(difference:) fonksiyonunda kullanarak asıl diziyi güncellersin. Bölümlü içerik için reorderable(collectionID:) + reorderContainer(for:in:) varyantı var. WWDC26 rehberi bunun aynı kodla "List, LazyVGrid, and more" üzerinde çalıştığını söylüyor.

onMove yerine reorderable ne zaman kullanılır?

onMove yalnızca List içinde, EditMode bağlamında çalışır. reorderable()/reorderContainer(for:) ise List dışında (grid, stack, custom layout) sürükle-bırak sıralama gerektiğinde ve watchOS'ta sıralama desteği gerektiğinde tercih edilir — WWDC26 rehberine göre watchOS'ta sıralama ilk kez bu API ile geliyor. List içinde kalıp EditMode akışını değiştirmeden basit taşıma yeterliyse onMove hâlâ geçerli, daha az kod gerektiren bir seçenek.

Full-swipe ve birden fazla aksiyon

swipeActionsContainer() sadece "aç/kapat" mekaniğini List dışına taşımıyor, allowsFullSwipe davranışını da beraberinde getiriyor. allowsFullSwipe: true verdiğinde kullanıcı satırı sona kadar kaydırırsa ilk aksiyon (genelde .destructive rol taşıyan) otomatik tetikleniyor — bu, List içinde zaten bildiğin davranışın birebir aynısı, sadece artık ScrollView + LazyVStack üzerinde de geçerli:

swift
1MessageRow(message: message)
2 .swipeActions(edge: .trailing, allowsFullSwipe: true) {
3 Button(role: .destructive) {
4 delete(message)
5 } label: {
6 Label("Sil", systemImage: "trash")
7 }
8 }
9 .swipeActions(edge: .leading) {
10 Button {
11 markAsRead(message)
12 } label: {
13 Label("Okundu İşaretle", systemImage: "envelope.open")
14 }
15 .tint(.blue)
16 }

Dikkat edilmesi gereken nokta: edge: .leading ve edge: .trailing için ayrı swipeActions çağrıları yazman gerekiyor — tek bir çağrıda her iki kenarı da tanımlayamazsın. Birden fazla buton olsa da tam kaydırma ilk aksiyonu tetikler; bu davranışı bir kenar için kapatmak istersen allowsFullSwipe: false verirsin.

reorderContainer(for:) tipini doğru eşleştirmek

reorderContainer(for:) çağrısındaki for: parametresi, ForEach'in üzerinde çalıştığı dizinin eleman tipiyle birebir eşleşmeli — Apple'ın örneğinde bu Photo.self, çünkü ForEach(photos) bir [Photo] dizisi üzerinde çalışıyor:

swift
1LazyVGrid(columns: columns) {
2 ForEach(photos) { photo in
3 PhotoThumbnail(photo: photo)
4 }
5 .reorderable()
6}
7.reorderContainer(for: Photo.self) { difference in
8 move(difference: difference)
9}

Bu, Swift'in generic tip çıkarımının doğal bir sonucu: reorderContainer(for:) hangi koleksiyon üzerinde çalışacağını bu parametreden anlıyor. Kendi projende birden fazla reorderContainer kullanıyorsan (örneğin hem fotoğraf hem etiket listesi sıralanabiliyorsa), her reorderContainer(for:) çağrısının kendi ForEach'inin eleman tipiyle eşleştiğinden emin ol.

Bölümlü (sectioned) sıralamada bu eşleşme bir adım daha karmaşık: reorderContainer(for:in:) hem öğe tipini hem bölüm kimliği (collectionID) TİPİNİ (ör. TaskSection.ID.self) alıyor, reorderable(collectionID:) ile ForEach'e verdiğin bölüm kimliği tipiyle de tutarlı olmalı.

isEnabled ile sıralamayı düzenleme moduna bağlamak

reorderContainer(for:isEnabled:move:) imzasındaki isEnabled parametresi, sıralamayı sürekli açık tutmak yerine belirli bir moda bağlamanı sağlıyor. Bunun tipik kullanımı, ekranın üstünde bir "Düzenle" butonuyla açılıp kapanan bir durumdur:

swift
1struct TaskListView: View {
2 @State private var tasks: [TaskItem] = TaskItem.samples
3 @State private var isEditing = false
4 
5 var body: some View {
6 VStack {
7 Toggle("Düzenle", isOn: $isEditing)
8 .toggleStyle(.button)
9 .padding(.horizontal)
10 
11 ScrollView {
12 LazyVStack {
13 ForEach(tasks) { task in
14 TaskRow(task: task)
15 }
16 .reorderable()
17 }
18 .reorderContainer(for: TaskItem.self, isEnabled: isEditing) { difference in
19 move(difference: difference)
20 }
21 }
22 }
23 }
24}

isEnabled: false olduğunda reorderable() işaretli view'lar üzerindeki sürükleme jestleri devre dışı kalıyor, ama view hiyerarşisini yeniden kurman gerekmiyor — tek bir @State değişkenini değiştirmen yeterli. Bu, kullanıcının yanlışlıkla listeyi karıştırmasını önlemek için (örneğin salt-okunur bir görünümde) pratik bir korunak.

swipeActionsContainer() için eşdeğer bir isEnabled parametresi yok; swipe aksiyonlarını koşullu kapatmak için swipeActions() çağrısını bir if bloğuyla sarmalarsın.

Sonuç

iOS 27, SwiftUI'da iki ayrı ama birbirini tamamlayan kısıtlamayı aynı anda kaldırıyor: swipe aksiyonları artık swipeActionsContainer() ile herhangi bir ScrollView'da çalışıyor, sürükle-sırala ise .reorderable() + .reorderContainer(for:) ile List'ten çıkıp LazyVGrid, LazyVStack ve watchOS'a kadar genişliyor. İkisi de bağımsız sistemler; birini kullanmak diğerini otomatik getirmiyor, ama aynı ekranda birlikte de kullanılabiliyorlar.

Bu API'leri kullanırken @State ile veri modelini yönetme şeklin değişmiyor — bunun detaylarını iOS 27'de @State makrosunun ContentBuilder ile kırılan yerlerini inceleyen yazımızda bulabilirsin. Sürükle-sırala sonrası veri senkronizasyonunu SwiftData ile yapıyorsan SwiftData'nın enum predicate ve sectioned query desteğine bakmak faydalı olur. Uygulamanı Siri/App Intents ile de kontrol edilebilir hale getirmek istiyorsan App Intents'in uzun süreli intent desteğini okuyabilirsin.

Custom layout üzerine kendi bileşen kütüphaneni kuruyorsan SwiftUI özel bileşen kütüphanesi ve özel Layout protokolü rehberi bu yazıdaki reorderContainer'ın custom layout'larla nasıl birleştiğini anlamana yardımcı olur.

Kaynaklar

Etiketler

#SwiftUI#iOS 27#swipeActions#reorderable#LazyVGrid#watchOS#drag and drop
Muhittin Çamdalı

Muhittin Çamdalı

Lead Mobile Engineer

12+ yıllık deneyime sahip Lead Mobile Engineer. Swift, SwiftUI, Kotlin ve Flutter ile iOS, Android ve cross-platform mimarilerde uzman. Performanslı ve kullanıcı dostu mobil 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ş

İlgili İçerik