├── 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 | 
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 | 
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 |
--------------------------------------------------------------------------------