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

判斷?ScrollView List?是否正在滾動(dòng)詳解

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

正文

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

方法一:Introspect

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

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

scrollViewDidScroll(_ scrollView: UIScrollView)

開(kāi)始滾動(dòng)時(shí)調(diào)用此方法

scrollViewDidEndDecelerating(_ scrollView: UIScrollView)

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

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

手指拖動(dòng)結(jié)束后( 手指離開(kāi)時(shí) ),調(diào)用此方法

在 SwiftUI 中,很多的視圖控件是對(duì) UIKit( AppKit )控件的二次包裝。因此,我們可以通過(guò)訪問(wèn)其背后的 UIKit 控件的方式( 使用 Introspect )來(lái)實(shí)現(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
        }
    }
    // 手指緩慢拖動(dòng)可滾動(dòng)控件,手指離開(kāi)后,decelerate 為 false,因此并不會(huì)調(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
            }
            // 同時(shí)支持 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)點(diǎn)

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

方案一缺點(diǎn)

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

方法二:Runloop

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

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

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

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

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

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

final class ExclusionStore: ObservableObject {
    @Published var isScrolling = false
    // 當(dāng) Runloop 處于 default( kCFRunLoopDefaultMode )模式時(shí),每隔 0.1 秒會(huì)發(fā)送一個(gè)時(shí)間信號(hào)
    private let idlePublisher = Timer.publish(every: 0.1, on: .main, in: .default).autoconnect()
    // 當(dāng) Runloop 處于 tracking( UITrackingRunLoopMode )模式時(shí),每隔 0.1 秒會(huì)發(fā)送一個(gè)時(shí)間信號(hào)
    private let scrollingPublisher = Timer.publish(every: 0.1, on: .main, in: .tracking).autoconnect()
    private var publisher: some Publisher {
        scrollingPublisher
            .map { _ in 1 } // 滾動(dòng)時(shí),發(fā)送 1
            .merge(with:
                idlePublisher
                    .map { _ in 0 } // 不滾動(dòng)時(shí),發(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)點(diǎn)

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

方案二缺點(diǎn)

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

方法三:PreferenceKey

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

在 ScrollView、List 發(fā)生滾動(dòng)時(shí),它們內(nèi)部的子視圖的位置也將發(fā)生改變。我們將以是否可以持續(xù)接收到它們的位置信息為依據(jù)判斷當(dāng)前是否處于滾動(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)入視圖時(shí)可能出現(xiàn)的狀態(tài)抖動(dòng)
            .handleEvents(
                receiveOutput: { _ in
                    self.timestamp = Date() 
                    // 如果 0.15 秒后沒(méi)有繼續(xù)收到位置變化的信號(hào),則發(fā)送滾動(dòng)狀態(tài)停止的信號(hào)
                    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
            }
        // 接收來(lái)自子視圖的位置信息
            .onPreferenceChange(MinValueKey.self) { _ in
                store.preferencePublisher.send(1) // 我們不關(guān)心具體的位置信息,只需將其標(biāo)注為滾動(dòng)中
            }
            .onDisappear {
                store.cancellable = nil
            }
    }
}
// 添加與 ScrollView、List 的子視圖之上,用于在位置發(fā)生變化時(shí)發(fā)送信息
func scrollSensor() -> some View {
    overlay(
        GeometryReader { proxy in
            Color.clear
                .preference(
                    key: MinValueKey.self,
                    value: proxy.frame(in: .global)
                )
        }
    )
}

方案三優(yōu)點(diǎn)

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

方案三缺點(diǎn)

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

IsScrolling

我將后兩種解決方案打包做成了一個(gè)庫(kù) —— IsScrolling 以方便大家使用。其中 exclusion 對(duì)應(yīng)著 Runloop 原理、common 對(duì)應(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)化中,很多積極的變化并不會(huì)立即體現(xiàn)出來(lái)。待 SwiftUI 更多的底層實(shí)現(xiàn)不再依賴 UIKit( AppKit )之時(shí),才會(huì)是它 API 的爆發(fā)期。

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

相關(guān)文章

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

    用SwiftUI實(shí)現(xiàn)3D Scroll滾動(dòng)效果的實(shí)現(xiàn)代碼

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

    Swift實(shí)現(xiàn)簡(jiǎn)易計(jì)算器功能

    這篇文章主要為大家詳細(xì)介紹了Swift實(shí)現(xiàn)簡(jiǎn)易計(jì)算器功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    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ǔ)上,提供了更強(qiáng)大的穩(wěn)健性和穩(wěn)定性。所以下面這篇文章就來(lái)給大家總結(jié)介紹關(guān)于Swift4新特性的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧。
    2017-11-11
  • 在Swift中如何使用正則表達(dá)式詳解

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

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

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

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

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

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

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

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

    Swift泛型Generics淺析講解

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

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

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

最新評(píng)論