├── Demo ├── Demo │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── DemoApp.swift │ ├── Demo.entitlements │ ├── MultiScrollableComponentsDemo.swift │ ├── ScrollInAnyDirectionDemo.swift │ ├── ListDemo.swift │ ├── VStackDemo.swift │ ├── LazyVStackDemo.swift │ ├── ContentView.swift │ └── HStackDemo.swift └── Demo.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── project.pbxproj ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Tests └── IsScrollingTests │ └── IsScrollingTests.swift ├── Sources └── IsScrolling │ ├── Key.swift │ └── IsScrolling.swift ├── LICENSE ├── Package.swift ├── READMECN.md └── README.md /Demo/Demo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo/Demo/DemoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoApp.swift 3 | // Demo 4 | // 5 | // Created by Yang Xu on 2022/9/7. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct DemoApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/Demo/Demo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Tests/IsScrollingTests/IsScrollingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import IsScrolling 3 | 4 | final class IsScrollingTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo/Demo/MultiScrollableComponentsDemo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultiMonitor.swift 3 | // Demo 4 | // 5 | // Created by Yang Xu on 2022/9/7. 6 | // 7 | 8 | import Foundation 9 | import IsScrolling 10 | import SwiftUI 11 | 12 | struct MultiScrollableComponentsDemo: View { 13 | @State var isScrolling1 = false 14 | @State var isScrolling2 = false 15 | var body: some View { 16 | VStack(spacing:30) { 17 | HStackCommonDemo() 18 | VStackCommonDemo() 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/IsScrolling/Key.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Key.swift 3 | // 4 | // 5 | // Created by Yang Xu on 2022/9/7. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct IsScrollingValueKey: EnvironmentKey { 12 | static var defaultValue = false 13 | } 14 | 15 | public extension EnvironmentValues { 16 | var isScrolling: Bool { 17 | get { self[IsScrollingValueKey.self] } 18 | set { self[IsScrollingValueKey.self] = newValue } 19 | } 20 | } 21 | 22 | public struct MinValueKey: PreferenceKey { 23 | public static var defaultValue: CGRect = .zero 24 | public static func reduce(value: inout CGRect, nextValue: () -> CGRect) { 25 | value = nextValue() 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Demo/Demo/ScrollInAnyDirectionDemo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollInAnyDirectionDemo.swift 3 | // Demo 4 | // 5 | // Created by Yang Xu on 2022/9/11. 6 | // 7 | 8 | import Foundation 9 | import IsScrolling 10 | import SwiftUI 11 | 12 | struct ScrollInAnyDirectionDemo: View { 13 | @State var isScrolling = false 14 | var body: some View { 15 | ScrollView([.horizontal, .vertical]) { 16 | Rectangle() 17 | .fill(LinearGradient(colors: [.red, .orange, .yellow, .pink, .cyan, .blue], startPoint: .topLeading, endPoint: .bottomTrailing)) 18 | .frame(width: 3000, height: 3000) 19 | .scrollSensor() 20 | } 21 | .scrollStatusMonitor($isScrolling, monitorMode: .common) 22 | .overlay( 23 | Text("Scrolling: \(isScrolling ? "True" : "False")") 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 东坡肘子( fatbobman ) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "IsScrolling", 8 | platforms: [ 9 | .iOS(.v14), 10 | .macOS(.v12), 11 | .macCatalyst(.v14), 12 | ], 13 | products: [ 14 | // Products define the executables and libraries a package produces, and make them visible to other packages. 15 | .library( 16 | name: "IsScrolling", 17 | targets: ["IsScrolling"]), 18 | ], 19 | dependencies: [ 20 | // Dependencies declare other packages that this package depends on. 21 | // .package(url: /* package url */, from: "1.0.0"), 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 25 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 26 | .target( 27 | name: "IsScrolling", 28 | dependencies: []), 29 | .testTarget( 30 | name: "IsScrollingTests", 31 | dependencies: ["IsScrolling"]), 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /Demo/Demo/ListDemo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListDemo.swift 3 | // Demo 4 | // 5 | // Created by Yang Xu on 2022/9/7. 6 | // 7 | 8 | import Foundation 9 | import IsScrolling 10 | import SwiftUI 11 | 12 | #if !os(macOS) && !targetEnvironment(macCatalyst) 13 | struct ListExclusionDemo: View { 14 | @State var isScrolling = false 15 | var body: some View { 16 | VStack { 17 | List { 18 | ForEach(0..<100) { i in 19 | CellView(index: i) 20 | } 21 | } 22 | .scrollStatusMonitor($isScrolling, monitorMode: .exclusion) 23 | .safeAreaInset(edge: .bottom) { 24 | Text("Scrolling : \(isScrolling ? "True" : "False")") 25 | .font(.headline) 26 | .foregroundColor(.blue) 27 | .padding() 28 | .frame(maxWidth: .infinity) 29 | .background(.regularMaterial) 30 | } 31 | } 32 | } 33 | } 34 | #endif 35 | 36 | struct ListCommonDemo: View { 37 | @State var isScrolling = false 38 | var body: some View { 39 | VStack { 40 | List { 41 | ForEach(0..<100) { i in 42 | CellView(index: i) 43 | .scrollSensor() // Need to add sensor for each subview 44 | } 45 | } 46 | .scrollStatusMonitor($isScrolling, monitorMode: .common) 47 | .safeAreaInset(edge: .bottom) { 48 | Text("Scrolling : \(isScrolling ? "True" : "False")") 49 | .font(.headline) 50 | .foregroundColor(.blue) 51 | .padding() 52 | .frame(maxWidth: .infinity) 53 | .background(.regularMaterial) 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "2x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "83.5x83.5" 82 | }, 83 | { 84 | "idiom" : "ios-marketing", 85 | "scale" : "1x", 86 | "size" : "1024x1024" 87 | } 88 | ], 89 | "info" : { 90 | "author" : "xcode", 91 | "version" : 1 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Demo/Demo/VStackDemo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VStackDemo.swift 3 | // Demo 4 | // 5 | // Created by Yang Xu on 2022/9/7. 6 | // 7 | 8 | import Foundation 9 | import IsScrolling 10 | import SwiftUI 11 | 12 | #if !os(macOS) && !targetEnvironment(macCatalyst) 13 | struct VStackExclusionDemo: View { 14 | @State var isScrolling = false 15 | var body: some View { 16 | VStack { 17 | ScrollView { 18 | VStack { 19 | ForEach(0..<100) { i in 20 | CellView(index: i) 21 | } 22 | } 23 | } 24 | .scrollStatusMonitor($isScrolling, monitorMode: .exclusion) 25 | .safeAreaInset(edge: .bottom) { 26 | Text("Scrolling : \(isScrolling ? "True" : "False")") 27 | .font(.headline) 28 | .foregroundColor(.blue) 29 | .padding() 30 | .frame(maxWidth: .infinity) 31 | .background(.regularMaterial) 32 | } 33 | } 34 | } 35 | } 36 | #endif 37 | 38 | struct VStackCommonDemo: View { 39 | @State var isScrolling = false 40 | var body: some View { 41 | VStack { 42 | ScrollView { 43 | VStack { 44 | ForEach(0..<100) { i in 45 | CellView(index: i) 46 | } 47 | } 48 | .scrollSensor() // only need one sensor for whole content in ScrollView 49 | } 50 | .scrollStatusMonitor($isScrolling, monitorMode: .common) 51 | .safeAreaInset(edge: .bottom) { 52 | Text("Scrolling : \(isScrolling ? "True" : "False")") 53 | .font(.headline) 54 | .foregroundColor(.blue) 55 | .padding() 56 | .frame(maxWidth: .infinity) 57 | .background(.regularMaterial) 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Demo/Demo/LazyVStackDemo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LazyVStackDemo.swift 3 | // Demo 4 | // 5 | // Created by Yang Xu on 2022/9/7. 6 | // 7 | 8 | import Foundation 9 | import IsScrolling 10 | import SwiftUI 11 | 12 | #if !os(macOS) && !targetEnvironment(macCatalyst) 13 | struct LazyVStackExclusionDemo: View { 14 | @State var isScrolling = false 15 | var body: some View { 16 | VStack { 17 | ScrollView { 18 | LazyVStack { 19 | ForEach(0..<100) { i in 20 | CellView(index: i) 21 | } 22 | } 23 | } 24 | .scrollStatusMonitor($isScrolling, monitorMode: .exclusion) 25 | .safeAreaInset(edge: .bottom) { 26 | Text("Scrolling : \(isScrolling ? "True" : "False")") 27 | .font(.headline) 28 | .foregroundColor(.blue) 29 | .padding() 30 | .frame(maxWidth: .infinity) 31 | .background(.regularMaterial) 32 | } 33 | } 34 | } 35 | } 36 | #endif 37 | 38 | struct LazyVStackCommonDemo: View { 39 | @State var isScrolling = false 40 | var body: some View { 41 | VStack { 42 | ScrollView { 43 | LazyVStack { 44 | ForEach(0..<100) { i in 45 | CellView(index: i) 46 | .scrollSensor() // Need to add sensor for each subview 47 | } 48 | } 49 | } 50 | .scrollStatusMonitor($isScrolling, monitorMode: .common) 51 | .safeAreaInset(edge: .bottom) { 52 | Text("Scrolling : \(isScrolling ? "True" : "False")") 53 | .font(.headline) 54 | .foregroundColor(.blue) 55 | .padding() 56 | .frame(maxWidth: .infinity) 57 | .background(.regularMaterial) 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Demo/Demo/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Demo 4 | // 5 | // Created by Yang Xu on 2022/9/7. 6 | // 7 | 8 | import IsScrolling 9 | import SwiftUI 10 | 11 | struct ContentView: View { 12 | @State var selection = 0 13 | var body: some View { 14 | NavigationView { 15 | List { 16 | #if !os(macOS) && !targetEnvironment(macCatalyst) 17 | NavigationLink("VStack - Exclusion") { VStackExclusionDemo() } 18 | #endif 19 | 20 | NavigationLink("VStack - Common") { VStackCommonDemo() } 21 | #if !os(macOS) && !targetEnvironment(macCatalyst) 22 | NavigationLink("LazyVStack - Exclusion") { LazyVStackExclusionDemo() } 23 | #endif 24 | 25 | NavigationLink("LazyVStack - Common") { LazyVStackCommonDemo() } 26 | 27 | #if !os(macOS) && !targetEnvironment(macCatalyst) 28 | NavigationLink("List - Exclusion") { ListExclusionDemo() } 29 | #endif 30 | 31 | NavigationLink("List - Common") { ListCommonDemo() } 32 | 33 | #if !os(macOS) && !targetEnvironment(macCatalyst) 34 | NavigationLink("HStack - Exclusion") { HStackExclusionDemo() } 35 | #endif 36 | 37 | NavigationLink("HStack - Common") { HStackCommonDemo() } 38 | NavigationLink("MultiMonitor - Common") { MultiScrollableComponentsDemo() } 39 | NavigationLink("Scroll In Any Direction") { 40 | ScrollInAnyDirectionDemo() 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | struct ContentView_Previews: PreviewProvider { 48 | static var previews: some View { 49 | ContentView() 50 | } 51 | } 52 | 53 | let colors = [Color.red, .cyan, .yellow, .orange, .orange, .blue, .brown, .indigo] 54 | 55 | struct CellView: View { 56 | @Environment(\.isScrolling) var isScrolling 57 | let index: Int 58 | var body: some View { 59 | Rectangle() 60 | .fill(colors[index % colors.count].opacity(0.6)) 61 | .frame(maxWidth: .infinity, minHeight: 80) 62 | .overlay(Text("ID: \(index) Scrolling: \(isScrolling ? "T" : "F")")) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Demo/Demo/HStackDemo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HStackDemo.swift 3 | // Demo 4 | // 5 | // Created by Yang Xu on 2022/9/7. 6 | // 7 | 8 | import Foundation 9 | import IsScrolling 10 | import SwiftUI 11 | 12 | #if !os(macOS) && !targetEnvironment(macCatalyst) 13 | struct HStackExclusionDemo: View { 14 | @State var isScrolling = false 15 | var body: some View { 16 | VStack { 17 | ScrollView(.horizontal) { 18 | HStack { 19 | ForEach(0..<100) { i in 20 | HCellView(index: i) 21 | } 22 | } 23 | } 24 | .scrollStatusMonitor($isScrolling, monitorMode: .exclusion) 25 | Text("Scrolling : \(isScrolling ? "True" : "False")") 26 | .font(.headline) 27 | .foregroundColor(.blue) 28 | .padding() 29 | .frame(maxWidth: .infinity) 30 | .background(.regularMaterial) 31 | } 32 | } 33 | } 34 | #endif 35 | 36 | struct HStackCommonDemo: View { 37 | @State var isScrolling = false 38 | var body: some View { 39 | VStack { 40 | ScrollView(.horizontal) { 41 | HStack { 42 | ForEach(0..<100) { i in 43 | HCellView(index: i) 44 | } 45 | } 46 | .scrollSensor() // change axis to horizontal 47 | } 48 | .scrollStatusMonitor($isScrolling, monitorMode: .common) 49 | 50 | Text("Scrolling : \(isScrolling ? "True" : "False")") 51 | .font(.headline) 52 | .foregroundColor(.blue) 53 | .padding() 54 | .frame(maxWidth: .infinity) 55 | .background(.regularMaterial) 56 | } 57 | } 58 | } 59 | 60 | struct HCellView: View { 61 | @Environment(\.isScrolling) private var isScrolling 62 | let index: Int 63 | let showText: Bool 64 | 65 | init(index: Int, showText: Bool = true) { 66 | self.index = index 67 | self.showText = showText 68 | } 69 | 70 | var body: some View { 71 | Rectangle() 72 | .fill(colors[index % colors.count].opacity(0.6)) 73 | .frame(width: 100, height: 200) 74 | .overlay( 75 | VStack { 76 | if showText { 77 | Text("ID: \(index)") 78 | Text("Scrolling: \(isScrolling ? "T" : "F")") 79 | } 80 | } 81 | ) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /READMECN.md: -------------------------------------------------------------------------------- 1 | # IsScrolling 2 | 3 | ![](https://img.shields.io/badge/Platform%20Compatibility-iOS%20|%20macOS%20|%20macCatalyst-red) 4 | 5 | 正如名称所示,IsScrolling 提供了一个 ViewModifier ,用来获取 SwiftUI 中 ScrollView 或 List 当前的滚动状态。由于完全采用了 SwiftUI 原生的方式实现此功能,因此 IsScrolling 具备了很好的前后兼容性。 6 | 7 | ## 动机 8 | 9 | 在两年前开发 [SwipeCell](https://github.com/fatbobman/SwipeCell) 的时候,我需要在可滚动组件( ScrollView、List )开始滚动时,关闭已经打开的侧滑菜单。当时是通过 [Introspect](https://github.com/siteline/SwiftUI-Introspect.git) 为可滚动组件注入了一个 Delegate 才达到此目的,一直打算用更加原生的方式替代此方案。 10 | 11 | ## 使用方法 12 | 13 | IsScrolling 拥有两种模式,它们分别基于了不同的实现原理: 14 | 15 | * exclusion 16 | 17 | 仅支持 iOS ,无需为滚动视图添加 sensor ,屏幕中仅能有一个可滚动组件 18 | 19 | ```swift 20 | struct VStackExclusionDemo: View { 21 | @State var isScrolling = false 22 | var body: some View { 23 | VStack { 24 | ScrollView { 25 | VStack { 26 | ForEach(0..<100) { i in 27 | CellView(index: i) // no need to add sensor in exclusion mode 28 | } 29 | } 30 | } 31 | .scrollStatusMonitor($isScrolling, monitorMode: .exclusion) // add scrollStatusMonitor to get scroll status 32 | } 33 | } 34 | } 35 | 36 | struct CellView: View { 37 | @Environment(\.isScrolling) var isScrolling // can get scroll status in scrollable content 38 | let index: Int 39 | var body: some View { 40 | Rectangle() 41 | .fill(colors[index % colors.count].opacity(0.6)) 42 | .frame(maxWidth: .infinity, minHeight: 80) 43 | .overlay(Text("ID: \(index) Scrolling: \(isScrolling ? "T" : "F")")) 44 | } 45 | } 46 | ``` 47 | 48 | * common 49 | 50 | 适用于全部平台,可同时监控屏幕中的多个可滚动组件,需要为视图添加 sensor 51 | 52 | ```swift 53 | struct ListCommonDemo: View { 54 | @State var isScrolling = false 55 | var body: some View { 56 | VStack { 57 | List { 58 | ForEach(0..<100) { i in 59 | CellView(index: i) 60 | .scrollSensor() // Need to add sensor for each subview 61 | } 62 | } 63 | .scrollStatusMonitor($isScrolling, monitorMode: .common) 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | 对于 ScrollView + VStack( HStack )这类的组合,只需为可滚动视图添加一个 scrollSensor 即可。对于 List、ScrollView + LazyVStack( LazyHStack )这类的组合,需要为每个子视图都添加一个 scrollSensor。 70 | 71 | 详细内容,请查看 [Demo](https://github.com/fatbobman/IsScrolling/tree/main/Demo) 72 | 73 | ## 限制与不足 74 | 75 | 无论采用 IsScrolling 提供的哪种监控模式,都无法做到 100% 的准确。毕竟 IsScrolling 是通过某些外在的现象来推断可滚动组件的当前滚动状态。已知的问题有: 76 | 77 | * 当滚动内容处于容器的顶部或底部且处于回弹状态时,此时点击停止滚动,再松手,可能会出现滚动状态的扰动( 状态会快速变化一次,此种情况即使使用 UIScrollViewDelegate 也同样存在 ) 78 | * 当可滚动组件中的内容出现了非滚动引起的尺寸或位置的变化( 例如 List 中某个视图的尺寸发生了动态变化 ),IsScrolling 在 common 模式下可能会误判断为发生了滚动,但在视图的变化结束后,状态会马上恢复到滚动结束 79 | * 滚动开始后( 状态已变化为滚动中 ),保持手指处于按压状态并停止滑动,common 模式会将此时视为滚动结束,exclusion 模式仍会保持滚动中的状态直到手指结束按压 80 | 81 | ## 需求 82 | 83 | ``` 84 | .iOS(.v14), 85 | 86 | .macOS(.v12), 87 | 88 | .macCatalyst(.v14), 89 | ``` 90 | 91 | ## 安装 92 | 93 | ``` 94 | dependencies: [ 95 | .package(url: "https://github.com/fatbobman/IsScrolling.git", from: "1.0.0") 96 | ] 97 | ``` 98 | 99 | ## 版权 100 | 101 | This library is released under the MIT license. See [LICENSE](https://github.com/fatbobman/IsScrolling/blob/main/LICENSE) for details. 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IsScrolling 2 | 3 | ![](https://img.shields.io/badge/Platform%20Compatibility-iOS%20|%20macOS%20|%20macCatalyst-red) 4 | 5 | [中文版说明](https://github.com/fatbobman/IsScrolling/blob/main/READMECN.md) 6 | 7 | As the name suggests, IsScrolling provides a ViewModifier to get the current scrolling state of a ScrollView or List in SwiftUI. IsScrolling has good backward and forward compatibility since it is fully implemented natively in SwiftUI. 8 | 9 | ## Motivation 10 | 11 | When I was developing [SwipeCell](https://github.com/fatbobman/SwipeCell) two years ago, I needed to close the opened side-swipe menu when the scrollable component (ScrollView, List) started scrolling. This was achieved by injecting a Delegate into the scrollable component via [Introspect](https://github.com/siteline/SwiftUI-Introspect.git), and I've been planning to replace this with a more native solution. 12 | 13 | ## Usage 14 | 15 | IsScrolling has two modes, each based on a different implementation principle: 16 | 17 | * exclusion 18 | 19 | Supports iOS only, no need to add sensors to the views of scrollable component, only one scrollable component in the screen 20 | 21 | ```swift 22 | struct VStackExclusionDemo: View { 23 | @State var isScrolling = false 24 | var body: some View { 25 | VStack { 26 | ScrollView { 27 | VStack { 28 | ForEach(0..<100) { i in 29 | CellView(index: i) // no need to add sensor in exclusion mode 30 | } 31 | } 32 | } 33 | .scrollStatusMonitor($isScrolling, monitorMode: .exclusion) // add scrollStatusMonitor to get scroll status 34 | } 35 | } 36 | } 37 | 38 | struct CellView: View { 39 | @Environment(\.isScrolling) var isScrolling // can get scroll status in scrollable content 40 | let index: Int 41 | var body: some View { 42 | Rectangle() 43 | .fill(colors[index % colors.count].opacity(0.6)) 44 | .frame(maxWidth: .infinity, minHeight: 80) 45 | .overlay(Text("ID: \(index) Scrolling: \(isScrolling ? "T" : "F")")) 46 | } 47 | } 48 | ``` 49 | 50 | * common 51 | 52 | Available for all platforms, monitors multiple scrollable components in the screen at the same time, requires sensor to be added to the views of scrollable component. 53 | 54 | ```swift 55 | struct ListCommonDemo: View { 56 | @State var isScrolling = false 57 | var body: some View { 58 | VStack { 59 | List { 60 | ForEach(0..<100) { i in 61 | CellView(index: i) 62 | .scrollSensor() // Need to add sensor for each subview 63 | } 64 | } 65 | .scrollStatusMonitor($isScrolling, monitorMode: .common) 66 | } 67 | } 68 | } 69 | ``` 70 | 71 | For combinations like ScrollView + VStack (HStack), just add one scrollSensor to the scrollable view. For combinations like List, ScrollView + LazyVStack (LazyHStack), you need to add a scrollSensor for each child view. 72 | 73 | For details, please check [Demo](https://github.com/fatbobman/IsScrolling/tree/main/Demo) 74 | 75 | ## Limitations and Shortcomings 76 | 77 | No matter which monitoring mode IsScrolling provides, it cannot be 100% accurate. After all, IsScrolling inferred the current scrolling state of a scrollable component from certain external phenomena. Known issues are. 78 | 79 | * When the scrolling content is at the top or bottom of the container and in a bouncy state, clicking on it to stop scrolling and then releasing it may result in a perturbation of the scrolling state (the state changes rapidly once,This situation also exists even with UIScrollViewDelegate) 80 | * When the content in the scrollable component changes in size or position not caused by scrolling (for example, the size of a view in a List changes dynamically), IsScrolling may mistakenly judge that scrolling has occurred in common mode, but in the view After the change is over, the state will immediately return to the end of the scroll 81 | * After the scrolling starts (the status has changed to scrolling ), stop scrolling, but the finger is still in the pressed state, the common mode will regard this as the end of the scrolling, and the exclusion mode will still keep the scrolling state until the finger ends pressing 82 | 83 | ## Requirements 84 | 85 | ``` 86 | .iOS(.v14), 87 | 88 | .macOS(.v12), 89 | 90 | .macCatalyst(.v14), 91 | ``` 92 | 93 | ## Installation 94 | 95 | ``` 96 | dependencies: [ 97 | .package(url: "https://github.com/fatbobman/IsScrolling.git", from: "1.0.0") 98 | ] 99 | ``` 100 | 101 | ## License 102 | 103 | This library is released under the MIT license. See [LICENSE](https://github.com/fatbobman/IsScrolling/blob/main/LICENSE) for details. 104 | -------------------------------------------------------------------------------- /Sources/IsScrolling/IsScrolling.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollStatusMonitor.swift 3 | // 4 | // 5 | // Created by Yang Xu on 2022/9/7. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import SwiftUI 11 | 12 | public extension View { 13 | @ViewBuilder 14 | func scrollStatusMonitor(_ isScrolling: Binding, monitorMode: ScrollStatusMonitorMode) -> some View { 15 | switch monitorMode { 16 | case .common: 17 | modifier(ScrollStatusMonitorCommonModifier(isScrolling: isScrolling)) 18 | #if !os(macOS) && !targetEnvironment(macCatalyst) 19 | case .exclusion: 20 | modifier(ScrollStatusMonitorExclusionModifier(isScrolling: isScrolling)) 21 | #endif 22 | } 23 | } 24 | 25 | func scrollSensor() -> some View { 26 | overlay( 27 | GeometryReader { proxy in 28 | Color.clear 29 | .preference( 30 | key: MinValueKey.self, 31 | value: proxy.frame(in: .global) 32 | ) 33 | } 34 | ) 35 | } 36 | } 37 | 38 | #if !os(macOS) && !targetEnvironment(macCatalyst) 39 | struct ScrollStatusMonitorExclusionModifier: ViewModifier { 40 | @StateObject private var store = ExclusionStore() 41 | @Binding var isScrolling: Bool 42 | func body(content: Content) -> some View { 43 | content 44 | .environment(\.isScrolling, store.isScrolling) 45 | .onChange(of: store.isScrolling) { value in 46 | isScrolling = value 47 | } 48 | .onAppear { 49 | store.startObserving() 50 | } 51 | .onDisappear { 52 | store.stopObserving() 53 | } 54 | } 55 | } 56 | 57 | final class ExclusionStore: ObservableObject { 58 | @Published var isScrolling = false 59 | 60 | private let idlePublisher = Timer.publish(every: 0.1, on: .main, in: .default).autoconnect() 61 | private let scrollingPublisher = Timer.publish(every: 0.1, on: .main, in: .tracking).autoconnect() 62 | 63 | private var publisher: some Publisher { 64 | scrollingPublisher 65 | .map { _ in 1 } 66 | .merge(with: 67 | idlePublisher 68 | .map { _ in 0 } 69 | ) 70 | } 71 | 72 | var cancellable: AnyCancellable? 73 | 74 | init() { 75 | startObserving() 76 | } 77 | 78 | func startObserving() { 79 | cancellable = publisher 80 | .receive(on: DispatchQueue.main) 81 | .sink(receiveCompletion: { _ in }, receiveValue: { output in 82 | guard let value = output as? Int else { return } 83 | if value == 1,!self.isScrolling { 84 | self.isScrolling = true 85 | } 86 | if value == 0, self.isScrolling { 87 | self.isScrolling = false 88 | } 89 | }) 90 | } 91 | 92 | func stopObserving() { 93 | cancellable = nil 94 | } 95 | } 96 | #endif 97 | 98 | struct ScrollStatusMonitorCommonModifier: ViewModifier { 99 | @StateObject private var store = CommonStore() 100 | @Binding var isScrolling: Bool 101 | func body(content: Content) -> some View { 102 | content 103 | .environment(\.isScrolling, store.isScrolling) 104 | .onChange(of: store.isScrolling) { value in 105 | isScrolling = value 106 | } 107 | .onPreferenceChange(MinValueKey.self) { _ in 108 | store.preferencePublisher.send(1) 109 | } 110 | .onAppear { 111 | store.startObserving() 112 | } 113 | .onDisappear { 114 | store.stopObserving() 115 | } 116 | } 117 | } 118 | 119 | final class CommonStore: ObservableObject { 120 | @Published var isScrolling = false 121 | private var timestamp = Date() 122 | 123 | let preferencePublisher = PassthroughSubject() 124 | let timeoutPublisher = PassthroughSubject() 125 | 126 | private var publisher: some Publisher { 127 | preferencePublisher 128 | .dropFirst(2) 129 | .handleEvents( 130 | receiveOutput: { _ in 131 | // Ensure that when multiple scrolling components are scrolling at the same time, 132 | // the stop state of each can still be obtained individually 133 | self.timestamp = Date() 134 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { 135 | if Date().timeIntervalSince(self.timestamp) > 0.1 { 136 | self.timeoutPublisher.send(0) 137 | } 138 | } 139 | } 140 | ) 141 | .merge(with: timeoutPublisher) 142 | } 143 | 144 | var cancellable: AnyCancellable? 145 | 146 | init() { 147 | startObserving() 148 | } 149 | 150 | func startObserving() { 151 | cancellable = publisher 152 | .receive(on: DispatchQueue.main) 153 | .sink(receiveCompletion: { _ in }, receiveValue: { output in 154 | guard let value = output as? Int else { return } 155 | if value == 1,!self.isScrolling { 156 | self.isScrolling = true 157 | } 158 | if value == 0, self.isScrolling { 159 | self.isScrolling = false 160 | } 161 | }) 162 | } 163 | 164 | func stopObserving() { 165 | cancellable = nil 166 | } 167 | } 168 | 169 | /// Monitoring mode for scroll status 170 | public enum ScrollStatusMonitorMode { 171 | #if !os(macOS) && !targetEnvironment(macCatalyst) 172 | /// The judgment of the start and end of scrolling is more accurate and timely. ( iOS only ) 173 | /// 174 | /// But only for scenarios where there is only one scrollable component in the screen 175 | case exclusion 176 | #endif 177 | /// This mode should be used when there are multiple scrollable parts in the scene. 178 | /// 179 | /// * The accuracy and timeliness are slightly inferior to the exclusion mode. 180 | /// * When using this mode, a **scroll sensor** must be added to the subview of the scroll widget. 181 | case common 182 | } 183 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7625789228C8861400D2F2C9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7625789128C8861400D2F2C9 /* Assets.xcassets */; }; 11 | 7625789528C8861400D2F2C9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7625789428C8861400D2F2C9 /* Preview Assets.xcassets */; }; 12 | 7625789E28C8863000D2F2C9 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7625789B28C8863000D2F2C9 /* ContentView.swift */; }; 13 | 7625789F28C8863000D2F2C9 /* DemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7625789C28C8863000D2F2C9 /* DemoApp.swift */; }; 14 | 762578A028C8863000D2F2C9 /* VStackDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7625789D28C8863000D2F2C9 /* VStackDemo.swift */; }; 15 | 7662DF3728CD893D004D2FC6 /* IsScrolling in Frameworks */ = {isa = PBXBuildFile; productRef = 7662DF3628CD893D004D2FC6 /* IsScrolling */; }; 16 | 7662DF3928CD90D0004D2FC6 /* ScrollInAnyDirectionDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7662DF3828CD90D0004D2FC6 /* ScrollInAnyDirectionDemo.swift */; }; 17 | 76BB8AC528C890F90079BC8C /* LazyVStackDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76BB8AC428C890F90079BC8C /* LazyVStackDemo.swift */; }; 18 | 76BB8AC728C891BE0079BC8C /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76BB8AC628C891BE0079BC8C /* ListDemo.swift */; }; 19 | 76BB8AC928C892850079BC8C /* HStackDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76BB8AC828C892850079BC8C /* HStackDemo.swift */; }; 20 | 76BB8ACB28C893DA0079BC8C /* MultiScrollableComponentsDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76BB8ACA28C893DA0079BC8C /* MultiScrollableComponentsDemo.swift */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXFileReference section */ 24 | 7625788A28C8861300D2F2C9 /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 25 | 7625789128C8861400D2F2C9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 26 | 7625789428C8861400D2F2C9 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 27 | 7625789B28C8863000D2F2C9 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 28 | 7625789C28C8863000D2F2C9 /* DemoApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoApp.swift; sourceTree = ""; }; 29 | 7625789D28C8863000D2F2C9 /* VStackDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VStackDemo.swift; sourceTree = ""; }; 30 | 7662DF3528CD8926004D2FC6 /* IsScrolling */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = IsScrolling; path = ..; sourceTree = ""; }; 31 | 7662DF3828CD90D0004D2FC6 /* ScrollInAnyDirectionDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollInAnyDirectionDemo.swift; sourceTree = ""; }; 32 | 76BB8AC428C890F90079BC8C /* LazyVStackDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyVStackDemo.swift; sourceTree = ""; }; 33 | 76BB8AC628C891BE0079BC8C /* ListDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListDemo.swift; sourceTree = ""; }; 34 | 76BB8AC828C892850079BC8C /* HStackDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HStackDemo.swift; sourceTree = ""; }; 35 | 76BB8ACA28C893DA0079BC8C /* MultiScrollableComponentsDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiScrollableComponentsDemo.swift; sourceTree = ""; }; 36 | 76D8800428C99C1B001A2B07 /* Demo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Demo.entitlements; sourceTree = ""; }; 37 | /* End PBXFileReference section */ 38 | 39 | /* Begin PBXFrameworksBuildPhase section */ 40 | 7625788728C8861300D2F2C9 /* Frameworks */ = { 41 | isa = PBXFrameworksBuildPhase; 42 | buildActionMask = 2147483647; 43 | files = ( 44 | 7662DF3728CD893D004D2FC6 /* IsScrolling in Frameworks */, 45 | ); 46 | runOnlyForDeploymentPostprocessing = 0; 47 | }; 48 | /* End PBXFrameworksBuildPhase section */ 49 | 50 | /* Begin PBXGroup section */ 51 | 7625788128C8861300D2F2C9 = { 52 | isa = PBXGroup; 53 | children = ( 54 | 762578A128C8864300D2F2C9 /* Packages */, 55 | 7625788C28C8861300D2F2C9 /* Demo */, 56 | 7625788B28C8861300D2F2C9 /* Products */, 57 | 762578A328C8867700D2F2C9 /* Frameworks */, 58 | ); 59 | sourceTree = ""; 60 | }; 61 | 7625788B28C8861300D2F2C9 /* Products */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | 7625788A28C8861300D2F2C9 /* Demo.app */, 65 | ); 66 | name = Products; 67 | sourceTree = ""; 68 | }; 69 | 7625788C28C8861300D2F2C9 /* Demo */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | 76D8800428C99C1B001A2B07 /* Demo.entitlements */, 73 | 7625789B28C8863000D2F2C9 /* ContentView.swift */, 74 | 76BB8AC628C891BE0079BC8C /* ListDemo.swift */, 75 | 7625789C28C8863000D2F2C9 /* DemoApp.swift */, 76 | 7625789D28C8863000D2F2C9 /* VStackDemo.swift */, 77 | 76BB8AC428C890F90079BC8C /* LazyVStackDemo.swift */, 78 | 76BB8AC828C892850079BC8C /* HStackDemo.swift */, 79 | 7662DF3828CD90D0004D2FC6 /* ScrollInAnyDirectionDemo.swift */, 80 | 76BB8ACA28C893DA0079BC8C /* MultiScrollableComponentsDemo.swift */, 81 | 7625789128C8861400D2F2C9 /* Assets.xcassets */, 82 | 7625789328C8861400D2F2C9 /* Preview Content */, 83 | ); 84 | path = Demo; 85 | sourceTree = ""; 86 | }; 87 | 7625789328C8861400D2F2C9 /* Preview Content */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | 7625789428C8861400D2F2C9 /* Preview Assets.xcassets */, 91 | ); 92 | path = "Preview Content"; 93 | sourceTree = ""; 94 | }; 95 | 762578A128C8864300D2F2C9 /* Packages */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | 7662DF3528CD8926004D2FC6 /* IsScrolling */, 99 | ); 100 | name = Packages; 101 | sourceTree = ""; 102 | }; 103 | 762578A328C8867700D2F2C9 /* Frameworks */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | ); 107 | name = Frameworks; 108 | sourceTree = ""; 109 | }; 110 | /* End PBXGroup section */ 111 | 112 | /* Begin PBXNativeTarget section */ 113 | 7625788928C8861300D2F2C9 /* Demo */ = { 114 | isa = PBXNativeTarget; 115 | buildConfigurationList = 7625789828C8861400D2F2C9 /* Build configuration list for PBXNativeTarget "Demo" */; 116 | buildPhases = ( 117 | 7625788628C8861300D2F2C9 /* Sources */, 118 | 7625788728C8861300D2F2C9 /* Frameworks */, 119 | 7625788828C8861300D2F2C9 /* Resources */, 120 | ); 121 | buildRules = ( 122 | ); 123 | dependencies = ( 124 | ); 125 | name = Demo; 126 | packageProductDependencies = ( 127 | 7662DF3628CD893D004D2FC6 /* IsScrolling */, 128 | ); 129 | productName = Demo; 130 | productReference = 7625788A28C8861300D2F2C9 /* Demo.app */; 131 | productType = "com.apple.product-type.application"; 132 | }; 133 | /* End PBXNativeTarget section */ 134 | 135 | /* Begin PBXProject section */ 136 | 7625788228C8861300D2F2C9 /* Project object */ = { 137 | isa = PBXProject; 138 | attributes = { 139 | BuildIndependentTargetsInParallel = 1; 140 | LastSwiftUpdateCheck = 1340; 141 | LastUpgradeCheck = 1340; 142 | TargetAttributes = { 143 | 7625788928C8861300D2F2C9 = { 144 | CreatedOnToolsVersion = 13.4.1; 145 | LastSwiftMigration = 1340; 146 | }; 147 | }; 148 | }; 149 | buildConfigurationList = 7625788528C8861300D2F2C9 /* Build configuration list for PBXProject "Demo" */; 150 | compatibilityVersion = "Xcode 13.0"; 151 | developmentRegion = en; 152 | hasScannedForEncodings = 0; 153 | knownRegions = ( 154 | en, 155 | Base, 156 | ); 157 | mainGroup = 7625788128C8861300D2F2C9; 158 | productRefGroup = 7625788B28C8861300D2F2C9 /* Products */; 159 | projectDirPath = ""; 160 | projectRoot = ""; 161 | targets = ( 162 | 7625788928C8861300D2F2C9 /* Demo */, 163 | ); 164 | }; 165 | /* End PBXProject section */ 166 | 167 | /* Begin PBXResourcesBuildPhase section */ 168 | 7625788828C8861300D2F2C9 /* Resources */ = { 169 | isa = PBXResourcesBuildPhase; 170 | buildActionMask = 2147483647; 171 | files = ( 172 | 7625789528C8861400D2F2C9 /* Preview Assets.xcassets in Resources */, 173 | 7625789228C8861400D2F2C9 /* Assets.xcassets in Resources */, 174 | ); 175 | runOnlyForDeploymentPostprocessing = 0; 176 | }; 177 | /* End PBXResourcesBuildPhase section */ 178 | 179 | /* Begin PBXSourcesBuildPhase section */ 180 | 7625788628C8861300D2F2C9 /* Sources */ = { 181 | isa = PBXSourcesBuildPhase; 182 | buildActionMask = 2147483647; 183 | files = ( 184 | 76BB8AC928C892850079BC8C /* HStackDemo.swift in Sources */, 185 | 76BB8AC528C890F90079BC8C /* LazyVStackDemo.swift in Sources */, 186 | 76BB8ACB28C893DA0079BC8C /* MultiScrollableComponentsDemo.swift in Sources */, 187 | 762578A028C8863000D2F2C9 /* VStackDemo.swift in Sources */, 188 | 7625789F28C8863000D2F2C9 /* DemoApp.swift in Sources */, 189 | 7662DF3928CD90D0004D2FC6 /* ScrollInAnyDirectionDemo.swift in Sources */, 190 | 7625789E28C8863000D2F2C9 /* ContentView.swift in Sources */, 191 | 76BB8AC728C891BE0079BC8C /* ListDemo.swift in Sources */, 192 | ); 193 | runOnlyForDeploymentPostprocessing = 0; 194 | }; 195 | /* End PBXSourcesBuildPhase section */ 196 | 197 | /* Begin XCBuildConfiguration section */ 198 | 7625789628C8861400D2F2C9 /* Debug */ = { 199 | isa = XCBuildConfiguration; 200 | buildSettings = { 201 | ALWAYS_SEARCH_USER_PATHS = NO; 202 | CLANG_ANALYZER_NONNULL = YES; 203 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 204 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 205 | CLANG_ENABLE_MODULES = YES; 206 | CLANG_ENABLE_OBJC_ARC = YES; 207 | CLANG_ENABLE_OBJC_WEAK = YES; 208 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 209 | CLANG_WARN_BOOL_CONVERSION = YES; 210 | CLANG_WARN_COMMA = YES; 211 | CLANG_WARN_CONSTANT_CONVERSION = YES; 212 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 213 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 214 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 215 | CLANG_WARN_EMPTY_BODY = YES; 216 | CLANG_WARN_ENUM_CONVERSION = YES; 217 | CLANG_WARN_INFINITE_RECURSION = YES; 218 | CLANG_WARN_INT_CONVERSION = YES; 219 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 220 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 221 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 222 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 223 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 224 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 225 | CLANG_WARN_STRICT_PROTOTYPES = YES; 226 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 227 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 228 | CLANG_WARN_UNREACHABLE_CODE = YES; 229 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 230 | COPY_PHASE_STRIP = NO; 231 | DEBUG_INFORMATION_FORMAT = dwarf; 232 | ENABLE_STRICT_OBJC_MSGSEND = YES; 233 | ENABLE_TESTABILITY = YES; 234 | GCC_C_LANGUAGE_STANDARD = gnu11; 235 | GCC_DYNAMIC_NO_PIC = NO; 236 | GCC_NO_COMMON_BLOCKS = YES; 237 | GCC_OPTIMIZATION_LEVEL = 0; 238 | GCC_PREPROCESSOR_DEFINITIONS = ( 239 | "DEBUG=1", 240 | "$(inherited)", 241 | ); 242 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 243 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 244 | GCC_WARN_UNDECLARED_SELECTOR = YES; 245 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 246 | GCC_WARN_UNUSED_FUNCTION = YES; 247 | GCC_WARN_UNUSED_VARIABLE = YES; 248 | IPHONEOS_DEPLOYMENT_TARGET = 15.5; 249 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 250 | MTL_FAST_MATH = YES; 251 | ONLY_ACTIVE_ARCH = YES; 252 | SDKROOT = iphoneos; 253 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 254 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 255 | }; 256 | name = Debug; 257 | }; 258 | 7625789728C8861400D2F2C9 /* Release */ = { 259 | isa = XCBuildConfiguration; 260 | buildSettings = { 261 | ALWAYS_SEARCH_USER_PATHS = NO; 262 | CLANG_ANALYZER_NONNULL = YES; 263 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 264 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 265 | CLANG_ENABLE_MODULES = YES; 266 | CLANG_ENABLE_OBJC_ARC = YES; 267 | CLANG_ENABLE_OBJC_WEAK = YES; 268 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 269 | CLANG_WARN_BOOL_CONVERSION = YES; 270 | CLANG_WARN_COMMA = YES; 271 | CLANG_WARN_CONSTANT_CONVERSION = YES; 272 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 273 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 274 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 275 | CLANG_WARN_EMPTY_BODY = YES; 276 | CLANG_WARN_ENUM_CONVERSION = YES; 277 | CLANG_WARN_INFINITE_RECURSION = YES; 278 | CLANG_WARN_INT_CONVERSION = YES; 279 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 280 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 281 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 282 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 283 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 284 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 285 | CLANG_WARN_STRICT_PROTOTYPES = YES; 286 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 287 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 288 | CLANG_WARN_UNREACHABLE_CODE = YES; 289 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 290 | COPY_PHASE_STRIP = NO; 291 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 292 | ENABLE_NS_ASSERTIONS = NO; 293 | ENABLE_STRICT_OBJC_MSGSEND = YES; 294 | GCC_C_LANGUAGE_STANDARD = gnu11; 295 | GCC_NO_COMMON_BLOCKS = YES; 296 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 297 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 298 | GCC_WARN_UNDECLARED_SELECTOR = YES; 299 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 300 | GCC_WARN_UNUSED_FUNCTION = YES; 301 | GCC_WARN_UNUSED_VARIABLE = YES; 302 | IPHONEOS_DEPLOYMENT_TARGET = 15.5; 303 | MTL_ENABLE_DEBUG_INFO = NO; 304 | MTL_FAST_MATH = YES; 305 | SDKROOT = iphoneos; 306 | SWIFT_COMPILATION_MODE = wholemodule; 307 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 308 | VALIDATE_PRODUCT = YES; 309 | }; 310 | name = Release; 311 | }; 312 | 7625789928C8861400D2F2C9 /* Debug */ = { 313 | isa = XCBuildConfiguration; 314 | buildSettings = { 315 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 316 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 317 | CLANG_ENABLE_MODULES = YES; 318 | CODE_SIGN_ENTITLEMENTS = Demo/Demo.entitlements; 319 | CODE_SIGN_STYLE = Automatic; 320 | CURRENT_PROJECT_VERSION = 1; 321 | DEVELOPMENT_ASSET_PATHS = "\"Demo/Preview Content\""; 322 | DEVELOPMENT_TEAM = VFBLFL665K; 323 | ENABLE_PREVIEWS = YES; 324 | GENERATE_INFOPLIST_FILE = YES; 325 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 326 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 327 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 328 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 329 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 330 | LD_RUNPATH_SEARCH_PATHS = ( 331 | "$(inherited)", 332 | "@executable_path/Frameworks", 333 | ); 334 | MARKETING_VERSION = 1.0; 335 | PRODUCT_BUNDLE_IDENTIFIER = com.fatbobman.Demo; 336 | PRODUCT_NAME = "$(TARGET_NAME)"; 337 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 338 | SUPPORTS_MACCATALYST = YES; 339 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; 340 | SWIFT_EMIT_LOC_STRINGS = YES; 341 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 342 | SWIFT_VERSION = 5.0; 343 | TARGETED_DEVICE_FAMILY = "1,2"; 344 | }; 345 | name = Debug; 346 | }; 347 | 7625789A28C8861400D2F2C9 /* Release */ = { 348 | isa = XCBuildConfiguration; 349 | buildSettings = { 350 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 351 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 352 | CLANG_ENABLE_MODULES = YES; 353 | CODE_SIGN_ENTITLEMENTS = Demo/Demo.entitlements; 354 | CODE_SIGN_STYLE = Automatic; 355 | CURRENT_PROJECT_VERSION = 1; 356 | DEVELOPMENT_ASSET_PATHS = "\"Demo/Preview Content\""; 357 | DEVELOPMENT_TEAM = VFBLFL665K; 358 | ENABLE_PREVIEWS = YES; 359 | GENERATE_INFOPLIST_FILE = YES; 360 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 361 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 362 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 363 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 364 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 365 | LD_RUNPATH_SEARCH_PATHS = ( 366 | "$(inherited)", 367 | "@executable_path/Frameworks", 368 | ); 369 | MARKETING_VERSION = 1.0; 370 | PRODUCT_BUNDLE_IDENTIFIER = com.fatbobman.Demo; 371 | PRODUCT_NAME = "$(TARGET_NAME)"; 372 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 373 | SUPPORTS_MACCATALYST = YES; 374 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; 375 | SWIFT_EMIT_LOC_STRINGS = YES; 376 | SWIFT_VERSION = 5.0; 377 | TARGETED_DEVICE_FAMILY = "1,2"; 378 | }; 379 | name = Release; 380 | }; 381 | /* End XCBuildConfiguration section */ 382 | 383 | /* Begin XCConfigurationList section */ 384 | 7625788528C8861300D2F2C9 /* Build configuration list for PBXProject "Demo" */ = { 385 | isa = XCConfigurationList; 386 | buildConfigurations = ( 387 | 7625789628C8861400D2F2C9 /* Debug */, 388 | 7625789728C8861400D2F2C9 /* Release */, 389 | ); 390 | defaultConfigurationIsVisible = 0; 391 | defaultConfigurationName = Release; 392 | }; 393 | 7625789828C8861400D2F2C9 /* Build configuration list for PBXNativeTarget "Demo" */ = { 394 | isa = XCConfigurationList; 395 | buildConfigurations = ( 396 | 7625789928C8861400D2F2C9 /* Debug */, 397 | 7625789A28C8861400D2F2C9 /* Release */, 398 | ); 399 | defaultConfigurationIsVisible = 0; 400 | defaultConfigurationName = Release; 401 | }; 402 | /* End XCConfigurationList section */ 403 | 404 | /* Begin XCSwiftPackageProductDependency section */ 405 | 7662DF3628CD893D004D2FC6 /* IsScrolling */ = { 406 | isa = XCSwiftPackageProductDependency; 407 | productName = IsScrolling; 408 | }; 409 | /* End XCSwiftPackageProductDependency section */ 410 | }; 411 | rootObject = 7625788228C8861300D2F2C9 /* Project object */; 412 | } 413 | --------------------------------------------------------------------------------