(Jika ada orang Apple yang melihat ini - saya mengajukan umpan balik: FB8910881)

Saya mencoba membangun aplikasi SwiftUI dengan tata letak yang mirip dengan aplikasi Apple Music - Halaman Artis atau seperti di tutorial SwiftUI dari Apple (Aplikasi Landmark). Di dalam aplikasi, Anda dapat menggulir secara vertikal di atas item daftar dan secara horizontal di atas semua baris.

Apple Music App Scrolling

Di aplikasi saya, saya memiliki daftar baris di dalam ScrollView (vertikal). Baris itu sendiri bergulir vertikal dan berisi ScrollView dan LazyHGrid. Ini berfungsi di iOS tetapi tidak di macOS.

Di iOS saya dapat menggulir secara vertikal melalui daftar dan secara horizontal melalui item baris - di Simulator yang Anda klik dan seret untuk menggulir di dalam aplikasi - berfungsi seperti pesona.

iOS scrolling works

Namun pada macOS saya hanya dapat menggulir secara vertikal jika saya tidak melewati satu baris... Saya menggulir melalui mouse ajaib ke atas/bawah/kiri/kanan. Tampilan Gulir baris tampaknya menangkap pengguliran vertikal... Di app Apple Music, Anda dapat menggulir ke atas dan ke bawah dan ke kiri dan ke kanan jika Anda melewati baris Daftar Putar Artis.

Xcode 12.2 dan Xcode 12.3 beta, macOS BigSur 11.0.1

enter image description here

macOS scrolling problems

Ini kode saya:

import SwiftUI

struct ScrollViewProblem: View {

    var body: some View {
        ScrollView {
            HStack {
                Text("Scrolling Problem")
                    .font(.largeTitle)
                Spacer()
            }.padding()

            ScrollRow()
            ScrollRow()
            ScrollRow()
            ScrollRow()
        }
    }
}

struct ScrollRow: View {

    let row = [
        GridItem(.flexible(minimum: 200))
    ]

    var body: some View {

        VStack {
            HStack {
                Text("Row")
                    .font(.largeTitle)
                Spacer()
            }
            ScrollView(.horizontal) {
                LazyHGrid(rows: row, spacing: 20) {
                    ForEach(1..<7) { index in
                        Rectangle()
                            .fill(Color.blue)
                            .frame(width: 200, height: 200)
                    }
                }
            }
        }
        .padding()
    }
}

struct ScrollViewProblem_Previews: PreviewProvider {
    static var previews: some View {
        ScrollViewProblem()
    }
}
16
Denise 20 November 2020, 01:07

1 menjawab

Jawaban Terbaik

Saya telah menemukan solusi untuk bug tersebut, meskipun saya harap ini akan segera diselesaikan oleh Apple.

Untuk pemahaman terbaik saya, masalah mendasarnya adalah bahwa peristiwa gulir roda mouse tidak diteruskan dari tampilan gulir horizontal dalam ke tampilan gulir vertikal luar.

Apa yang membuatnya sangat frustasi adalah bahwa ada metode AppKit persis untuk skenario itu: menentukan apakah peristiwa gulir harus diteruskan ke rantai responden. Ini disebut wantsForwardedScrollEvents dan didokumentasikan di sini di dokumentasi Apple. Meskipun dokumentasi mengatakan itu untuk "pengguliran elastis", tumpukan ini overflow answer menunjukkan betapa bergunanya meneruskan peristiwa ke rantai responden untuk semua peristiwa yang bergulir.

Jadi yang kita butuhkan adalah:

  1. Buat NSView yang menggantikan wantsForwardedScrollEvents dan mengembalikan true untuk sumbu pengguliran vertikal sehingga peristiwa pengguliran vertikal diteruskan dari pengguliran horizontal dalam ke pengguliran vertikal luar.
  2. Masukkan NSView ini ke dalam hierarki tampilan SwiftUI antara gulir horizontal dan gulir vertikal.

