Untuk apl Mac dengan rak mendatar di dalam senarai menegak, tetapkan destinasi yang jelas bagi setiap gerak isyarat. Mulakan dengan konfigurasi paksi SwiftUI; jika anda memerlukan penghalaan eksplisit, kendalikan paparan tatal AppKit bersarang melalui jambatan SwiftUI.
Tugas khusus di sini ialah menyemak imbas kad sintetik: menatal secara menegak di atas mana-mana rak sepatutnya menggerakkan keseluruhan senarai, manakala menatal secara mendatar sepatutnya menggerakkan rak itu secara berasingan. Hal ini memerlukan ofset mendatar yang berasingan bagi setiap baris. Ini berbeza daripada satu kanvas yang bergerak secara mendatar dan menegak secara serentak.
Pilih struktur tatal yang sepadan dengan tugas
Apple mendokumenkan bahawa ScrollView SwiftUI boleh menatal secara mendatar, menegak atau pada kedua-dua paksi. Untuk satu dokumen besar, gunakan ScrollView([.horizontal, .vertical]) dan berikan kandungannya dimensi yang melebihi port pandang. Ini ialah struktur paling ringkas apabila semua kandungan berkongsi satu kedudukan mendatar dan satu kedudukan menegak. ScrollView
Rak yang berasingan memerlukan bekas tatal yang berasingan. Struktur SwiftUI yang semula jadi ialah ScrollView menegak yang mengandungi tika ScrollView mendatar, setiap satunya dengan ketinggian terhad. Namun, mengkonfigurasi paksi itu sendiri tidak menentukan cara peristiwa yang diterima di atas bekas dalaman patut sampai ke bekas luaran. Dokumentasi ScrollView Apple menerangkan paksi yang disokong tanpa menetapkan kontrak penghalaan peristiwa umum untuk kes bersarang ini. ScrollView
| Tingkah laku yang diperlukan | Titik permulaan yang sesuai |
|---|---|
| Satu dokumen bergerak dalam kedua-dua arah | Satu ScrollView SwiftUI dua paksi |
| Setiap rak mengekalkan kedudukan mendatarnya sendiri | Rak mendatar bersarang di dalam bekas menegak |
| Gerak isyarat menegak di atas rak memerlukan destinasi eksplisit | Subkelas NSScrollView milik apl yang didedahkan melalui NSViewRepresentable |
| Gerak isyarat pepenjuru mesti menggerakkan seluruh dokumen dengan bebas | Satu bekas dua paksi; AppKit turut menyediakan usesPredominantAxisScrolling |
AppKit membezakan kandungan yang kebanyakannya menatal sepanjang satu paksi pada satu-satu masa daripada kandungan seperti kanvas yang sepatutnya bergerak bebas pada kedua-dua paksi. Sifat usesPredominantAxisScrolling menyatakan perbezaan itu; nilai lalai yang didokumenkannya ialah true. Sifat itu berkaitan dengan tingkah laku paparan tatal, bukannya memilih paparan tatal bersarang yang lain sebagai destinasi peristiwa. usesPredominantAxisScrolling
Fahami batas pemajuan yang didokumenkan
Nama wantsForwardedScrollEvents(for:) boleh memberi gambaran bahawa ia penyelesaian universal untuk tatal bersarang. Apple mendokumenkan syarat yang lebih khusus: paparan yang melakukan tatal anjal boleh memajukan peristiwa gerak isyarat apabila kandungan sudah berada di tepi yang berkenaan pada permulaan gerak isyarat. Dokumentasi itu tidak menetapkan pemajuan tanpa syarat bagi peristiwa roda biasa atau pemisahan paksi sewenang-wenangnya. wantsForwardedScrollEvents(for:)
Begitu juga, pelaksanaan lalai NSResponder.scrollWheel(with:) menghantar mesej kepada responden seterusnya, manakala NSScrollView melaksanakan kaedah itu untuk melakukan penatalan. Oleh itu, anda patut membezakan responden yang meneruskan peristiwa yang tidak dikendalikannya dengan bekas tatal yang menggunakan peristiwa tersebut. NSResponder scrollWheel(with:), NSScrollView scrollWheel(with:)
Untuk antara muka yang memerlukan kawalan eksplisit, blok binaan yang disokong ialah NSViewRepresentable, yang membenamkan paparan AppKit dalam SwiftUI, dan NSHostingView, yang membenamkan kandungan SwiftUI dalam AppKit. Pelaksanaan di bawah menggunakan jambatan tersebut untuk mengendalikan kedua-dua bekas tatal sambil mengekalkan kad dalam SwiftUI. Polisi penghalaannya ialah kod aplikasi yang berasaskan API yang didokumenkan, bukannya resipi tatal bersarang yang disediakan Apple. NSViewRepresentable, NSHostingView
Bina contoh tatal bersarang yang dikendalikan apl
Cipta projek apl macOS dalam Xcode menggunakan SwiftUI dan Swift. Tetapkan sasaran penggunaan kepada macOS 11 atau lebih baharu. Gantikan titik masuk apl yang dijana dengan fail pertama di bawah, kemudian tambahkan fail kedua pada sasaran yang sama. Bersama-sama, kedua-dua fail ini membentuk program demonstrasi lengkap dengan label sintetik dan tanpa aset tambahan.
Contoh ini menggunakan kitar hayat App SwiftUI yang tersedia sejak macOS 11. Kekalkan tepat satu pengisytiharan @main dalam sasaran: dokumentasi kitar hayat Apple dan rujukan bahasa Swift memerlukan satu titik masuk program. App, Attributes
Cipta senarai luaran dan kad SwiftUI
Fail pertama mencipta lapan rak. Setiap rak mempunyai enam belas kad dan dokumen mendatarnya sendiri. Dokumen luaran menjejak lebar port pandang dan mempunyai ketinggian demonstrasi tetap, jadi hanya bekas luaran memerlukan penatalan menegak.
SwiftUI mengawal bingkai paparan luaran yang diwakili. Reka letak tersuai hanya mengubah dokumen AppKit dan bingkai rak di dalamnya, selaras dengan amaran Apple supaya tidak mengubah sifat reka letak paparan yang diurus secara langsung oleh NSViewRepresentable. NSViewRepresentable
Simpan sebagai NestedScrollDemoApp.swift:
import SwiftUI
import AppKit
@main
struct NestedScrollDemoApp: App {
var body: some Scene {
WindowGroup {
ShelfBoard()
.frame(minWidth: 420, minHeight: 320)
}
}
}
struct ShelfBoard: NSViewRepresentable {
func makeNSView(context: Context) -> BoardScrollView {
BoardScrollView(frame: .zero)
}
func updateNSView(_ nsView: BoardScrollView, context: Context) {
// The demonstration content is static.
}
}
struct ShelfCards: View {
let row: Int
var body: some View {
HStack(spacing: 12) {
ForEach(1...16, id: \.self) { column in
Text("Shelf \(row) · Card \(column)")
.frame(width: 140, height: 96)
.background(Color.gray.opacity(0.15))
.cornerRadius(8)
}
}
.padding(12)
.frame(width: 2444, height: 120)
}
}
final class TopDocument: NSView {
override var isFlipped: Bool { true }
}
final class BoardScrollView: NSScrollView {
private let board = TopDocument(frame: .zero)
private var shelves: [ShelfScrollView] = []
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
hasVerticalScroller = true
hasHorizontalScroller = false
autohidesScrollers = true
horizontalScrollElasticity = .none
documentView = board
for row in 1...8 {
let shelf = ShelfScrollView(frame: .zero)
shelf.verticalOwner = self
shelf.hasHorizontalScroller = true
shelf.hasVerticalScroller = false
shelf.autohidesScrollers = true
shelf.verticalScrollElasticity = .none
let cards = NSHostingView(rootView: ShelfCards(row: row))
cards.frame = NSRect(x: 0, y: 0, width: 2444, height: 120)
shelf.documentView = cards
board.addSubview(shelf)
shelves.append(shelf)
}
}
required init?(coder: NSCoder) {
fatalError("Use init(frame:)")
}
override func layout() {
super.layout()
let width = contentSize.width
board.frame = NSRect(x: 0, y: 0, width: width, height: 1184)
for (index, shelf) in shelves.enumerated() {
shelf.frame = NSRect(
x: 0, y: 8 + CGFloat(index) * 146,
width: width, height: 140
)
}
}
}
NSScrollView menyelaras paparan dokumen, paparan pemotongan dan palang tatalnya. Di sini, dokumen luaran mengandungi paparan tatal rak; dokumen setiap rak ialah NSHostingView yang mengandungi kad SwiftUI. Lebar 2444 titik ialah pengiraan ilustrasi reka letak: enam belas kad pada 140, lima belas ruang sela pada 12 dan 24 titik pelapik mendatar. NSScrollView, NSHostingView
Halakan gerak isyarat sekali dan kekalkan destinasinya
Fail kedua menyediakan polisi penghalaan. Gerak isyarat yang pergerakan bukan sifar pertamanya lebih banyak menegak dihantar ke senarai luaran. Jika tidak, gerak isyarat itu kekal pada rak. Jika seimbang, gerak isyarat itu diberikan kepada rak. Ini ialah pilihan reka bentuk eksplisit untuk demonstrasi ini.
Apple menyediakan sifat phase dan momentumPhase secara berasingan. Peristiwa roda legasi tidak mempunyai fasa, manakala peristiwa momentum tidak mempunyai fasa gerak isyarat biasa. Oleh itu, contoh ini mengendalikan peristiwa roda tanpa fasa secara berasingan dan mengekalkan destinasi gerak isyarat berfasa sepanjang urutan momentumnya. phase, momentumPhase
Simpan sebagai ShelfScrollView.swift:
import AppKit
final class ShelfScrollView: NSScrollView {
weak var verticalOwner: NSScrollView?
private var routesVertically: Bool?
private var pending: [NSEvent] = []
override func scrollWheel(with event: NSEvent) {
let phase = event.phase
let momentum = event.momentumPhase
let x = event.scrollingDeltaX
let y = event.scrollingDeltaY
// A legacy wheel event has no gesture or momentum phase.
if phase.isEmpty && momentum.isEmpty {
routesVertically = nil
pending.removeAll()
deliver(event, vertically: abs(y) > abs(x))
return
}
if phase.contains(.mayBegin) {
routesVertically = nil
pending = [event]
return
}
if phase.contains(.began) {
routesVertically = nil
if pending.last?.phase.contains(.mayBegin) != true {
pending.removeAll()
}
}
let finishes = phase.contains(.cancelled)
|| momentum.contains(.ended)
|| momentum.contains(.cancelled)
if let vertical = routesVertically {
deliver(event, vertically: vertical)
} else {
// Keep zero-delta opening events until direction is known.
pending.append(event)
if x != 0 || y != 0 {
let vertical = abs(y) > abs(x)
routesVertically = vertical
let openingEvents = pending
pending.removeAll()
for openingEvent in openingEvents {
deliver(openingEvent, vertically: vertical)
}
} else if phase.contains(.ended) || finishes {
pending.removeAll()
}
}
if finishes {
routesVertically = nil
pending.removeAll()
}
// Retain direction after phase.ended for possible momentum.
}
private func deliver(_ event: NSEvent, vertically: Bool) {
if vertically, let owner = verticalOwner {
owner.scrollWheel(with: event)
} else {
super.scrollWheel(with: event)
}
}
}
Kod ini membaca scrollingDeltaX dan scrollingDeltaY hanya untuk memilih destinasi. Kod itu menghantar peristiwa asal kepada satu paparan tatal, lalu membolehkan AppKit melakukan penatalan. Kod itu tidak mentafsir semula delta sebagai ofset titik. Hal ini penting kerana Apple mendokumenkan pengendalian delta tepat dan tidak tepat yang berbeza; contohnya, delta menegak tidak tepat memerlukan penskalaan ketinggian baris apabila aplikasi melaksanakan pergerakan sendiri. scrollingDeltaX, scrollingDeltaY
Rujukan verticalOwner yang lemah menunjuk terus kepada paparan tatal luaran yang dicipta oleh contoh ini. Cara ini mengelakkan pergantungan pada hierarki paparan dalaman SwiftUI. Contoh ini tidak menambah pemantau peristiwa; penggantian kaedahnya mengendalikan peristiwa yang dihantar kepada paparan rak apl.
Periksa kedua-dua arah, momentum dan saiz semula
Contoh ini telah diperiksa berdasarkan dokumentasi API yang diperoleh, tetapi tidak dikompil atau dijalankan: persekitaran penyelidikan tidak mempunyai pengkompil Swift atau masa jalan macOS. Bina contoh ini dalam Xcode dan lakukan pemeriksaan berikut sebelum menggunakan polisi penghalaan tersebut dalam antara muka sedia ada.
| Pemeriksaan | Hasil yang diinginkan |
|---|---|
| Tatal secara menegak dengan penuding di atas kad | Senarai luaran bergerak |
| Tatal secara mendatar di atas rak pertama | Hanya rak itu bergerak secara mendatar |
| Gerakkan rak lain secara mendatar | Ofsetnya berubah secara berasingan |
| Mulakan secara menegak, kemudian tambahkan gerakan pepenjuru | Gerak isyarat kekal diberikan kepada senarai luaran |
| Mulakan secara mendatar, kemudian tambahkan gerakan pepenjuru | Gerak isyarat kekal diberikan kepada rak |
| Lepaskan gerak isyarat pad sentuh sehingga menghasilkan momentum | Momentum mengekalkan destinasi yang dipilih |
| Gunakan roda menegak konvensional | Setiap peristiwa roda dihalakan ke senarai luaran |
| Ubah saiz tetingkap | Rak mengikuti lebar port pandang luaran |
Uji juga permulaan gerak isyarat dengan delta sifar, pembatalan, perubahan arah pantas serta palang tatal tindanan dan palang tatal yang sentiasa kelihatan. Penimbalan dalam ShelfScrollView disediakan supaya permulaan dengan delta sifar tidak memilih penatalan mendatar terlalu awal. Tingkah lakunya pada peranti dan versi macOS yang anda sokong masih perlu disahkan semasa jalan.
Polisi pepenjuru sengaja memilih satu destinasi untuk gerak isyarat berfasa. Polisi ini tidak menjanjikan pergerakan serentak senarai luaran dan rak. Di hujung rak mendatar, contoh ini juga mengekalkan rak itu sebagai destinasi dan bukannya menyerahkan gerak isyarat kepada senarai menegak. Jika tugas anda memerlukan penatalan pepenjuru berterusan, gunakan satu dokumen dua paksi.
Contoh ini membina semua kad serta-merta dan menggunakan dimensi tetap. Anggap dimensi tersebut sebagai nilai input demonstrasi. Untuk kandungan yang berubah-ubah, laksanakan updateNSView bagi mengemas kini dokumen dan paparan hosting sedia ada sambil mengekalkan kedudukan tatalnya; Apple mendokumenkan kaedah itu sebagai tempat untuk menggunakan keadaan SwiftUI baharu pada paparan AppKit yang diwakili. NSViewRepresentable
Singkirkan punca penatalan dilumpuhkan dan masalah reka letak teks
Jika kedua-dua arah tidak berfungsi, periksa pengubah sebelum mengubah penghalaan. Pada macOS 13 dan lebih baharu, scrollDisabled(_:) meneruskan tetapannya melalui persekitaran SwiftUI dan boleh menjejaskan paparan tatal SwiftUI luaran serta paparan tatal bersarang. Pengubah ini mengawal pengaktifan penatalan, bukannya mekanisme pemajuan paksi. scrollDisabled(_:)
Jika rak anda akhirnya mengandungi paparan teks AppKit, periksa geometri teks secara berasingan. NSTextContainer.widthTracksTextView daripada TextKit mengawal sama ada lebar bekas teks mengikuti lebar paparan teks. Ia tidak memilih paparan tatal yang menerima gerak isyarat roda. Perubahan reka letak teks boleh mengubah sama ada kandungan melimpah secara mendatar, tetapi perubahan itu tidak patut dikemukakan sebagai pembaikan penghalaan peristiwa. Demonstrasi kad mengelakkan pemboleh ubah tambahan itu. widthTracksTextView
Mulakan dengan satu paparan tatal SwiftUI dua paksi apabila antara muka anda ialah satu dokumen. Untuk rak bebas yang memerlukan penghalaan menegak eksplisit, jambatan milik apl memberikan destinasi yang jelas bagi setiap gerak isyarat sambil mengekalkan kandungan kad SwiftUI. Sahkan polisi gerak isyarat pada Mac dan peranti input yang disokong oleh apl anda sebelum mengembangkan contoh ini kepada kandungan dinamik.
Sumber
- ScrollView — Apple
- usesPredominantAxisScrolling — Apple
- wantsForwardedScrollEvents(for:) — Apple
- scrollWheel(with:) — NSResponder — Apple
- scrollWheel(with:) — NSScrollView — Apple
- NSViewRepresentable — Apple
- NSHostingView — Apple
- NSScrollView — Apple
- App — Apple
- Attributes — Swift.org
- phase — Apple
- momentumPhase — Apple
- scrollingDeltaX — Apple
- scrollingDeltaY — Apple
- scrollDisabled(_:) — Apple
- widthTracksTextView — Apple