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()vereorderContainer(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ı
- swipeActionsContainer() ile ScrollView'da swipe
- Grid ve stack'te reorder: List sınırının kalkışı
- .reorderable() ve .reorderContainer(for:) API'si
- Veri modelini ReorderDifference ile senkron tutmak
- Bölümlü (sectioned) listelerde sıralama
- watchOS'ta ilk kez sıralama
- Platform kapsamı: tvOS istisnası
- Karar tablosu: onMove mu reorderable mı
- SSS
- LazyVGrid'e swipe action nasıl eklenir?
- swipeActionsContainer nasıl kullanılır?
- SwiftUI'da List dışında sürükle-sırala nasıl yapılır?
- onMove yerine reorderable ne zaman kullanılır?
- Full-swipe ve birden fazla aksiyon
- reorderContainer(for:) tipini doğru eşleştirmek
- isEnabled ile sıralamayı düzenleme moduna bağlamak
- Sonuç
- Kaynaklar
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.samples3 4 var body: some View {5 ScrollView {6 LazyVStack(spacing: 8) {7 ForEach(messages) { message in8 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.samples3 let columns = [GridItem(.adaptive(minimum: 100))]4 5 var body: some View {6 ScrollView {7 LazyVGrid(columns: columns, spacing: 8) {8 ForEach(photos) { photo in9 PhotoThumbnail(photo: photo)10 }11 .reorderable()12 }13 .reorderContainer(for: Photo.self) { difference in14 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 in3 TaskRow(task: task)4 }5 .reorderable()6}7.reorderContainer(for: TaskItem.self, isEnabled: isEditing) { difference in8 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 in3 PhotoThumbnail(photo: photo)4 }5 .reorderable()6}7.reorderContainer(for: Photo.self) { difference in8 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 in4 Section(section.title) {5 ForEach(section.items) { item in6 TaskCard(item: item)7 }8 .reorderable(collectionID: section.id)9 }10 }11 }12 .reorderContainer(for: TaskItem.self, in: TaskSection.ID.self) { difference in13 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 in3 PhotoThumbnail(photo: photo)4 }5 .reorderable()6}7.reorderContainer(for: Photo.self) { difference in8 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.samples3 @State private var isEditing = false4 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 in14 TaskRow(task: task)15 }16 .reorderable()17 }18 .reorderContainer(for: TaskItem.self, isEnabled: isEditing) { difference in19 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
- WWDC26 SwiftUI Rehberi — Apple'ın resmi WWDC26 SwiftUI özet sayfası; swipeActionsContainer ve reorderable/reorderContainer duyurularının birebir alıntı kaynağı.
- [Apple Developer — swipeActions(edge:allowsFullSwipe:content:)](<https://developer.apple.com/documentation/swiftui/view/swipeactions(edge:allowsfullswipe:content:)>) — resmi API referansı, modifier'ın orijinal List-bağımlı tanımı.
- Apple Developer — Reordering items in lists, stacks, grids, and custom layouts — reorderable() ve reorderContainer(for:) için resmi Apple makalesi ve kod örnekleri.
- onmyway133 — What's new in SwiftUI in iOS 27 — platform kapsamı (watchOS ilk kez, tvOS yok) ve ReorderDifference detaylarının kaynağı.
- Swift with Majid — Swipe actions outside of List in SwiftUI — swipeActionsContainer'ın tek-seferde-tek-satır davranışı ve scroll/tap-dışı dismiss detayları.