Poin kedua sedikit rumit, tetapi untungnya blog SwiftUI Lab yang benar-benar menakjubkan memiliki tutorial yang bagus tentang bagaimana melakukannya dengan tepat. Serius, saya tidak dapat menghitung berapa kali blog ini telah menyelamatkan kewarasan saya ketika bekerja dengan SwiftUI.

Jadi kode akhir dapat terlihat seperti itu:

// we need this workaround only for macOS
#if os(macOS) 

// this is the NSView that implements proper `wantsForwardedScrollEvents` method
final class VerticalScrollingFixHostingView<Content>: NSHostingView<Content> where Content: View {

  override func wantsForwardedScrollEvents(for axis: NSEvent.GestureAxis) -> Bool {
    return axis == .vertical
  }
}

// this is the SwiftUI wrapper for our NSView
struct VerticalScrollingFixViewRepresentable<Content>: NSViewRepresentable where Content: View {
  
  let content: Content
  
  func makeNSView(context: Context) -> NSHostingView<Content> {
    return VerticalScrollingFixHostingView<Content>(rootView: content)
  }

  func updateNSView(_ nsView: NSHostingView<Content>, context: Context) {}

}

// this is the SwiftUI wrapper that makes it easy to insert the view 
// into the existing SwiftUI view builders structure
struct VerticalScrollingFixWrapper<Content>: View where Content : View {

  let content: () -> Content
  
  init(@ViewBuilder content: @escaping () -> Content) {
    self.content = content
  }
  
  var body: some View {
    VerticalScrollingFixViewRepresentable(content: self.content())
  }
}
#endif

Setelah struktur itu siap, kami dapat menggunakannya dalam sampel Anda seperti ini:

import SwiftUI

// just a helper to make using nicer by keeping #if os(macOS) in one place
extension View {
  @ViewBuilder func workaroundForVerticalScrollingBugInMacOS() -> some View {
    #if os(macOS)
    VerticalScrollingFixWrapper { self }
    #else
    self
    #endif
  }
}

struct ScrollViewProblem: View {
  
  var body: some View {
    ScrollView {
      HStack {
        Text("Scrolling Problem")
          .font(.largeTitle)
        Spacer()
      }.padding()
      
      ScrollRow().workaroundForVerticalScrollingBugInMacOS()
      ScrollRow().workaroundForVerticalScrollingBugInMacOS()
      ScrollRow().workaroundForVerticalScrollingBugInMacOS()
      ScrollRow().workaroundForVerticalScrollingBugInMacOS()
    }
  }
}

Diverifikasi di macOS 11.0.1 dan Xcode 12.2.

[diperbarui berdasarkan komentar oleh @marshallino16]

Anda mungkin mengalami satu masalah SwiftUI setelah menambahkan NSView ke dalam hierarki tampilan. Tampilan dalam berhenti merespons perubahan status tampilan luar.

Menggunakan istilah dari kode sampel di sini: jika ada variabel @State di ScrollViewProblem yang diturunkan ke ScrollRow, ScrollRow tidak selalu menghitung ulang pada @State perubahan variabel.

Masalah ini disebutkan di bagian "Kejutan yang Tidak Menyenangkan" di postingan blog SwiftUI Lab.

Sayangnya, saya belum menemukan solusi langsung untuk membuat propagasi @State berfungsi lagi, jadi saat ini solusi kerja terbaik yang saya ketahui (dan telah saya gunakan sendiri) adalah tidak meneruskan variabel @State dari ScrollViewProblem ke ScrollRow.

Ini berarti bahwa informasi apa pun yang Anda perlukan untuk diteruskan ke ScrollRow, Anda harus merangkumnya dalam objek eksternal (model tampilan, penyimpanan tampilan, apa pun yang digunakan arsitektur Anda). Kemudian Anda dapat meneruskan objek ini ke ScrollRow dan membiarkan ScrollRow menyimpannya dan menggunakannya sebagai variabel @State atau @StateObject.

6
siejkowski 1 Desember 2020, 11:03