欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

判斷?ScrollView List?是否正在滾動詳解

 更新時間:2022年09月14日 08:36:44   作者:東坡肘子  
這篇文章主要為大家介紹了判斷?ScrollView、List?是否正在滾動示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

正文

判斷一個可滾動控件( ScrollView、List )是否處于滾動狀態(tài)在某些場景下具有重要的作用。比如在 SwipeCell 中,需要在可滾動組件開始滾動時,自動關(guān)閉已經(jīng)打開的側(cè)滑菜單。遺憾的是,SwiftUI 并沒有提供這方面的 API 。本文將介紹幾種在 SwiftUI 中獲取當(dāng)前滾動狀態(tài)的方法,每種方法都有各自的優(yōu)勢和局限性。

方法一:Introspect

可在 此處 獲取本節(jié)的代碼

在 UIKit( AppKit )中,開發(fā)者可以通過 Delegate 的方式獲知當(dāng)前的滾動狀態(tài),主要依靠以下三個方法:

scrollViewDidScroll(_ scrollView: UIScrollView)

開始滾動時調(diào)用此方法

scrollViewDidEndDecelerating(_ scrollView: UIScrollView)

手指滑動可滾動區(qū)域后( 此時手指已經(jīng)離開 ),滾動逐漸減速,在滾動停止時會調(diào)用此方法

scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool)

手指拖動結(jié)束后( 手指離開時 ),調(diào)用此方法

在 SwiftUI 中,很多的視圖控件是對 UIKit( AppKit )控件的二次包裝。因此,我們可以通過訪問其背后的 UIKit 控件的方式( 使用 Introspect )來實現(xiàn)本文的需求。

final class ScrollDelegate: NSObject, UITableViewDelegate, UIScrollViewDelegate {
    var isScrolling: Binding<Bool>?
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if let isScrolling = isScrolling?.wrappedValue,!isScrolling {
            self.isScrolling?.wrappedValue = true
        }
    }
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        if let isScrolling = isScrolling?.wrappedValue, isScrolling {
            self.isScrolling?.wrappedValue = false
        }
    }
    // 手指緩慢拖動可滾動控件,手指離開后,decelerate 為 false,因此并不會調(diào)用 scrollViewDidEndDecelerating 方法
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        if !decelerate {
            if let isScrolling = isScrolling?.wrappedValue, isScrolling {
                self.isScrolling?.wrappedValue = false
            }
        }
    }
}
extension View {
    func scrollStatusByIntrospect(isScrolling: Binding<Bool>) -> some View {
        modifier(ScrollStatusByIntrospectModifier(isScrolling: isScrolling))
    }
}
struct ScrollStatusByIntrospectModifier: ViewModifier {
    @State var delegate = ScrollDelegate()
    @Binding var isScrolling: Bool
    func body(content: Content) -> some View {
        content
            .onAppear {
                self.delegate.isScrolling = $isScrolling
            }
            // 同時支持 ScrollView 和 List
            .introspectScrollView { scrollView in
                scrollView.delegate = delegate
            }
            .introspectTableView { tableView in
                tableView.delegate = delegate
            }
    }
}

調(diào)用方法:

struct ScrollStatusByIntrospect: View {
    @State var isScrolling = false
    var body: some View {
        VStack {
            Text("isScrolling: \(isScrolling1 ? "True" : "False")")
            List {
                ForEach(0..<100) { i in
                    Text("id:\(i)")
                }
            }
            .scrollStatusByIntrospect(isScrolling: $isScrolling)
        }
    }
}

方案一優(yōu)點

  • 準(zhǔn)確
  • 及時
  • 系統(tǒng)負(fù)擔(dān)小

方案一缺點

  • 向后兼容性差
  • SwiftUI 隨時可能會改變控件的內(nèi)部實現(xiàn)方式,這種情況已經(jīng)多次出現(xiàn)。目前 SwiftUI 在內(nèi)部的實現(xiàn)上去 UIKit( AppKit )化很明顯,比如,本節(jié)介紹的方法在 SwiftUI 4.0 中已經(jīng)失效

