Untuk memindahkan grup beserta item anaknya bersama-sama, simpan anak-anak di dalam model induk dan susun ulang koleksi induk. Panduan ini membuat daftar SwiftUI Mac sederhana tempat setiap grup berpindah sebagai satu baris, dengan anak-anaknya tetap melekat.
Tugasnya adalah menyusun ulang grup dalam aplikasi Anda sendiri: misalnya, memindahkan Group A, yang berisi Tile A1 dan Tile A2, ke bawah Group B. Contoh ini menggunakan data lokal sintetis dan menyediakan tombol Move Up dan Move Down secara eksplisit bersama callback pemindahan daftar SwiftUI.
Apa yang seharusnya dianggap sebagai satu item yang dapat dipindahkan?
Sesuaikan unit pemindahan dengan unit dalam data Anda. Untuk antarmuka ini, larik terluar berisi grup, dan setiap grup memiliki larik anaknya sendiri. Menyusun ulang larik terluar mengubah urutan grup sekaligus mempertahankan anak-anak setiap grup beserta urutannya saat ini.
Representasikan setiap grup sebagai satu baris daftar komposit: judul, tombol pemindahan, dan label anak yang menjorok ke dalam di sebuah VStack. Pasang onMove(perform:) pada ForEach terluar. Apple menjelaskan bahwa offset callback merujuk ke koleksi dasar dari tampilan dinamis tersebut; di sini, koleksi itu adalah larik grup. Dokumentasi callback pemindahan Apple.
Berikan pengenal stabil kepada grup dan anak-anaknya. ForEach milik Apple memerlukan elemen yang memiliki pengenal, baik melalui Identifiable maupun jalur kunci pengenal yang eksplisit. Dalam desain ini, pengenal melekat pada model dan tetap sama ketika posisinya berubah. Hindari menggunakan posisi larik sebagai identitas konten yang dapat disusun ulang. Dokumentasi ForEach Apple.
Tampilan SwiftUI Group bukanlah model aplikasi yang digunakan di bawah ini. Menamai sebuah wadah atau menampilkan judul tidak menentukan catatan anak mana yang seharusnya ikut berpindah dengannya. Larik children yang disimpan pada induk memperjelas hubungan tersebut.
Buat contoh SwiftUI yang lengkap
Buat proyek aplikasi macOS di Xcode dengan antarmuka SwiftUI dan target deployment macOS 12 atau lebih baru. Ganti berkas titik masuk aplikasi yang dibuat otomatis dengan program lengkap di bawah ini, dan pastikan hanya ada satu deklarasi aplikasi @main dalam target. Contoh ini sengaja menggunakan daftar sederhana dan tombol teks agar pemindahannya mudah diperiksa.
import Foundation
import SwiftUI
private struct DemoItem: Identifiable {
let id: String
let title: String
}
private struct DemoGroup: Identifiable {
let id: String
let title: String
let children: [DemoItem]
}
@main
struct GroupMoveDemoApp: App {
var body: some Scene {
WindowGroup {
GroupCollectionView()
}
}
}
private struct GroupCollectionView: View {
@State private var groups: [DemoGroup] = [
DemoGroup(
id: "group-a", title: "Group A",
children: [
DemoItem(id: "a1", title: "Tile A1"),
DemoItem(id: "a2", title: "Tile A2")
]
),
DemoGroup(
id: "group-b", title: "Group B",
children: [
DemoItem(id: "b1", title: "Tile B1")
]
),
DemoGroup(
id: "group-c", title: "Group C",
children: []
)
]
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Arrange Groups")
.font(.title2)
.padding(.horizontal)
List {
ForEach(groups) { group in
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(group.title).font(.headline)
Spacer()
Button("Move Up") {
moveOne(id: group.id, down: false)
}
.disabled(group.id == groups.first?.id)
Button("Move Down") {
moveOne(id: group.id, down: true)
}
.disabled(group.id == groups.last?.id)
}
.buttonStyle(BorderlessButtonStyle())
ForEach(group.children) { child in
Text(child.title)
.padding(.leading, 16)
}
}
.padding(.vertical, 8)
}
.onMove(perform: moveGroups)
}
}
.padding(.vertical)
.frame(minWidth: 520, minHeight: 320)
}
private func moveGroups(from source: IndexSet, to destination: Int) {
withAnimation {
groups.move(fromOffsets: source, toOffset: destination)
}
}
private func moveOne(id: String, down: Bool) {
guard let index = groups.firstIndex(where: { $0.id == id }) else {
return
}
let neighbor = down ? index + 1 : index - 1
guard groups.indices.contains(neighbor) else {
return
}
let destination = down ? index + 2 : index - 1
moveGroups(from: IndexSet(integer: index), to: destination)
}
}
ForEach terluar menghasilkan satu VStack untuk setiap grup. ForEach bersarang menampilkan anak-anak dalam baris tersebut; tidak ada penangan pemindahan anak yang terpisah. Kedua tombol pemindahan mencari posisi grup saat ini berdasarkan ID dan memanggil mutasi yang sama dengan onMove.
Jalankan aplikasi dan klik Move Down di samping Group A. Urutan yang diharapkan menjadi Group B, Group A, Group C, dengan Tile A1 dan Tile A2 tetap ditampilkan di bawah Group A. Klik Move Up di samping grup tersebut untuk mengembalikan urutan awal. Group C juga menunjukkan bahwa grup tanpa anak tetap dapat dipindahkan.
Program ini menyimpan urutannya dalam memori selama tampilan saat ini aktif; program ini tidak memiliki lapisan persistensi. Referensi dan logika pemindahannya telah diperiksa, tetapi program ini tidak dikompilasi di sini karena shell yang tersedia tidak memiliki compiler Swift. Build dan periksa antarmukanya di Xcode pada Mac sebelum mengintegrasikannya.
Mengapa pemindahan ke bawah menggunakan tujuan yang berbeda?
Tujuan yang diberikan ke move(fromOffsets:toOffset:) adalah batas penyisipan dalam koleksi asli. Apple mendefinisikannya sebagai offset sebelum elemen yang dipindahkan disisipkan, dalam rentang 0...count. Memberikan offset elemen itu sendiri atau offset berikutnya tidak akan mengubah apa pun. Dokumentasi pemindahan koleksi Apple.
Untuk urutan awal ilustratif [A, B, C], operasi-operasi terpisah berikut menunjukkan perbedaannya:
| Hasil yang diinginkan | Offset sumber | Tujuan yang diberikan ke move |
Hasil |
|---|---|---|---|
Memindahkan A ke bawah B |
0 |
2 |
[B, A, C] |
Memindahkan B ke atas A |
1 |
0 |
[B, A, C] |
Memindahkan A ke akhir |
0 |
3 |
[B, C, A] |
Membiarkan A tetap di tempat |
0 |
1 |
[A, B, C] |
Itulah sebabnya tombol turun menggunakan index + 2: elemen disisipkan sebelum elemen setelah tetangga berikutnya, atau di akhir. Tombol naik menggunakan index - 1. Untuk offset yang diberikan langsung oleh onMove, teruskan offset tersebut ke metode koleksi tanpa menerapkan lagi perhitungan khusus tombol ini.
Perhitungan tujuan dalam contoh ini berlaku untuk satu pemindahan berdekatan. Untuk callback pemindahan yang memuat beberapa offset sumber, gunakan IndexSet lengkap bersama metode koleksi, yang mendokumentasikan bahwa urutan elemen yang dipindahkan tetap dipertahankan. Dokumentasi pemindahan koleksi Apple.
Antarmuka mana yang memerlukan implementasi berbeda?
Pilih implementasi berdasarkan tampilan grup dan cara pengguna perlu berinteraksi dengan anak-anaknya. Contoh baris komposit cocok ketika grup harus selalu berpindah sebagai satu blok. Contoh ini sengaja menampilkan anak sebagai label di dalam blok tersebut, bukan sebagai baris daftar tersendiri.
| Persyaratan antarmuka | Titik awal yang sesuai | Tanggung jawab pemindahan |
|---|---|---|
| Judul grup dan anak-anaknya membentuk satu blok yang dapat dipindahkan | ForEach terluar dengan satu baris komposit untuk setiap grup |
Susun ulang larik induk |
| Hierarki yang dapat diperluas dengan turunan bersarang | OutlineGroup |
Implementasikan pemindahan model untuk koleksi induk yang dimaksud |
| Koleksi AppKit dengan satu bagian untuk setiap grup | NSCollectionView |
Perbarui sumber data, lalu beri tahu tampilan koleksi tentang pemindahan bagian |
| Teks yang dapat diedit secara khusus di dalam sebuah item | TextKit di dalam editor item | Pertahankan penyusunan ulang grup di model koleksi sekelilingnya |
Pohon yang dapat diperluas memerlukan aturan pemindahan yang eksplisit
Apple menjelaskan OutlineGroup sebagai tampilan data terstruktur pohon yang memiliki pengenal, dengan kontrol pengungkapan untuk memperluas dan menciutkan cabang. Fitur ini tersedia di macOS 11 dan versi lebih baru. Perilaku presentasi yang didokumentasikan tidak menetapkan kebijakan umum untuk menyusun ulang subpohon dalam aplikasi Anda. Dokumentasi OutlineGroup Apple.
Untuk pohon, tentukan tujuan dengan tepat: memindahkan induk di antara saudara-saudaranya dan memindahkannya ke dalam induk lain adalah operasi yang berbeda. Penyusunan ulang antar-saudara mengubah satu larik induk. Pemindahan induk memerlukan penghapusan simpul dari induk lama, penyisipan ke induk baru, serta penolakan tujuan yang berada di dalam turunan simpul itu sendiri. Ini adalah aturan model aplikasi yang diturunkan dari struktur pohon.
Jika tugasnya tetap memindahkan seluruh grup tingkat atas, implementasi baris komposit menghindari kebijakan tujuan tambahan tersebut. Jika anak-anak memerlukan pemilihan atau penyusunan ulang secara mandiri, rancang interaksi tersebut secara eksplisit sebelum meratakan hierarki menjadi baris yang terlihat.
AppKit mendukung pemindahan bagian beserta item di dalamnya
Jika antarmuka Anda menggunakan NSCollectionView dan setiap grup direpresentasikan sebagai bagian, Apple menyediakan moveSection(_:toSection:). Dokumentasinya secara eksplisit mencakup pengaturan ulang bagian beserta item di dalamnya dan mengharuskan sumber data diperbarui sebelum metode tersebut dipanggil. API ini tersedia di macOS 10.11 dan versi lebih baru. Dokumentasi pemindahan bagian Apple.
Untuk jalur tersebut, tentukan indeks bagian sumber dan tujuan akhir, susun ulang data grup, lalu beri tahu tampilan koleksi menggunakan API pemindahan bagiannya. Pisahkan perhitungan batas penyisipan SwiftUI dari indeks bagian tujuan yang diharapkan AppKit. Kedua metode tersebut mendefinisikan tujuannya secara berbeda.
Untuk menyematkan koleksi AppKit di SwiftUI, gunakan NSViewRepresentable, pembungkus terdokumentasi Apple untuk membuat dan memperbarui tampilan AppKit. Teruskan interaksi koleksi kembali ke model SwiftUI melalui mekanisme koordinasi yang sesuai, dan biarkan SwiftUI mengendalikan tata letak luar tampilan yang direpresentasikan. Dokumentasi NSViewRepresentable Apple.
TextKit menangani tata letak teks di dalam sebuah item
Apple mendefinisikan NSTextLayoutManager sebagai pengelola tata letak dan presentasi teks khusus, yang tersedia di macOS 12 dan versi lebih baru. Tanggung jawabnya mencakup kontainer teks, elemen, dan fragmen tata letak. Untuk tugas koleksi ini, konsekuensi arsitekturalnya adalah mempertahankan pengurutan grup di model koleksi, sekalipun sebuah anak berisi editor TextKit khusus. Dokumentasi NSTextLayoutManager Apple.
Bagaimana mendiagnosis grup yang berpindah secara keliru?
Mulailah dengan contoh terkecil yang terlihat dan periksa model setelah satu operasi. Pemeriksaan di bawah membedakan kesalahan urutan dari ketidakcocokan antara baris yang ditampilkan dan data yang mendasarinya.
- Hanya judul yang berpindah: periksa apakah anak-anak disimpan sebagai entri yang tidak terkait dalam larik datar. Pindahkan objek induk yang memilikinya, atau implementasikan secara eksplisit pemindahan blok untuk representasi datar tersebut.
- Pemindahan ke bawah tidak melakukan apa pun: periksa apakah tombol memberikan
index + 1. Untuk satu item sumber, batas tersebut membiarkan item tetap di tempatnya. - Grup yang salah berpindah setelah pemfilteran: periksa apakah offset yang terlihat diterapkan pada larik penuh. Petakan ID grup yang terlihat ke model kanonis sebelum mutasi, dan tentukan posisi tujuan dalam hasil yang difilter.
- Urutannya langsung kembali seperti semula: periksa apakah larik yang ditampilkan kembali diurutkan pada setiap render. Pengurutan manual memerlukan urutan yang benar-benar dipertahankan oleh tampilan.
- Anak berpindah secara mandiri: periksa lokasi pemasangan penangan pemindahan. Dalam contoh ini, penangan hanya terpasang pada koleksi grup terluar.
Sebelum integrasi, periksa pemindahan ke atas dan ke bawah, kedua ujung daftar, satu grup yang tersisa, dan grup kosong. Verifikasi invarians model sekaligus tampilannya: ID grup yang sama tetap ada, setiap grup tetap memiliki ID anak yang sama, dan urutan anak tidak berubah. Pemeriksaan ini secara langsung menguji persyaratan bahwa induk dan anak-anaknya berpindah bersama.
Untuk antarmuka Mac yang ringkas, mulailah dengan satu model induk untuk setiap baris yang dapat dipindahkan dan satu fungsi penyusunan ulang bersama. Dengan begitu, pengguna dapat memindahkan Group A dengan satu tindakan sementara anak-anaknya tetap melekat. Tambahkan implementasi hierarkis atau AppKit jika antarmuka memerlukan interaksi anak secara mandiri atau perilaku koleksi berbasis bagian.
Sumber
- onMove(perform:) — Apple
- ForEach — Apple
- move(fromOffsets:toOffset:) — Apple
- OutlineGroup — Apple
- moveSection(_:toSection:) — Apple
- NSViewRepresentable — Apple
- NSTextLayoutManager — Apple