Geliştiriciler
iOS 27'de SwiftUI List Sıralama: reorderable() ve Swipe Actions
iOS 27'de reorderable() ve reorderContainer ile SwiftUI list, stack veya grid'leri sürükleyerek sıralayın; List dışında swipe actions kullanın; iPhone ve iPhone Duo için.
AppleDuo EditörlüğüYayınlanma 6 dk okuma
Bu sayfada
Yıllarca "Kullanıcıların bir SwiftUI list'ini yeniden sıralamasına nasıl izin veririm?" sorusunun cevabı aynıydı: bir List içindeki ForEach'e .onMove ekleyin, bir EditButton koyun ve index offset'lerini modelinize geri çevirin. Bu yalnızca List'te çalışıyordu ve edit mode ile birlikte geliyordu. Bu rehber, iOS 27'deki SwiftUI list sıralama API'sini anlatıyor; bu API her container'da çalışıyor ve ikisine de ihtiyaç duymuyor.
iOS 27, List'e özgü ikinci bir sınırı da kaldırıyor: swipe actions. İki değişiklik de WWDC26'dan geliyor ve Apple'ın SwiftUI referansında belgelenmiş durumda. Bu yazı o dokümantasyona, Apple'ın Code-along: Build powerful drag and drop in SwiftUI oturumuna ve iOS geliştiricisi @sarunw'nun paylaşımlarına dayanıyor. Bu API'ler iOS 27.0 ile geldi; ayrıntılar sürümler arasında değişebilir.
iOS 27'de bir SwiftUI list'i nasıl sıralanır?
İki modifier'a ihtiyacınız var. @sarunw'nun özetlediği gibi iOS 27, ForEach'e kendi sürükleyerek sıralama özelliğini veriyor: onu .reorderable() ile işaretleyin ve List'e .reorderContainer ekleyin. "No .onMove. No edit mode." (Ne .onMove ne edit mode.)
reorderable(), view'ları kullanıcılar tarafından taşınabilenForEach'e (herhangi birDynamicViewContent'e) eklenir.reorderContainer(for:isEnabled:move:)container'a eklenir ve sürüklemenin kapsamını belirler. Öğe türünüzü ve değişikliği alan birmoveclosure'ı verirsiniz.
struct TaskListView: View {
@State private var tasks: [TaskItem] = TaskItem.samples
var body: some View {
List {
ForEach(tasks) { task in
TaskRow(task: task)
}
.reorderable()
}
.reorderContainer(for: TaskItem.self) { difference in
tasks.apply(difference)
}
}
}
TaskItem, Identifiable'dır ve Apple'ın rehberi Hashable ve Sendable olan bir ID türü istiyor. apply(_:) çağrısı bizim kendi yardımcımız, bir sonraki bölümde gösteriliyor; SwiftUI diziler için böyle bir yöntem belgelemiyor.
Apple etkileşimin kendisini de tarif ediyor: kullanıcı bir view'u sürükleme hareketiyle kaldırır, sistem bırakılabilecek yere bir placeholder koyar, placeholder sürüklemeyi takip eder ve bırakıldığında sistem move'u çağırır. Hareket kodu veya animasyon yazmazsınız.
Container'ın bir List olması gerekmiyor. Apple'ın dokümantasyonu onun bir list, stack, grid veya özel bir layout olabileceğini söylüyor. Apple'ın kendi örneği .reorderContainer'ı düz bir VStack'e uyguluyor; WWDC26 oturumu ise oyun kartlarından oluşan bir HStack kullanıyor.
Sıralama closure'ı verilerimi nasıl günceller?
move closure'ı bir ReorderDifference alır. Bu değer değişikliği index'e göre değil, kimliğe göre tarif eder:
| Özellik | Tür | Anlamı |
|---|---|---|
sources | [ItemID] | Taşınan öğelerin ID'leri, kullanıcının seçtiği sırayla |
destination.position | .before(_:) veya .end | O ID'ye sahip öğenin önüne ekle veya sona ekle |
destination.collectionID | Kendi koleksiyon ID türünüz | Öğeleri hangi bölümün aldığı (çok koleksiyonlu container'lar) |
ID'lerle çalışmanın pratik bir faydası var. Apple'ın rehberi öğeleri "dizide bulut senkronizasyonu gibi başka değişiklikler aynı anda gerçekleşse bile" taşıyabileceğinizi belirtiyor, çünkü bir ID dizi değiştikten sonra da doğru öğeyi gösterir. .onMove'dan gelen bir IndexSet bunu yapmaz.
Tek koleksiyonlu bir difference'ı bir diziye uygulamanın bir yolu şöyle. Taşınan öğeleri çıkarır ve seçim sırasıyla hedefe yeniden ekler:
@available(iOS 27.0, *)
extension Array where Element: Identifiable, Element.ID: Sendable {
mutating func apply(
_ difference: ReorderDifference<Element.ID, ReorderableSingleCollectionIdentifier>
) {
let moved = difference.sources.compactMap { id in
first { $0.id == id }
}
removeAll { difference.sources.contains($0.id) }
switch difference.destination.position {
case .before(let id):
let index = firstIndex { $0.id == id } ?? endIndex
insert(contentsOf: moved, at: index)
case .end:
append(contentsOf: moved)
}
}
}
Generic tür, closure'ı ReorderDifference<Item.ID, ReorderableSingleCollectionIdentifier> alan tek koleksiyonlu overload ile eşleşiyor. Bu yardımcıyı bir başlangıç noktası olarak görün ve kendi modelinizle test edin.
Bölümler arasında sıralama
Bölümlere ayrılmış veride her ForEach'e reorderable(collectionID:) ile bir tanımlayıcı verin ve bölümleri tanımlayan türle birlikte reorderContainer(for:in:isEnabled:move:) kullanın. Kullanıcılar böylece öğeleri bölümlerin içinde ve arasında taşıyabilir; destination.collectionID de hangi bölümün onları aldığını söyler.
List {
ForEach(model.sections) { section in
Section(section.name) {
ForEach(section.reminders) { reminder in
ReminderView(reminder)
}
.reorderable(collectionID: section.id)
}
}
}
.reorderContainer(for: Reminder.self, in: ReminderModel.Section.ID.self) { difference in
model.apply(difference: difference)
}
Bu, Apple'ın dokümantasyon örneğini yansıtıyor; model.apply(difference:) sizin modelinizin kodu. ForEach'iniz id dışında bir ID kullanıyorsa itemID: overload'ları \.databaseID gibi bir key path alır.
reorderable(), onMove ve EditButton'dan nasıl farklı?
Eski kalıp hâlâ çalışıyor. Geçiş yapmadan önce tam olarak neyin değiştiğini bilmekte fayda var:
.onMove + EditButton | .reorderable() + .reorderContainer | |
|---|---|---|
| Geldiği sürüm | iOS 13 | iOS 27.0 |
| Container'lar | List | List, stack, grid veya özel layout |
| Nasıl başlar | Genellikle edit mode'da gösterilen sürükleme tutamaçları | Sistemin sürükleme hareketi |
| Closure girdisi | (IndexSet, Int) offset'leri | Öğe ID'leri içeren ReorderDifference |
| Bölümler arasında taşıma | Her ForEach'in kendi onMove'u | reorderable(collectionID:), tek closure |
| Birden çok öğe | Dokümantasyonda tarif edilmiyor | Kaynaklar seçim sırasıyla listelenir |
| Kapatma | onMove'a nil verin | isEnabled: false |
| Diğer uygulamalardan sürükleyip bırakma | Ayrı onInsert veya dropDestination | dropDestination ve reorderDestination ile eşleştirin |
Hacking with Swift'in uzun süredir yayında olan rehberi klasik kurulumu gösteriyor: ForEach'te onMove(perform:) ve sürükleme tutamaçlarını göstermek için toolbar'da bir EditButton. Apple'ın onMove dokümantasyonu closure argümanlarını alttaki koleksiyondaki offset'ler olarak tanımlıyor; eski kodun genellikle move(fromOffsets:toOffset:) ile bitmesinin nedeni bu.
isEnabled parametresi gerçek uygulamalarda işe yarar. Apple'ın rehberi senkronizasyon sırasında sıralamayı kapatmayı öneriyor, örneğin isEnabled: !model.isSaving.
Oturum, Solitaire örneğinden bir tasarım ipucu da ekliyor: bazı öğeleri sabit tutmak için onları .reorderable() olmayan ikinci bir ForEach'e ayırın. Yalnızca işaretlediğiniz içerik kaldırılabilir. Apple'ın dokümantasyonu aynı ForEach'te .onMove ile .reorderable()'ı birleştirmeyi tarif etmiyor; bu yüzden birini seçin.
iOS 27'de swipe actions'ı List dışında kullanabilir miyim?
Evet. @sarunw bunu basitçe ifade ediyor: "Swipe actions used to need a List row. In iOS 27 any view can have them." (Swipe actions eskiden bir List satırı gerektiriyordu; iOS 27'de her view'da olabiliyor.) Bunu mümkün kılan parça swipeActionsContainer().
Her satıra .swipeActions'ı yine tam olarak bir List'teki gibi yazarsınız. Ardından satırları tutan ScrollView'a, LazyVStack'e veya başka bir container'a .swipeActionsContainer() uygularsınız:
ScrollView {
LazyVStack {
ForEach(items) { item in
ItemRow(item)
.swipeActions {
Button("Delete", role: .destructive) {
delete(item)
}
}
}
}
}
.swipeActionsContainer()
Apple'ın dokümantasyonu container'ın neleri koordine ettiğini listeliyor:
- Aynı anda yalnızca bir satırın swipe actions'ı açılır.
- Container'ı kaydırmak açık eylemleri kapatır.
- Etkin satırın dışına dokunmak onun eylemlerini kapatır.
List bunların hepsini zaten yapıyor; bu yüzden Apple modifier'ı bir List'e uygulamanın etkisiz (no-op) olduğunu söylüyor. Majid Jabrayilov'un yazısı da aynı kurala varıyor: List dışındaki her şey container modifier'ına ihtiyaç duyar.
iOS 27 ayrıca swipeActions(edge:allowsFullSwipe:content:onPresentationChanged:)'ı ekliyor. Closure'ı bir satırın eylemleri göründüğünde true, kapatıldığında false alır. Apple bunu, eylemler gösterilirken satırı soluklaştırmak veya çevredeki arayüzü güncellemek için kullanmayı öneriyor.
Kontrol edilmesi gereken bilinen sorunlar
Apple'ın forumlarındaki iki geliştirici raporunu kendi build'inizde test etmekte fayda var. Biri, sağdan sola yerleşimlerde eylemlerin aynalanmış kenarda göründüğünü ama hareketin hâlâ soldan sağa yönü beklediğini söylüyor. Diğeri, List dışında swipe actions içeriğinin state değiştiğinde yeniden değerlendirilmediğini, bu yüzden bir Pin başlığının satır yeniden oluşturulana kadar Unpin'e dönmeyebileceğini söylüyor. Bu yazı yazılırken iki başlıkta da Apple'dan bir yanıt yoktu.
Bu API'ler eski iOS sürümlerinde çalışır mı?
Hayır. reorderable(), tüm reorderContainer overload'ları, swipeActionsContainer() ve onPresentationChanged overload'ı iOS 27.0, iPadOS 27.0, Mac Catalyst 27.0, macOS 27.0, visionOS 27.0 ve watchOS 27.0'dan itibaren kullanılabilir olarak belgelenmiş. WWDC26 oturumu sıralama API'lerini "sürükle bırakı destekleyen tüm Apple platformlarında yeni kullanılabilir" olarak tanımlıyor.
iOS 26 veya daha eskisini destekliyorsanız #available ile dallanın ve eski yolu koruyun:
@ViewBuilder
private var taskList: some View {
if #available(iOS 27.0, *) {
List {
ForEach(tasks) { TaskRow(task: $0) }
.reorderable()
}
.reorderContainer(for: TaskItem.self) { tasks.apply($0) }
} else {
List {
ForEach(tasks) { TaskRow(task: $0) }
.onMove { tasks.move(fromOffsets: $0, toOffset: $1) }
}
.toolbar { EditButton() }
}
}
Eski sürümlerde List dışındaki swipe actions'ın bir yedeği yok; Nil Coalescing, iOS 27'den önce bunların bir LazyVStack veya grid'deki satırlarda hiçbir etkisi olmadığını belirtiyor. Oralarda bu satırları bir List'te tutun veya aynı eylemleri bir context menu'de sunun.
Bunun iPhone Duo'ya faydası ne?
iPhone Duo'da iç ekran bir iPad gibi regular yatay size class bildirirken dış ekran compact kalır (iPhone Duo size class'ları rehberimize bakın). Birçok uygulama bu ekstra genişliğe tek sütunlu bir List yerine bir grid veya özel bir layout ile karşılık veriyor.
iOS 27'ye kadar bu tercih size özelliklere mal oluyordu. Kartlardan oluşan bir LazyVGrid'in yerleşik sıralaması ve swipe actions'ı yoktu; bu yüzden iç ekranda grid'e geçmek ikisini de elle yeniden yazmak demekti. Artık bir size class geçişi dış ekranda bir List, iç ekranda bir grid gösterse de aynı .reorderable() ve .swipeActions kodu çalışıyor:
let columns = [GridItem(.adaptive(minimum: 220))]
ScrollView {
LazyVGrid(columns: columns) {
ForEach(boards) { board in
BoardCard(board: board)
}
.reorderable()
}
}
.reorderContainer(for: Board.self) { boards.apply($0) }
Apple'ın rehberi sıralamanın yalnızca container içindeki taşımaları kapsadığını belirtiyor. Öğeleri uygulamanızın başka bölümlerine sürüklemek veya diğer uygulamalardan içeri almak, dragContainer(for:in:_:) ve dropDestination(for:isEnabled:action:) ile yapılan ayrı bir adım. İç ekran ArrangementView ile iki paneli yan yana gösteriyorsa bölmenin kendisini ArrangementView rehberimiz anlatıyor.
Önemli çıkarımlar
- iOS 27'de bir
ForEach'te.reorderable()ile container'ında.reorderContainer(for:), edit mode ve.onMoveolmadan sürükleyerek sıralama sağlar. - Container bir
List, stack, grid veya özel layout olabilir. moveclosure'ı birReorderDifferencealır: taşınan ID'lersources'ta, hedef ise.before(_:)veya.endolan birdestination'da. Bunu dizinize kendiniz uygulayın.- Öğeleri bölümler arasında taşımak için
reorderable(collectionID:)vereorderContainer(for:in:)kullanın. - Swipe actions artık
Listdışında çalışıyor;ScrollView'a veya layout'a.swipeActionsContainer()ekleyin. - Buradaki her şey iOS 27.0 ve sonrası için; bu yüzden
#availableile koruyun ve yedek olarak.onMove'u tutun.
List'ler özenli bir arayüzün yalnızca yarısı; satırların içindeki metin de önemli. Sırada iOS 26'da SwiftUI satır yüksekliği rehberimiz var. iPhone Duo'dan en iyi şekilde yararlanan uygulama ve oyunlar için Built for the fold sayfasına göz atın.
Sık sorulan sorular
iOS 27'de bir SwiftUI List'i edit mode olmadan nasıl sıralarım?
List içindeki ForEach'e .reorderable(), List'in kendisine de öğe türünüzü vererek .reorderContainer(for:) uygulayın. Kullanıcılar satırları doğrudan sürükleyebilir ve move closure verinize uygulamanız için bir ReorderDifference verir. EditButton veya onMove gerekmez.
reorderable() LazyVStack ve LazyVGrid'de çalışır mı?
Evet. Apple'ın dokümantasyonuna göre sıralanabilir bir container bir list, stack, grid veya özel bir layout olabilir. .reorderContainer'ı o container'a, .reorderable()'ı da içindeki ForEach'e ekleyin.
SwiftUI'da ReorderDifference nedir?
Bir sürüklemenin sonunda move closure'ın aldığı değerdir. sources özelliği taşınan öğelerin ID'lerini kullanıcının seçtiği sırayla listeler; destination ise nereye gideceklerini söyler: belirli bir öğenin önüne veya bir koleksiyonun sonuna.
iOS 27'de swipe actions'ı List dışında kullanabilir miyim?
Evet. Her satıra her zamanki gibi .swipeActions ekleyin ve saran ScrollView'a veya layout'a .swipeActionsContainer() uygulayın. Container aynı anda yalnızca bir satırın açık olmasını, kaydırmanın veya başka bir yere dokunmanın onu kapatmasını sağlar.
reorderable() ve swipeActionsContainer() iOS 26'ya geriye dönük çalışır mı?
Hayır. İkisi de iOS 27.0'dan itibaren kullanılabilir olarak belgelenmiştir; bu yüzden daha eski sürümleri destekleyen uygulamalar bir if #available kontrolüne ve List içinde onMove gibi bir yedek yola ihtiyaç duyar.
Kaynaklar
- reorderable() · Apple Developer Documentation
- reorderContainer(for:isEnabled:move:) · Apple Developer Documentation
- reorderContainer(for:in:isEnabled:move:) · Apple Developer Documentation
- Reordering items in lists, stacks, grids, and custom layouts · Apple Developer Documentation
- ReorderDifference · Apple Developer Documentation
- onMove(perform:) · Apple Developer Documentation
- swipeActionsContainer() · Apple Developer Documentation
- swipeActions(edge:allowsFullSwipe:content:onPresentationChanged:) · Apple Developer Documentation
- Code-along: Build powerful drag and drop in SwiftUI (WWDC26) · Apple Developer
- swipeActionsContainer() does not mirror the swipe gesture in right-to-left layouts · Apple Developer Forums
- iOS 27: swipeActions outside List never re-evaluates its content closure · Apple Developer Forums
- iOS 27 gives ForEach its own drag to reorder: .reorderable() and .reorderContainer · @sarunw on X
- No .onMove. No edit mode. · @sarunw on X
- Swipe actions used to need a List row; in iOS 27 any view can have them · @sarunw on X
- New SwiftUI APIs for reordering and drag and drop on iOS 27 · Nil Coalescing
- Custom scroll layouts with swipe actions in SwiftUI on iOS 27 · Nil Coalescing
- Swipe actions outside of List in SwiftUI · Swift with Majid
- How to let users move rows in a list · Hacking with Swift
Katlanmayı uygulamanızın bir parçası yapın.
iPhone Duo ile gerçekten bir şey yapan uygulama ve oyunları tanıtıyoruz. App Store bağlantısıyla sizinkini gönderin ya da diğer geliştiricilerin neler yaptığına bakın.