方法二:Runloop

我第一次接觸 Runloop 是在學(xué)習(xí) Combine 的時候,直到我碰到 Timer 的閉包并沒有按照預(yù)期被調(diào)用時才對其進(jìn)行了一定的了解

Runloop 是一個事件處理循環(huán)。當(dāng)沒有事件時,Runloop 會進(jìn)入休眠狀態(tài),而有事件時,Runloop 會調(diào)用對應(yīng)的 Handler。

Runloop 與線程是綁定的。在應(yīng)用程序啟動的時候,主線程的 Runloop 會被自動創(chuàng)建并啟動。

Runloop 擁有多種模式( Mode ),它只會運行在一個模式之下。如果想切換 Mode,必須先退出 loop 然后再重新指定一個 Mode 進(jìn)入。

在絕大多數(shù)的時間里,Runloop 都處于 kCFRunLoopDefaultMode( default )模式中,當(dāng)可滾動控件處于滾動狀態(tài)時,為了保證滾動的效率,系統(tǒng)會將 Runloop 切換至 UITrackingRunLoopMode( tracking )模式下。

本節(jié)采用的方法便是利用了上述特性,通過創(chuàng)建綁定于不同 Runloop 模式下的 TimerPublisher ,實現(xiàn)對滾動狀態(tài)的判斷。

final class ExclusionStore: ObservableObject {
    @Published var isScrolling = false
    // 當(dāng) Runloop 處于 default( kCFRunLoopDefaultMode )模式時,每隔 0.1 秒會發(fā)送一個時間信號
    private let idlePublisher = Timer.publish(every: 0.1, on: .main, in: .default).autoconnect()
    // 當(dāng) Runloop 處于 tracking( UITrackingRunLoopMode )模式時,每隔 0.1 秒會發(fā)送一個時間信號
    private let scrollingPublisher = Timer.publish(every: 0.1, on: .main, in: .tracking).autoconnect()
    private var publisher: some Publisher {
        scrollingPublisher
            .map { _ in 1 } // 滾動時,發(fā)送 1
            .merge(with:
                idlePublisher
                    .map { _ in 0 } // 不滾動時,發(fā)送 0
            )
    }
    var cancellable: AnyCancellable?
    init() {
        cancellable = publisher
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: { _ in }, receiveValue: { output in
                guard let value = output as? Int else { return }
                if value == 1,!self.isScrolling {
                    self.isScrolling = true
                }
                if value == 0, self.isScrolling {
                    self.isScrolling = false
                }
            })
    }
}
struct ScrollStatusMonitorExclusionModifier: ViewModifier {
    @StateObject private var store = ExclusionStore()
    @Binding var isScrolling: Bool
    func body(content: Content) -> some View {
        content
            .environment(\.isScrolling, store.isScrolling)
            .onChange(of: store.isScrolling) { value in
                isScrolling = value
            }
            .onDisappear {
                store.cancellable = nil // 防止內(nèi)存泄露
            }
    }
}

方案二優(yōu)點

  • 具備與 Delegate 方式幾乎一致的準(zhǔn)確性和及時性
  • 實現(xiàn)的邏輯非常簡單

方案二缺點

  • 只能運行于 iOS 系統(tǒng)
  • 在 macOS 下的 eventTracking 模式中,該方案的表現(xiàn)并不理想
  • 屏幕中只能有一個可滾動控件
  • 由于任意可滾動控件滾動時,都會導(dǎo)致主線程的 Runloop 切換至 tracing 模式,因此無法有效地區(qū)分滾動是由那個控件造成的

方法三:PreferenceKey

在 SwiftUI 中,子視圖可以通過 preference 視圖修飾器向其祖先視圖傳遞信息( PreferenceKey )。preference 與 onChange 的調(diào)用時機非常類似,只有在值發(fā)生改變后才會傳遞數(shù)據(jù)。

在 ScrollView、List 發(fā)生滾動時,它們內(nèi)部的子視圖的位置也將發(fā)生改變。我們將以是否可以持續(xù)接收到它們的位置信息為依據(jù)判斷當(dāng)前是否處于滾動狀態(tài)。

