Untuk menjadikan kawalan seretan SwiftUI tersuai melekap pada nilai diskret, tukarkan kedudukan setempat penuding kepada pecahan terbatas, bundarkannya kepada indeks integer dan lukis pemegang berdasarkan indeks itu. Contoh macOS ini memilih satu daripada lima saiz bulatan dan menggunakan pilihan itu apabila dilepaskan.
Tentukan interaksi sebelum menulis gerak isyarat
Tugas pembaca mudah: tambah pemilih saiz mendatar tersuai pada app Mac. Saiz yang tersedia ialah 20, 40, 60, 80 dan 100 mata. Menyeret mempratonton pilihan yang dilekapkan; melepaskan menggunakan saiz itu pada bulatan sintetik. Semuanya kekal dalam memori.
Mula-mula tentukan sama ada lukisan tersuai diperlukan. Slider standard Apple sudah menyokong kenaikan diskret melalui parameter step dan mengemas kini nilai terikat apabila pemegang bergerak. Untuk pemilih saiz biasa, julat 20...100 dengan langkah 20 menyatakan pilihan ini secara langsung. Gunakan kawalan yang disokong itu jika rupa dan interaksinya memenuhi keperluan. Dokumentasi Slider Apple
Pelaksanaan tersuai di bawah berguna apabila anda perlu mengawal trek, tanda senggatan, pemegang dan tingkah laku apabila dilepaskan. Pelaksanaan ini sengaja menggunakan indeks pilihan integer. Saiz yang dipaparkan diambil daripada tatasusunan, jadi model tidak boleh menyimpan saiz perantaraan secara tidak sengaja seperti 57.3.
Tiga keputusan menentukan contoh ini:
- Kedudukan tanda senggatan dijarakkan sama rata, dengan saiz lebih besar di sebelah kanan.
- Pemegang dan label nilai kawalan kedua-duanya melekap semasa diseret.
- Saiz pilihan pada induk berubah apabila dilepaskan; bulatan mengekalkan saiz yang telah digunakan sebelum ini semasa pratonton.
Tukarkan kedudukan penuding kepada indeks diskret
Pastikan geometri input dan geometri lukisan sepadan. Di sini, lebar kawalan ialah 336 mata dan jejari pemegang ialah 12 mata. Oleh itu, pusatnya bergerak dari 12 ke 324, memberikan panjang trek yang boleh digunakan sebanyak 312 mata. Jika lebar penuh digunakan untuk pengiraan input, kedudukan penuding dan tanda senggatan hujung tidak akan sepadan.
Pemetaan ini mempunyai empat operasi: tolak inset kiri, bahagikan dengan panjang trek yang boleh digunakan, hadkan kepada julat 0...1 dan darab dengan bilangan sela sebelum membundarkan. Lima pilihan mempunyai empat sela. Pemetaan songsang meletakkan pemegang terpilih pada tanda senggatan yang sepadan.
Fungsi pembantu berikut ialah sebahagian daripada app lengkap yang dibina dalam bahagian seterusnya. Dasar pembundarannya memilih indeks lebih tinggi pada titik tengah tepat kerana indeks ternormal bukan negatif. Apple menerangkan .toNearestOrAwayFromZero sebagai pembundaran kepada nilai terdekat, dengan nilai seri dibundarkan menjauhi sifar. Dokumentasi rounded(_:) Apple
import SwiftUI
enum SnapGeometry {
static func index(
at x: CGFloat,
width: CGFloat,
inset: CGFloat,
count: Int
) -> Int {
guard count > 1, x.isFinite, width > 2 * inset else {
return 0
}
let fraction = min(max((x - inset) / (width - 2 * inset), 0), 1)
return Int(
(fraction * CGFloat(count - 1))
.rounded(.toNearestOrAwayFromZero)
)
}
static func position(
for index: Int,
width: CGFloat,
inset: CGFloat,
count: Int
) -> CGFloat {
guard count > 1 else { return width / 2 }
return inset + CGFloat(index) / CGFloat(count - 1)
* (width - 2 * inset)
}
}
Input ilustrasi ini menunjukkan tingkah laku sempadan untuk geometri tetap. Pembatasan memastikan seretan yang melepasi mana-mana hujung tetap memilih pilihan yang sah; interaksi itu tidak dibuang.
| Koordinat x penuding | Indeks terpilih | Saiz dalam mata |
|---|---|---|
-10 |
0 |
20 |
12 |
0 |
20 |
51 |
1 |
40 |
90 |
1 |
40 |
168 |
2 |
60 |
324 |
4 |
100 |
350 |
4 |
100 |
Pada 51, penuding tepat berada di tengah-tengah antara dua tanda senggatan pertama, jadi peraturan ini memilih indeks 1. Saya menyemak pengiraan deterministik ini dengan Python. Pengkompil Swift tidak tersedia dalam persekitaran penyelidikan, jadi app SwiftUI di bawah telah diperiksa tetapi belum dikompil atau dijalankan di sini.
Bina kawalan macOS dan paparan pemiliknya
Cipta projek App macOS dalam Xcode menggunakan SwiftUI dan sasaran penggunaan macOS 13 atau lebih baharu. Gantikan deklarasi app dan paparan kandungan yang dijana dengan kod di bawah, dan sertakan fungsi pembantu sebelumnya dalam fail Swift yang sama. Fungsi pembantu dan dua blok seterusnya membentuk satu program lengkap; gunakan hanya satu deklarasi app @main.
Induk memiliki indeks yang telah digunakan dalam @State. Kawalan menerima @Binding, yang menghubungkannya kepada sumber kebenaran itu tanpa mewujudkan pilihan kekal kedua. Ini mengikut corak pemilikan yang didokumenkan Apple untuk binding. Dokumentasi Binding Apple
@main
struct SnapSizeApp: App {
var body: some Scene {
WindowGroup {
SizeDemo()
}
}
}
struct SizeDemo: View {
private let sizes = [20, 40, 60, 80, 100]
@State private var selectedIndex = 2
var body: some View {
VStack(spacing: 24) {
Text("Circle size")
.font(.headline)
Circle()
.fill(Color.accentColor)
.frame(
width: CGFloat(sizes[selectedIndex]),
height: CGFloat(sizes[selectedIndex])
)
.frame(width: 120, height: 120)
DiscreteSizeControl(
sizes: sizes,
selection: $selectedIndex
)
Text("Applied size: \(sizes[selectedIndex]) pt")
}
.padding(32)
.frame(minWidth: 420, minHeight: 340)
}
}
Kawalan menggunakan @GestureState untuk pratonton sementara. Apple menerangkan bahawa keadaan gerak isyarat ditetapkan semula kepada nilai awalnya apabila gerak isyarat menjadi tidak aktif. Di sini, kembali kepada nil menjadikan kawalan memaparkan binding yang telah digunakan semula, jadi pilihan sementara tidak memerlukan pembersihan manual dalam pemboleh ubah kekal yang berasingan. Dokumentasi GestureState Apple
struct DiscreteSizeControl: View {
let sizes: [Int]
@Binding var selection: Int
@GestureState private var previewIndex: Int? = nil
private let trackWidth: CGFloat = 336
private let radius: CGFloat = 12
private var shownIndex: Int {
previewIndex ?? selection
}
private func snappedIndex(at x: CGFloat) -> Int {
SnapGeometry.index(
at: x, width: trackWidth,
inset: radius, count: sizes.count
)
}
private func xPosition(for index: Int) -> CGFloat {
SnapGeometry.position(
for: index, width: trackWidth,
inset: radius, count: sizes.count
)
}
private func moveSelection(by amount: Int) {
guard previewIndex == nil else { return }
selection = min(max(selection + amount, 0), sizes.count - 1)
}
var body: some View {
VStack(spacing: 10) {
Text("Selected size: \(sizes[shownIndex]) pt")
.monospacedDigit()
ZStack(alignment: .topLeading) {
Capsule()
.fill(Color.secondary.opacity(0.25))
.frame(width: trackWidth - 2 * radius, height: 4)
.position(x: trackWidth / 2, y: 20)
ForEach(sizes.indices, id: \.self) { index in
Circle()
.fill(Color.secondary)
.frame(width: 5, height: 5)
.position(x: xPosition(for: index), y: 20)
}
Circle()
.fill(Color.accentColor)
.frame(width: 2 * radius, height: 2 * radius)
.position(x: xPosition(for: shownIndex), y: 20)
}
.frame(width: trackWidth, height: 40)
.contentShape(Rectangle())
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.updating($previewIndex) { value, state, _ in
state = snappedIndex(at: value.location.x)
}
.onEnded { value in
selection = snappedIndex(at: value.location.x)
}
)
.accessibilityElement(children: .ignore)
.accessibilityLabel("Circle size")
.accessibilityValue("\(sizes[shownIndex]) points")
.accessibilityAdjustableAction { direction in
switch direction {
case .increment:
moveSelection(by: 1)
case .decrement:
moveSelection(by: -1)
@unknown default:
break
}
}
HStack {
Button("Smaller") { moveSelection(by: -1) }
.disabled(selection == 0 || previewIndex != nil)
Button("Larger") { moveSelection(by: 1) }
.disabled(
selection == sizes.count - 1 || previewIndex != nil
)
}
}
}
}
Sampel ini mempunyai prasyarat yang jelas: tatasusunan saiz tidak kosong, indeks yang telah digunakan adalah sah dan tatasusunan kekal tetap sepanjang interaksi. Induk yang diberikan memenuhi ketiga-tiganya. Nilai pulangan defensif fungsi pembantu geometri tidak menjadikan tatasusunan kosong sah untuk kawalan ini kerana label masih mengindeks tatasusunan itu. Jika pilihan datang daripada model yang berubah, sahkan koleksi dan pilihan sebelum memaparkannya, serta tentukan perkara yang berlaku jika pilihan berubah semasa seretan.
Kekalkan gerak isyarat pada geometri kawalan yang stabil
Seretan terikat pada bekas trek 336 mata yang tidak bergerak. Pemegang bergerak di dalam bekas itu. Apple mentakrifkan location sebagai lokasi peristiwa semasa dan membolehkan DragGesture menentukan ruang koordinat yang digunakan untuk lokasi. Penggunaan .local pada bekas yang stabil memastikan koordinat x yang dilaporkan sepadan dengan pengiraan lukisan. Dokumentasi DragGesture Apple, dokumentasi location Apple
Bentuk kandungan segi empat tepat menjadikan kawasan trek kawalan boleh diuji untuk sentuhan, termasuk ruang di antara bentuk yang dilukis. Jarak minimum sifar membolehkan pengecaman tanpa memerlukan ambang pergerakan awal. Dalam reka bentuk ini, menekan bahagian lain pada trek akan mempratonton tanda senggatan terdekat dengan serta-merta. Tingkah laku melompat ke kedudukan ini disengajakan. Dokumentasi contentShape(_:eoFill:) Apple, dokumentasi DragGesture Apple
Untuk interaksi yang hanya menggunakan pemegang dan mengekalkan ofset cengkaman awal, gunakan pemetaan yang berbeza: tangkap pusat awal pemegang sekali, kemudian tambah translasi mendatar keseluruhan gerak isyarat sebelum melekapkannya. Jangan berulang kali menambah translasi keseluruhan pada pusat yang sudah dikemas kini. SwiftUI mendokumenkan translasi sebagai anjakan dari permulaan gerak isyarat, bukannya pergerakan sejak panggilan balik sebelumnya. Dokumentasi translation Apple
Jauhkan gerak isyarat daripada tindanan label dan butang di sekelilingnya. Pemasangan gerak isyarat mempengaruhi hubungan pengecaman dengan paparan yang dipasang padanya dan subpaparannya; meluaskan skop itu boleh menyebabkan persaingan dengan kawalan lain. Jika anda membenamkan contoh ini dalam paparan skrol, uji pengecaman di situ secara khusus. Seretan dengan jarak sifar ialah interaksi serta-merta yang disengajakan, jadi panel kendiri ialah titik permulaan yang paling jelas. Dokumentasi gesture(_:including:) Apple
Tentukan masa pratonton menjadi keadaan yang telah digunakan
Semasa menyeret, hanya pemegang kawalan dan label nilai terpilih yang berubah. Bulatan yang lebih besar dan label nilai yang telah digunakan mengikut indeks yang telah digunakan pada induk. Apabila dilepaskan, pengendali mengira indeks akhir daripada lokasi akhir, bukannya bergantung pada panggilan balik pratonton terkini.
Jika gerak isyarat menjadi tidak aktif tanpa penggunaan itu, pratonton sementara ditetapkan semula dan pilihan yang telah digunakan sebelum ini dikekalkan. Tingkah laku ini terhasil daripada penyimpanan keadaan pratonton dalam @GestureState dan penulisan kekal dalam pengendali tamat. Dokumentasi GestureState Apple
Untuk penggunaan secara langsung, tulis indeks yang dilekapkan kepada binding semasa perubahan seretan. Ini mengubah kontrak interaksi: pilihan perantaraan menjadi kemas kini model yang telah digunakan. Tentukan sama ada itu dikehendaki sebelum menghubungkan pemilih dengan kerja pemaparan yang lebih rumit. Dalam mana-mana reka bentuk, lukis pemegang berdasarkan indeks terkuantum yang sama dengan nilai terpilih.
Butang Smaller dan Larger menggunakan perubahan satu langkah dengan serta-merta dan berhenti di hujung julat. Trek juga menyediakan tindakan kebolehcapaian boleh laras yang pengendali kenaikan dan penurunannya menggunakan fungsi langkah yang sama. Apple mendokumenkan pengubah suai ini sebagai mekanisme untuk teknologi bantuan seperti VoiceOver memanggil tindakan boleh laras. Dokumentasi accessibilityAdjustableAction(_:) Apple
Apabila paparan AppKit mengendalikan interaksi
Paparan SwiftUI tulen tidak memerlukan jambatan AppKit untuk tugas ini. Jika trek sudah berada dalam NSView anda sendiri, kekalkan pengecaman dalam paparan itu. addGestureRecognizer(_:) Apple melampirkan pengecam pada paparan, mengehadkan gerak isyarat yang diwakilinya kepada paparan itu dan subpaparannya, serta mewujudkan rujukan kuat kepada pengecam tersebut. Dokumentasi addGestureRecognizer(_:) Apple
Untuk pelaksanaan AppKit yang menggunakan NSPanGestureRecognizer, ukur translasi dalam sistem koordinat paparan trek yang tidak bergerak. Tangkap kedudukan awal pemegang, tambah translasi keseluruhan yang dilaporkan, kemudian hantar koordinat x yang terhasil melalui pengiraan pelekap yang sama. Apple menyatakan dengan jelas bahawa translation(in:) mengembalikan jumlah jarak sejak gerak isyarat bermula, dengan transformasi paparan yang dipilih digunakan. Dokumentasi translation(in:) Apple
Gunakan satu pelaksanaan gerak isyarat bagi setiap permukaan interaksi. Jika trek AppKit dibenamkan dalam SwiftUI, biarkan trek itu mengendalikan seretan dan melaporkan perubahan pilihan melalui sambungan model app. Ini mengekalkan laluan yang jelas daripada koordinat peristiwa kepada pratonton dan kemudian kepada pilihan yang telah digunakan.
Semak hujung julat, ambang dan seretan yang terganggu
Sahkan interaksi dalam app Mac dengan urutan ringkas dan konkrit:
- Mulakan pada tanda senggatan tengah. Seret merentasi setiap sela dan pastikan label terpilih hanya memaparkan lima saiz yang dibenarkan.
- Bergerak melepasi kedua-dua hujung mendatar. Pemegang sepatutnya kekal pada tanda senggatan pertama atau terakhir.
- Lepaskan di antara tanda senggatan. Bulatan sepatutnya menggunakan saiz terdekat, sepadan dengan kedudukan akhir pemegang.
- Klik bahagian trek yang kosong. Pastikan tingkah laku melompat ke kedudukan sesuai untuk tugas ini.
- Ganggu interaksi yang sedang berlangsung dan pastikan tiada pratonton sementara yang tersangkut.
- Gunakan kedua-dua butang langkah dan pelarasan VoiceOver. Pastikan semuanya berhenti pada hujung julat yang sama.
Untuk pilihan tidak sekata seperti 20, 30, 60 dan 100, reka bentuk berasaskan indeks ini masih menjarakkan pilihan sama rata. Itu ialah susun atur mengikut tertib yang disengajakan. Jika jarak sepatutnya mencerminkan jarak angka, petakan setiap nilai angka kepada kedudukan treknya sendiri dan pilih kedudukan terdekat; pembundaran indeks seragam akan menghasilkan interaksi yang berbeza.
Kawalan pelekap yang boleh diharap menggunakan satu sistem koordinat dan satu pemetaan untuk input serta lukisan. Apabila kedua-duanya sejajar, indeks integer memastikan setiap nilai terpilih kekal diskret, manakala keadaan pratonton dan keadaan yang telah digunakan secara berasingan menjelaskan tingkah laku apabila dilepaskan.
Sumber
- Slider — Apple
- rounded(_:) — Apple
- Binding — Apple
- GestureState — Apple
- DragGesture — Apple
- location — Apple
- contentShape(_:eoFill:) — Apple
- translation — Apple
- gesture(_:including:) — Apple
- accessibilityAdjustableAction(_:) — Apple
- addGestureRecognizer(_:) — Apple
- translation(in:) — Apple