最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

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

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

正文

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

方法一:Introspect

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

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

scrollViewDidScroll(_ scrollView: UIScrollView)

開始滾動時調用此方法

scrollViewDidEndDecelerating(_ scrollView: UIScrollView)

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

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

手指拖動結束后( 手指離開時 ),調用此方法

在 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,因此并不會調用 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
            }
    }
}

調用方法:

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)點

  • 準確
  • 及時
  • 系統(tǒng)負擔小

方案一缺點

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

方法二:Runloop

我第一次接觸 Runloop 是在學習 Combine 的時候,直到我碰到 Timer 的閉包并沒有按照預期被調用時才對其進行了一定的了解

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

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

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

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

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

final class ExclusionStore: ObservableObject {
    @Published var isScrolling = false
    // 當 Runloop 處于 default( kCFRunLoopDefaultMode )模式時,每隔 0.1 秒會發(fā)送一個時間信號
    private let idlePublisher = Timer.publish(every: 0.1, on: .main, in: .default).autoconnect()
    // 當 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 方式幾乎一致的準確性和及時性
  • 實現(xiàn)的邏輯非常簡單

方案二缺點

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

方法三:PreferenceKey

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

在 ScrollView、List 發(fā)生滾動時,它們內(nèi)部的子視圖的位置也將發(fā)生改變。我們將以是否可以持續(xù)接收到它們的位置信息為依據(jù)判斷當前是否處于滾動狀態(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) // 改善進入視圖時可能出現(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) // 我們不關心具體的位置信息,只需將其標注為滾動中
            }
            .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。
  • 判斷的準確度沒有前兩種方式高
  • 當可滾動組件中的內(nèi)容出現(xiàn)了非滾動引起的尺寸或位置的變化( 例如 List 中某個視圖的尺寸發(fā)生了動態(tài)變化 ),本方式會誤判斷為發(fā)生了滾動,但在視圖的變化結束后,狀態(tài)會馬上恢復到滾動結束
  • 滾動開始后( 狀態(tài)已變化為滾動中 ),保持手指處于按壓狀態(tài)并停止滑動,此方式會將此時視為滾動結束,而前兩種方式仍會保持滾動中的狀態(tài)直到手指結束按壓

IsScrolling

我將后兩種解決方案打包做成了一個庫 —— IsScrolling 以方便大家使用。其中 exclusion 對應著 Runloop 原理、common 對應著 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)
        }
    }
}

總結

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

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

相關文章

  • Swift實現(xiàn)JSON轉Model的方法及HandyJSON使用講解

    Swift實現(xiàn)JSON轉Model的方法及HandyJSON使用講解

    這篇文章給大家介紹了Swift實現(xiàn)JSON轉Model的方法及HandyJSON使用講解,非常不錯,具有參考借鑒價值,需要的朋友參考下吧
    2017-07-07
  • Combine中錯誤處理和Scheduler使用詳解

    Combine中錯誤處理和Scheduler使用詳解

    這篇文章主要為大家介紹了Combine中錯誤處理和Scheduler使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-12-12
  • Swift 使用 Observe 監(jiān)測頁面滾動的實現(xiàn)方法

    Swift 使用 Observe 監(jiān)測頁面滾動的實現(xiàn)方法

    這篇文章主要介紹了Swift 使用 Observe 監(jiān)測頁面滾動的實現(xiàn)方法,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-05-05
  • Swift data范圍截取問題解決方案

    Swift data范圍截取問題解決方案

    這篇文章主要介紹了Swift data范圍截取問題解決方案,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-09-09
  • Ubuntu 16.04上安裝 Swift 3.0及問題解答

    Ubuntu 16.04上安裝 Swift 3.0及問題解答

    本文給大家分享的是在Ubuntu系統(tǒng)中安裝 Swift 3.0的方法和步驟,以及安裝過程中有可能遇到的問題的解答,這里推薦給小伙伴們,希望大家能夠喜歡
    2016-07-07
  • 本地推送通知UserNotifications在Swift中的實現(xiàn)方式

    本地推送通知UserNotifications在Swift中的實現(xiàn)方式

    這篇文章主要介紹了本地推送通知UserNotifications在Swift中的實現(xiàn)方式,想了解消息推送的同學,一定要看一下
    2021-04-04
  • Swift Set集合及常用方法詳解總結

    Swift Set集合及常用方法詳解總結

    Set集合為集類型,集是最簡單的一種集合,存放于集中的對象不按特定方式排序,只是簡單地把對象加入集合中,類似于向口袋里放東西,對集中存在的對象的訪問和操作是通過對象的引用進行的,因此在集中不能存放重復對象
    2021-11-11
  • 詳解Swift的內(nèi)存管理

    詳解Swift的內(nèi)存管理

    這篇文章主要介紹了Swift的內(nèi)存管理,對內(nèi)存管理感興趣的同學,可以參考下
    2021-04-04
  • LeetCode?刷題?Swift?兩個數(shù)組的交集

    LeetCode?刷題?Swift?兩個數(shù)組的交集

    這篇文章主要為大家介紹了LeetCode?刷題?Swift?兩個數(shù)組的交集示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-09-09
  • Swift UILable 設置內(nèi)邊距實例代碼

    Swift UILable 設置內(nèi)邊距實例代碼

    本文主要介紹Swift UILable 設置內(nèi)邊距,這里提供示例代碼供大家參考,有需要的小伙伴可以看下
    2016-07-07

最新評論

千阳县| 大厂| 鄂托克旗| 名山县| 内丘县| 渭南市| 大新县| 科技| 天津市| 霍山县| 柘城县| 康乐县| 酉阳| 射洪县| 清流县| 竹北市| 兰考县| 汾阳市| 曲阜市| 高淳县| 沙田区| 正阳县| 亚东县| 嵊州市| 武威市| 建阳市| 蒲江县| 清水县| 布拖县| 卢氏县| 永顺县| 扶风县| 天全县| 洛宁县| 关岭| 丹寨县| 华阴市| 顺义区| 黎平县| 博兴县| 泸西县|