final class CommonStore: ObservableObject {
    @Published var isScrolling = false
    private var timestamp = Date()
    let preferencePublisher = PassthroughSubject<Int, Never>()
    let timeoutPublisher = PassthroughSubject<Int, Never>()
    private var publisher: some Publisher {
        preferencePublisher
            .dropFirst(2) // 改善進(jìn)入視圖時可能出現(xiàn)的狀態(tài)抖動
            .handleEvents(
                receiveOutput: { _ in
                    self.timestamp = Date() 
                    // 如果 0.15 秒后沒有繼續(xù)收到位置變化的信號,則發(fā)送滾動狀態(tài)停止的信號
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
                        if Date().timeIntervalSince(self.timestamp) > 0.1 {
                            self.timeoutPublisher.send(0)
                        }
                    }
                }
            )
            .merge(with: timeoutPublisher)
    }
    var cancellable: AnyCancellable?
    init() {
        cancellable = publisher
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: { _ in }, receiveValue: { output in
                guard let value = output as? Int else { return }
                if value == 1,!self.isScrolling {
                    self.isScrolling = true
                }
                if value == 0, self.isScrolling {
                    self.isScrolling = false
                }
            })
    }
}
public struct MinValueKey: PreferenceKey {
    public static var defaultValue: CGRect = .zero
    public static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
        value = nextValue()
    }
}
struct ScrollStatusMonitorCommonModifier: ViewModifier {
    @StateObject private var store = CommonStore()
    @Binding var isScrolling: Bool
    func body(content: Content) -> some View {
        content
            .environment(\.isScrolling, store.isScrolling)
            .onChange(of: store.isScrolling) { value in
                isScrolling = value
            }
        // 接收來自子視圖的位置信息
            .onPreferenceChange(MinValueKey.self) { _ in
                store.preferencePublisher.send(1) // 我們不關(guān)心具體的位置信息,只需將其標(biāo)注為滾動中
            }
            .onDisappear {
                store.cancellable = nil
            }
    }
}
// 添加與 ScrollView、List 的子視圖之上,用于在位置發(fā)生變化時發(fā)送信息
func scrollSensor() -> some View {
    overlay(
        GeometryReader { proxy in
            Color.clear
                .preference(
                    key: MinValueKey.self,
                    value: proxy.frame(in: .global)
                )
        }
    )
}

方案三優(yōu)點

  • 支持多平臺( iOS、macOS、macCatalyst )
  • 擁有較好的前后兼容性

方案三缺點

  • 需要為可滾動容器的子視圖添加修飾器
  • 對于 ScrollView + VStack( HStack )這類的組合,只需為可滾動視圖添加一個 scrollSensor 即可。對于 List、ScrollView + LazyVStack( LazyHStack )這類的組合,需要為每個子視圖都添加一個 scrollSensor。
  • 判斷的準(zhǔn)確度沒有前兩種方式高
  • 當(dāng)可滾動組件中的內(nèi)容出現(xiàn)了非滾動引起的尺寸或位置的變化( 例如 List 中某個視圖的尺寸發(fā)生了動態(tài)變化 ),本方式會誤判斷為發(fā)生了滾動,但在視圖的變化結(jié)束后,狀態(tài)會馬上恢復(fù)到滾動結(jié)束
  • 滾動開始后( 狀態(tài)已變化為滾動中 ),保持手指處于按壓狀態(tài)并停止滑動,此方式會將此時視為滾動結(jié)束,而前兩種方式仍會保持滾動中的狀態(tài)直到手指結(jié)束按壓

IsScrolling

我將后兩種解決方案打包做成了一個庫 —— IsScrolling 以方便大家使用。其中 exclusion 對應(yīng)著 Runloop 原理、common 對應(yīng)著 PreferenceKey 解決方案。

使用范例( exclusion ):

struct VStackExclusionDemo: View {
    @State var isScrolling = false
    var body: some View {
        VStack {
            ScrollView {
                VStack {
                    ForEach(0..<100) { i in
                        CellView(index: i) // no need to add sensor in exclusion mode
                    }
                }
            }
            .scrollStatusMonitor($isScrolling, monitorMode: .exclusion) // add scrollStatusMonitor to get scroll status
        }
    }
}

使用范例( common ):

struct ListCommonDemo: View {
    @State var isScrolling = false
    var body: some View {
        VStack {
            List {
                ForEach(0..<100) { i in
                    CellView(index: i)
                        .scrollSensor() // Need to add sensor for each subview
                }
            }
            .scrollStatusMonitor($isScrolling, monitorMode: .common)
        }
    }
}

總結(jié)

SwiftUI 仍在高速進(jìn)化中,很多積極的變化并不會立即體現(xiàn)出來。待 SwiftUI 更多的底層實現(xiàn)不再依賴 UIKit( AppKit )之時,才會是它 API 的爆發(fā)期。

以上就是判斷 ScrollView、List 是否正在滾動的詳細(xì)內(nèi)容,更多關(guān)于ScrollView List 滾動判斷的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 用SwiftUI實現(xiàn)3D Scroll滾動效果的實現(xiàn)代碼

    用SwiftUI實現(xiàn)3D Scroll滾動效果的實現(xiàn)代碼

    這篇文章主要介紹了用SwiftUI實現(xiàn)3D Scroll效果的實現(xiàn)代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)
    2020-04-04
  • Swift實現(xiàn)簡易計算器功能

    Swift實現(xiàn)簡易計算器功能

    這篇文章主要為大家詳細(xì)介紹了Swift實現(xiàn)簡易計算器功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-01-01
  • Swift map和filter函數(shù)原型基礎(chǔ)示例

    Swift map和filter函數(shù)原型基礎(chǔ)示例

    這篇文章主要為大家介紹了Swift map和filter函數(shù)原型基礎(chǔ)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-07-07
  • Swift 4最全的新特性詳細(xì)解析(推薦)

    Swift 4最全的新特性詳細(xì)解析(推薦)

    Swift 4 在 Swift 3 的基礎(chǔ)上,提供了更強大的穩(wěn)健性和穩(wěn)定性。所以下面這篇文章就來給大家總結(jié)介紹關(guān)于Swift4新特性的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧。
    2017-11-11
  • 在Swift中如何使用正則表達(dá)式詳解

    在Swift中如何使用正則表達(dá)式詳解

    正則表達(dá)式是對字符串操作的一種邏輯公式,相信大家應(yīng)該都不陌生,下面這篇文章主要給大家介紹了關(guān)于在Swift中如何使用正則表達(dá)式的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2018-09-09
  • RxSwift發(fā)送及訂閱 Subjects、Variables代碼示例

    RxSwift發(fā)送及訂閱 Subjects、Variables代碼示例

    這篇文章主要介紹了RxSwift發(fā)送及訂閱 Subjects、Variables代碼示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-12-12
  • swift中自定義正則表達(dá)式運算符=~詳解

    swift中自定義正則表達(dá)式運算符=~詳解

    這篇文章主要給大家介紹了關(guān)于swift中自定義正則表達(dá)式運算符=~的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。
    2017-12-12
  • swift4更新中所遇到的一些問題總結(jié)

    swift4更新中所遇到的一些問題總結(jié)

    這篇文章主要給大家介紹了關(guān)于在swift4更新中所遇到的一些問題,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。
    2017-12-12
  • Swift泛型Generics淺析講解

    Swift泛型Generics淺析講解

    泛型代碼讓你能根據(jù)你所定義的要求,寫出可以用于任何類型的靈活的、可復(fù)用的函數(shù)。泛型是 Swift 最強大的特性之一,很多 Swift 標(biāo)準(zhǔn)庫是基于泛型代碼構(gòu)建的
    2022-08-08
  • Swift?Error重構(gòu)的基礎(chǔ)示例詳解

    Swift?Error重構(gòu)的基礎(chǔ)示例詳解

    這篇文章主要為大家介紹了Swift?Error基礎(chǔ)錯誤處理的方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-11-11

最新評論