├── .gitignore
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ └── xcschemes
│ └── ScrollViewReactiveHeader.xcscheme
├── Package.swift
├── README.md
├── ScrollViewReactiveHeaderDemoApp
├── ScrollViewReactiveHeaderDemoApp.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── Sources
│ ├── ContentView.swift
│ ├── Example2
│ ├── Models
│ │ ├── Album.swift
│ │ ├── HomeViewModel.swift
│ │ └── Song.swift
│ ├── NetworkManager.swift
│ ├── SpotifyView.swift
│ └── Views
│ │ ├── AlbumScroll.swift
│ │ ├── AlbumThumbnail.swift
│ │ ├── HomeHeaderView.swift
│ │ ├── HomeSectionHeader.swift
│ │ ├── PremiumBannerView.swift
│ │ ├── QuickPlayGrid.swift
│ │ └── SpotifyHomeView.swift
│ ├── Example3
│ ├── StoryList
│ │ ├── Model
│ │ │ ├── Story.swift
│ │ │ └── StoryListViewModel.swift
│ │ └── Views
│ │ │ ├── StoryListCell.swift
│ │ │ ├── StoryListContentView.swift
│ │ │ └── StoryListHeaderOverlay.swift
│ └── StoryListView.swift
│ ├── Resource Files
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ ├── Contents.json
│ │ ├── Vibes.imageset
│ │ │ ├── Contents.json
│ │ │ └── Vibes.png
│ │ ├── a-tribe-called-quest.imageset
│ │ │ ├── Contents.json
│ │ │ └── a-tribe-called-quest.jpg
│ │ ├── abbey-road.imageset
│ │ │ ├── Contents.json
│ │ │ └── abbey-road.jpg
│ │ ├── background3.imageset
│ │ │ ├── Contents.json
│ │ │ └── background.jpg
│ │ ├── banana.imageset
│ │ │ ├── Contents.json
│ │ │ └── banana.jpg
│ │ ├── blue-train.imageset
│ │ │ ├── Contents.json
│ │ │ └── blue-train.jpg
│ │ ├── boutique.imageset
│ │ │ ├── Contents.json
│ │ │ └── boutique.jpg
│ │ ├── frank-sinatra.imageset
│ │ │ ├── Contents.json
│ │ │ └── frank-sinatra.jpg
│ │ ├── funkadelic.imageset
│ │ │ ├── Contents.json
│ │ │ └── funkadelic.jpg
│ │ ├── go-2.imageset
│ │ │ ├── Contents.json
│ │ │ └── go-2.jpg
│ │ ├── grainy.imageset
│ │ │ ├── Contents.json
│ │ │ └── grainy.jpg
│ │ ├── heaven-or-vegas.imageset
│ │ │ ├── Contents.json
│ │ │ └── heaven-or-vegas.jpg
│ │ ├── heroes.imageset
│ │ │ ├── Contents.json
│ │ │ └── heroes.jpg
│ │ ├── jesus-of-cool.imageset
│ │ │ ├── Contents.json
│ │ │ └── jesus-of-cool.jpg
│ │ ├── moving-pictures.imageset
│ │ │ ├── Contents.json
│ │ │ └── Rush-Moving-Pictures-Album-Cover-web-optimised-820.jpg
│ │ ├── night-sky.imageset
│ │ │ ├── Contents.json
│ │ │ └── night-sky.jpg
│ │ ├── odessa.imageset
│ │ │ ├── Contents.json
│ │ │ └── odessa.jpg
│ │ ├── peppers.imageset
│ │ │ ├── Contents.json
│ │ │ └── peppers.jpg
│ │ ├── speaking-in-tongues.imageset
│ │ │ ├── Contents.json
│ │ │ └── speaking-in-tongues.jpg
│ │ ├── unknown-pleasures.imageset
│ │ │ ├── Contents.json
│ │ │ └── unknown-pleasures.jpg
│ │ └── yeezus.imageset
│ │ │ ├── Contents.json
│ │ │ └── yeezus.jpg
│ ├── Info.plist
│ └── Preview Content
│ │ └── Preview Assets.xcassets
│ │ └── Contents.json
│ └── ScrollViewReactiveHeaderDemoAppApp.swift
├── Sources
├── Model
│ ├── HeaderPreferenceKey.swift
│ ├── ScrollViewConfiguration.swift
│ └── ScrollViewPreferenceKey.swift
├── ScrollViewReactiveHeader.swift
└── Views
│ └── GeometryReaderOverlay.swift
└── Tests
└── ScrollViewReactiveHeaderTests
└── ScrollViewReactiveHeaderTests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/ScrollViewReactiveHeader.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
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: "ScrollViewReactiveHeader",
8 | platforms: [.macOS(.v11), .iOS(.v13),],
9 | products: [
10 | // Products define the executables and libraries a package produces, and make them visible to other packages.
11 | .library(
12 | name: "ScrollViewReactiveHeader",
13 | targets: ["ScrollViewReactiveHeader"]),
14 | ],
15 | dependencies: [
16 | // Dependencies declare other packages that this package depends on.
17 | // .package(url: /* package url */, from: "1.0.0"),
18 | ],
19 | targets: [
20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
21 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
22 | .target(
23 | name: "ScrollViewReactiveHeader",
24 | dependencies: [],
25 | path: "Sources"),
26 | .testTarget(
27 | name: "ScrollViewReactiveHeaderTests",
28 | dependencies: ["ScrollViewReactiveHeader"]),
29 | ]
30 | )
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ScrollViewReactiveHeader
2 |
3 | A replacement `ScrollView` that provides a header with subtle scroll animations.
4 |
5 | To see the rest of the SwiftUI Library, visit [our website](https://swiftuilibrary.com).
6 |
7 | https://user-images.githubusercontent.com/8763719/132362666-99609c48-0762-4351-b532-49ae03dda274.mov
8 |
9 | Using `ScrollViewReactiveHeader` is easy:
10 |
11 | ```swift
12 | ScrollViewReactiveHeader(header: {
13 |
14 | MyHeaderBackground()
15 | .frame(height: 300)
16 | }, headerOverlay: {
17 |
18 | MyHeaderContent()
19 | .frame(height: 300)
20 | }, body: {
21 |
22 | // Note: This view will be placed inside a ScrollView
23 | MyScrollingContentView()
24 | }, configuration: .init(showStatusBar: true, backgroundColor: .white))
25 | ```
26 |
27 | ## Future Todos
28 |
29 | - [ ] Make `headerOverlay` interactive. At the moment, taps will be blocked by the overlaid `ScrollView`
30 | - [ ] Add optional callback that reports internally-calculated scroll offset.
31 | - [ ] Remove dependency on `GeometryReader` for calculating status bar height. (at the moment, setting `.edgesIgnoringSafeArea(.top)` will interfere with this package's ability to calculate the height of the status bar. )
32 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/ScrollViewReactiveHeaderDemoApp.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 8459FC3026E39A7600EB434F /* Story.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8459FC2F26E39A7600EB434F /* Story.swift */; };
11 | 8459FC3226E39AB400EB434F /* StoryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8459FC3126E39AB400EB434F /* StoryListViewModel.swift */; };
12 | 8459FC3526E39DA600EB434F /* StoryListHeaderOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8459FC3426E39DA600EB434F /* StoryListHeaderOverlay.swift */; };
13 | 8459FC3826E3B1D700EB434F /* StoryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8459FC3726E3B1D700EB434F /* StoryListView.swift */; };
14 | 8459FC3B26E3B36E00EB434F /* StoryListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8459FC3A26E3B36E00EB434F /* StoryListCell.swift */; };
15 | 8459FC3D26E3C94900EB434F /* StoryListContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8459FC3C26E3C94900EB434F /* StoryListContentView.swift */; };
16 | 848BCF8826DE4ED800F8D967 /* ScrollViewReactiveHeaderDemoAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848BCF8726DE4ED800F8D967 /* ScrollViewReactiveHeaderDemoAppApp.swift */; };
17 | 848BCF8A26DE4ED800F8D967 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848BCF8926DE4ED800F8D967 /* ContentView.swift */; };
18 | 848BCF8C26DE4EDA00F8D967 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 848BCF8B26DE4EDA00F8D967 /* Assets.xcassets */; };
19 | 848BCF8F26DE4EDA00F8D967 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 848BCF8E26DE4EDA00F8D967 /* Preview Assets.xcassets */; };
20 | 84D4F3B926DE4FE300E6C464 /* ScrollViewReactiveHeader in Frameworks */ = {isa = PBXBuildFile; productRef = 84D4F3B826DE4FE300E6C464 /* ScrollViewReactiveHeader */; };
21 | 84D76F0E26E51FDA004C937D /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D76EFC26E51FDA004C937D /* NetworkManager.swift */; };
22 | 84D76F0F26E51FDA004C937D /* Song.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D76EFE26E51FDA004C937D /* Song.swift */; };
23 | 84D76F1026E51FDA004C937D /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D76EFF26E51FDA004C937D /* HomeViewModel.swift */; };
24 | 84D76F1126E51FDA004C937D /* Album.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D76F0026E51FDA004C937D /* Album.swift */; };
25 | 84D76F1426E51FDA004C937D /* AlbumThumbnail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D76F0526E51FDA004C937D /* AlbumThumbnail.swift */; };
26 | 84D76F1526E51FDA004C937D /* SpotifyHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D76F0626E51FDA004C937D /* SpotifyHomeView.swift */; };
27 | 84D76F1626E51FDA004C937D /* QuickPlayGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D76F0726E51FDA004C937D /* QuickPlayGrid.swift */; };
28 | 84D76F1726E51FDA004C937D /* HomeSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D76F0826E51FDA004C937D /* HomeSectionHeader.swift */; };
29 | 84D76F1826E51FDA004C937D /* AlbumScroll.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D76F0926E51FDA004C937D /* AlbumScroll.swift */; };
30 | 84D76F1926E51FDA004C937D /* HomeHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D76F0A26E51FDA004C937D /* HomeHeaderView.swift */; };
31 | 84D76F1A26E51FDA004C937D /* SpotifyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D76F0B26E51FDA004C937D /* SpotifyView.swift */; };
32 | 84D76F1D26E5243E004C937D /* PremiumBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D76F1C26E5243E004C937D /* PremiumBannerView.swift */; };
33 | /* End PBXBuildFile section */
34 |
35 | /* Begin PBXFileReference section */
36 | 8459FC2F26E39A7600EB434F /* Story.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Story.swift; sourceTree = ""; };
37 | 8459FC3126E39AB400EB434F /* StoryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryListViewModel.swift; sourceTree = ""; };
38 | 8459FC3426E39DA600EB434F /* StoryListHeaderOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryListHeaderOverlay.swift; sourceTree = ""; };
39 | 8459FC3726E3B1D700EB434F /* StoryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryListView.swift; sourceTree = ""; };
40 | 8459FC3A26E3B36E00EB434F /* StoryListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryListCell.swift; sourceTree = ""; };
41 | 8459FC3C26E3C94900EB434F /* StoryListContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryListContentView.swift; sourceTree = ""; };
42 | 848BCF8426DE4ED800F8D967 /* ScrollViewReactiveHeaderDemoApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ScrollViewReactiveHeaderDemoApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
43 | 848BCF8726DE4ED800F8D967 /* ScrollViewReactiveHeaderDemoAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewReactiveHeaderDemoAppApp.swift; sourceTree = ""; };
44 | 848BCF8926DE4ED800F8D967 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
45 | 848BCF8B26DE4EDA00F8D967 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
46 | 848BCF8E26DE4EDA00F8D967 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
47 | 848BCF9026DE4EDA00F8D967 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
48 | 84D4F3B626DE4FCE00E6C464 /* ScrollViewReactiveHeader */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ScrollViewReactiveHeader; path = ..; sourceTree = ""; };
49 | 84D76EFC26E51FDA004C937D /* NetworkManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; };
50 | 84D76EFE26E51FDA004C937D /* Song.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Song.swift; sourceTree = ""; };
51 | 84D76EFF26E51FDA004C937D /* HomeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; };
52 | 84D76F0026E51FDA004C937D /* Album.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Album.swift; sourceTree = ""; };
53 | 84D76F0526E51FDA004C937D /* AlbumThumbnail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlbumThumbnail.swift; sourceTree = ""; };
54 | 84D76F0626E51FDA004C937D /* SpotifyHomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpotifyHomeView.swift; sourceTree = ""; };
55 | 84D76F0726E51FDA004C937D /* QuickPlayGrid.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuickPlayGrid.swift; sourceTree = ""; };
56 | 84D76F0826E51FDA004C937D /* HomeSectionHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeSectionHeader.swift; sourceTree = ""; };
57 | 84D76F0926E51FDA004C937D /* AlbumScroll.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlbumScroll.swift; sourceTree = ""; };
58 | 84D76F0A26E51FDA004C937D /* HomeHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeHeaderView.swift; sourceTree = ""; };
59 | 84D76F0B26E51FDA004C937D /* SpotifyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpotifyView.swift; sourceTree = ""; };
60 | 84D76F1C26E5243E004C937D /* PremiumBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumBannerView.swift; sourceTree = ""; };
61 | /* End PBXFileReference section */
62 |
63 | /* Begin PBXFrameworksBuildPhase section */
64 | 848BCF8126DE4ED800F8D967 /* Frameworks */ = {
65 | isa = PBXFrameworksBuildPhase;
66 | buildActionMask = 2147483647;
67 | files = (
68 | 84D4F3B926DE4FE300E6C464 /* ScrollViewReactiveHeader in Frameworks */,
69 | );
70 | runOnlyForDeploymentPostprocessing = 0;
71 | };
72 | /* End PBXFrameworksBuildPhase section */
73 |
74 | /* Begin PBXGroup section */
75 | 8459FC2E26E39A5300EB434F /* StoryList */ = {
76 | isa = PBXGroup;
77 | children = (
78 | 8459FC3926E3B2AA00EB434F /* Model */,
79 | 8459FC3326E39D9400EB434F /* Views */,
80 | );
81 | path = StoryList;
82 | sourceTree = "";
83 | };
84 | 8459FC3326E39D9400EB434F /* Views */ = {
85 | isa = PBXGroup;
86 | children = (
87 | 8459FC3426E39DA600EB434F /* StoryListHeaderOverlay.swift */,
88 | 8459FC3A26E3B36E00EB434F /* StoryListCell.swift */,
89 | 8459FC3C26E3C94900EB434F /* StoryListContentView.swift */,
90 | );
91 | path = Views;
92 | sourceTree = "";
93 | };
94 | 8459FC3626E3B1B100EB434F /* Example3 */ = {
95 | isa = PBXGroup;
96 | children = (
97 | 8459FC2E26E39A5300EB434F /* StoryList */,
98 | 8459FC3726E3B1D700EB434F /* StoryListView.swift */,
99 | );
100 | path = Example3;
101 | sourceTree = "";
102 | };
103 | 8459FC3926E3B2AA00EB434F /* Model */ = {
104 | isa = PBXGroup;
105 | children = (
106 | 8459FC2F26E39A7600EB434F /* Story.swift */,
107 | 8459FC3126E39AB400EB434F /* StoryListViewModel.swift */,
108 | );
109 | path = Model;
110 | sourceTree = "";
111 | };
112 | 848BCF7B26DE4ED800F8D967 = {
113 | isa = PBXGroup;
114 | children = (
115 | 84D4F3B626DE4FCE00E6C464 /* ScrollViewReactiveHeader */,
116 | 848BCF8626DE4ED800F8D967 /* Sources */,
117 | 848BCF8526DE4ED800F8D967 /* Products */,
118 | 84D4F3B726DE4FE300E6C464 /* Frameworks */,
119 | );
120 | sourceTree = "";
121 | };
122 | 848BCF8526DE4ED800F8D967 /* Products */ = {
123 | isa = PBXGroup;
124 | children = (
125 | 848BCF8426DE4ED800F8D967 /* ScrollViewReactiveHeaderDemoApp.app */,
126 | );
127 | name = Products;
128 | sourceTree = "";
129 | };
130 | 848BCF8626DE4ED800F8D967 /* Sources */ = {
131 | isa = PBXGroup;
132 | children = (
133 | 848BCF9626DE4EFC00F8D967 /* Resource Files */,
134 | 848BCF8726DE4ED800F8D967 /* ScrollViewReactiveHeaderDemoAppApp.swift */,
135 | 848BCF8926DE4ED800F8D967 /* ContentView.swift */,
136 | 84D76EFA26E51FDA004C937D /* Example2 */,
137 | 8459FC3626E3B1B100EB434F /* Example3 */,
138 | );
139 | path = Sources;
140 | sourceTree = "";
141 | };
142 | 848BCF8D26DE4EDA00F8D967 /* Preview Content */ = {
143 | isa = PBXGroup;
144 | children = (
145 | 848BCF8E26DE4EDA00F8D967 /* Preview Assets.xcassets */,
146 | );
147 | path = "Preview Content";
148 | sourceTree = "";
149 | };
150 | 848BCF9626DE4EFC00F8D967 /* Resource Files */ = {
151 | isa = PBXGroup;
152 | children = (
153 | 848BCF8B26DE4EDA00F8D967 /* Assets.xcassets */,
154 | 848BCF9026DE4EDA00F8D967 /* Info.plist */,
155 | 848BCF8D26DE4EDA00F8D967 /* Preview Content */,
156 | );
157 | path = "Resource Files";
158 | sourceTree = "";
159 | };
160 | 84D4F3B726DE4FE300E6C464 /* Frameworks */ = {
161 | isa = PBXGroup;
162 | children = (
163 | );
164 | name = Frameworks;
165 | sourceTree = "";
166 | };
167 | 84D76EFA26E51FDA004C937D /* Example2 */ = {
168 | isa = PBXGroup;
169 | children = (
170 | 84D76EFC26E51FDA004C937D /* NetworkManager.swift */,
171 | 84D76EFD26E51FDA004C937D /* Models */,
172 | 84D76F0426E51FDA004C937D /* Views */,
173 | 84D76F0B26E51FDA004C937D /* SpotifyView.swift */,
174 | );
175 | path = Example2;
176 | sourceTree = "";
177 | };
178 | 84D76EFD26E51FDA004C937D /* Models */ = {
179 | isa = PBXGroup;
180 | children = (
181 | 84D76EFE26E51FDA004C937D /* Song.swift */,
182 | 84D76EFF26E51FDA004C937D /* HomeViewModel.swift */,
183 | 84D76F0026E51FDA004C937D /* Album.swift */,
184 | );
185 | path = Models;
186 | sourceTree = "";
187 | };
188 | 84D76F0426E51FDA004C937D /* Views */ = {
189 | isa = PBXGroup;
190 | children = (
191 | 84D76F0526E51FDA004C937D /* AlbumThumbnail.swift */,
192 | 84D76F0626E51FDA004C937D /* SpotifyHomeView.swift */,
193 | 84D76F0726E51FDA004C937D /* QuickPlayGrid.swift */,
194 | 84D76F0826E51FDA004C937D /* HomeSectionHeader.swift */,
195 | 84D76F0926E51FDA004C937D /* AlbumScroll.swift */,
196 | 84D76F0A26E51FDA004C937D /* HomeHeaderView.swift */,
197 | 84D76F1C26E5243E004C937D /* PremiumBannerView.swift */,
198 | );
199 | path = Views;
200 | sourceTree = "";
201 | };
202 | /* End PBXGroup section */
203 |
204 | /* Begin PBXNativeTarget section */
205 | 848BCF8326DE4ED800F8D967 /* ScrollViewReactiveHeaderDemoApp */ = {
206 | isa = PBXNativeTarget;
207 | buildConfigurationList = 848BCF9326DE4EDA00F8D967 /* Build configuration list for PBXNativeTarget "ScrollViewReactiveHeaderDemoApp" */;
208 | buildPhases = (
209 | 848BCF8026DE4ED800F8D967 /* Sources */,
210 | 848BCF8126DE4ED800F8D967 /* Frameworks */,
211 | 848BCF8226DE4ED800F8D967 /* Resources */,
212 | );
213 | buildRules = (
214 | );
215 | dependencies = (
216 | );
217 | name = ScrollViewReactiveHeaderDemoApp;
218 | packageProductDependencies = (
219 | 84D4F3B826DE4FE300E6C464 /* ScrollViewReactiveHeader */,
220 | );
221 | productName = ScrollViewReactiveHeaderDemoApp;
222 | productReference = 848BCF8426DE4ED800F8D967 /* ScrollViewReactiveHeaderDemoApp.app */;
223 | productType = "com.apple.product-type.application";
224 | };
225 | /* End PBXNativeTarget section */
226 |
227 | /* Begin PBXProject section */
228 | 848BCF7C26DE4ED800F8D967 /* Project object */ = {
229 | isa = PBXProject;
230 | attributes = {
231 | LastSwiftUpdateCheck = 1250;
232 | LastUpgradeCheck = 1250;
233 | TargetAttributes = {
234 | 848BCF8326DE4ED800F8D967 = {
235 | CreatedOnToolsVersion = 12.5.1;
236 | };
237 | };
238 | };
239 | buildConfigurationList = 848BCF7F26DE4ED800F8D967 /* Build configuration list for PBXProject "ScrollViewReactiveHeaderDemoApp" */;
240 | compatibilityVersion = "Xcode 9.3";
241 | developmentRegion = en;
242 | hasScannedForEncodings = 0;
243 | knownRegions = (
244 | en,
245 | Base,
246 | );
247 | mainGroup = 848BCF7B26DE4ED800F8D967;
248 | productRefGroup = 848BCF8526DE4ED800F8D967 /* Products */;
249 | projectDirPath = "";
250 | projectRoot = "";
251 | targets = (
252 | 848BCF8326DE4ED800F8D967 /* ScrollViewReactiveHeaderDemoApp */,
253 | );
254 | };
255 | /* End PBXProject section */
256 |
257 | /* Begin PBXResourcesBuildPhase section */
258 | 848BCF8226DE4ED800F8D967 /* Resources */ = {
259 | isa = PBXResourcesBuildPhase;
260 | buildActionMask = 2147483647;
261 | files = (
262 | 848BCF8F26DE4EDA00F8D967 /* Preview Assets.xcassets in Resources */,
263 | 848BCF8C26DE4EDA00F8D967 /* Assets.xcassets in Resources */,
264 | );
265 | runOnlyForDeploymentPostprocessing = 0;
266 | };
267 | /* End PBXResourcesBuildPhase section */
268 |
269 | /* Begin PBXSourcesBuildPhase section */
270 | 848BCF8026DE4ED800F8D967 /* Sources */ = {
271 | isa = PBXSourcesBuildPhase;
272 | buildActionMask = 2147483647;
273 | files = (
274 | 84D76F0E26E51FDA004C937D /* NetworkManager.swift in Sources */,
275 | 84D76F1026E51FDA004C937D /* HomeViewModel.swift in Sources */,
276 | 8459FC3D26E3C94900EB434F /* StoryListContentView.swift in Sources */,
277 | 84D76F1526E51FDA004C937D /* SpotifyHomeView.swift in Sources */,
278 | 8459FC3226E39AB400EB434F /* StoryListViewModel.swift in Sources */,
279 | 8459FC3526E39DA600EB434F /* StoryListHeaderOverlay.swift in Sources */,
280 | 84D76F1A26E51FDA004C937D /* SpotifyView.swift in Sources */,
281 | 84D76F0F26E51FDA004C937D /* Song.swift in Sources */,
282 | 84D76F1626E51FDA004C937D /* QuickPlayGrid.swift in Sources */,
283 | 84D76F1926E51FDA004C937D /* HomeHeaderView.swift in Sources */,
284 | 84D76F1826E51FDA004C937D /* AlbumScroll.swift in Sources */,
285 | 8459FC3826E3B1D700EB434F /* StoryListView.swift in Sources */,
286 | 848BCF8A26DE4ED800F8D967 /* ContentView.swift in Sources */,
287 | 848BCF8826DE4ED800F8D967 /* ScrollViewReactiveHeaderDemoAppApp.swift in Sources */,
288 | 84D76F1426E51FDA004C937D /* AlbumThumbnail.swift in Sources */,
289 | 8459FC3026E39A7600EB434F /* Story.swift in Sources */,
290 | 8459FC3B26E3B36E00EB434F /* StoryListCell.swift in Sources */,
291 | 84D76F1126E51FDA004C937D /* Album.swift in Sources */,
292 | 84D76F1D26E5243E004C937D /* PremiumBannerView.swift in Sources */,
293 | 84D76F1726E51FDA004C937D /* HomeSectionHeader.swift in Sources */,
294 | );
295 | runOnlyForDeploymentPostprocessing = 0;
296 | };
297 | /* End PBXSourcesBuildPhase section */
298 |
299 | /* Begin XCBuildConfiguration section */
300 | 848BCF9126DE4EDA00F8D967 /* Debug */ = {
301 | isa = XCBuildConfiguration;
302 | buildSettings = {
303 | ALWAYS_SEARCH_USER_PATHS = NO;
304 | CLANG_ANALYZER_NONNULL = YES;
305 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
306 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
307 | CLANG_CXX_LIBRARY = "libc++";
308 | CLANG_ENABLE_MODULES = YES;
309 | CLANG_ENABLE_OBJC_ARC = YES;
310 | CLANG_ENABLE_OBJC_WEAK = YES;
311 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
312 | CLANG_WARN_BOOL_CONVERSION = YES;
313 | CLANG_WARN_COMMA = YES;
314 | CLANG_WARN_CONSTANT_CONVERSION = YES;
315 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
316 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
317 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
318 | CLANG_WARN_EMPTY_BODY = YES;
319 | CLANG_WARN_ENUM_CONVERSION = YES;
320 | CLANG_WARN_INFINITE_RECURSION = YES;
321 | CLANG_WARN_INT_CONVERSION = YES;
322 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
323 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
324 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
325 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
326 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
327 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
328 | CLANG_WARN_STRICT_PROTOTYPES = YES;
329 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
330 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
331 | CLANG_WARN_UNREACHABLE_CODE = YES;
332 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
333 | COPY_PHASE_STRIP = NO;
334 | DEBUG_INFORMATION_FORMAT = dwarf;
335 | ENABLE_STRICT_OBJC_MSGSEND = YES;
336 | ENABLE_TESTABILITY = YES;
337 | GCC_C_LANGUAGE_STANDARD = gnu11;
338 | GCC_DYNAMIC_NO_PIC = NO;
339 | GCC_NO_COMMON_BLOCKS = YES;
340 | GCC_OPTIMIZATION_LEVEL = 0;
341 | GCC_PREPROCESSOR_DEFINITIONS = (
342 | "DEBUG=1",
343 | "$(inherited)",
344 | );
345 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
346 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
347 | GCC_WARN_UNDECLARED_SELECTOR = YES;
348 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
349 | GCC_WARN_UNUSED_FUNCTION = YES;
350 | GCC_WARN_UNUSED_VARIABLE = YES;
351 | IPHONEOS_DEPLOYMENT_TARGET = 14.5;
352 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
353 | MTL_FAST_MATH = YES;
354 | ONLY_ACTIVE_ARCH = YES;
355 | SDKROOT = iphoneos;
356 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
357 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
358 | };
359 | name = Debug;
360 | };
361 | 848BCF9226DE4EDA00F8D967 /* Release */ = {
362 | isa = XCBuildConfiguration;
363 | buildSettings = {
364 | ALWAYS_SEARCH_USER_PATHS = NO;
365 | CLANG_ANALYZER_NONNULL = YES;
366 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
367 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
368 | CLANG_CXX_LIBRARY = "libc++";
369 | CLANG_ENABLE_MODULES = YES;
370 | CLANG_ENABLE_OBJC_ARC = YES;
371 | CLANG_ENABLE_OBJC_WEAK = YES;
372 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
373 | CLANG_WARN_BOOL_CONVERSION = YES;
374 | CLANG_WARN_COMMA = YES;
375 | CLANG_WARN_CONSTANT_CONVERSION = YES;
376 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
377 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
378 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
379 | CLANG_WARN_EMPTY_BODY = YES;
380 | CLANG_WARN_ENUM_CONVERSION = YES;
381 | CLANG_WARN_INFINITE_RECURSION = YES;
382 | CLANG_WARN_INT_CONVERSION = YES;
383 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
384 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
385 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
386 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
387 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
388 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
389 | CLANG_WARN_STRICT_PROTOTYPES = YES;
390 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
391 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
392 | CLANG_WARN_UNREACHABLE_CODE = YES;
393 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
394 | COPY_PHASE_STRIP = NO;
395 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
396 | ENABLE_NS_ASSERTIONS = NO;
397 | ENABLE_STRICT_OBJC_MSGSEND = YES;
398 | GCC_C_LANGUAGE_STANDARD = gnu11;
399 | GCC_NO_COMMON_BLOCKS = YES;
400 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
401 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
402 | GCC_WARN_UNDECLARED_SELECTOR = YES;
403 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
404 | GCC_WARN_UNUSED_FUNCTION = YES;
405 | GCC_WARN_UNUSED_VARIABLE = YES;
406 | IPHONEOS_DEPLOYMENT_TARGET = 14.5;
407 | MTL_ENABLE_DEBUG_INFO = NO;
408 | MTL_FAST_MATH = YES;
409 | SDKROOT = iphoneos;
410 | SWIFT_COMPILATION_MODE = wholemodule;
411 | SWIFT_OPTIMIZATION_LEVEL = "-O";
412 | VALIDATE_PRODUCT = YES;
413 | };
414 | name = Release;
415 | };
416 | 848BCF9426DE4EDA00F8D967 /* Debug */ = {
417 | isa = XCBuildConfiguration;
418 | buildSettings = {
419 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
420 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
421 | CODE_SIGN_STYLE = Automatic;
422 | DEVELOPMENT_ASSET_PATHS = "\"Sources/Resource Files/Preview Content\"";
423 | DEVELOPMENT_TEAM = 3XTTWH436M;
424 | ENABLE_PREVIEWS = YES;
425 | INFOPLIST_FILE = "Sources/Resource Files/Info.plist";
426 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
427 | LD_RUNPATH_SEARCH_PATHS = (
428 | "$(inherited)",
429 | "@executable_path/Frameworks",
430 | );
431 | PRODUCT_BUNDLE_IDENTIFIER = com.trentguillory.ScrollViewReactiveHeaderDemoApp;
432 | PRODUCT_NAME = "$(TARGET_NAME)";
433 | SWIFT_VERSION = 5.0;
434 | TARGETED_DEVICE_FAMILY = "1,2";
435 | };
436 | name = Debug;
437 | };
438 | 848BCF9526DE4EDA00F8D967 /* Release */ = {
439 | isa = XCBuildConfiguration;
440 | buildSettings = {
441 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
442 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
443 | CODE_SIGN_STYLE = Automatic;
444 | DEVELOPMENT_ASSET_PATHS = "\"Sources/Resource Files/Preview Content\"";
445 | DEVELOPMENT_TEAM = 3XTTWH436M;
446 | ENABLE_PREVIEWS = YES;
447 | INFOPLIST_FILE = "Sources/Resource Files/Info.plist";
448 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
449 | LD_RUNPATH_SEARCH_PATHS = (
450 | "$(inherited)",
451 | "@executable_path/Frameworks",
452 | );
453 | PRODUCT_BUNDLE_IDENTIFIER = com.trentguillory.ScrollViewReactiveHeaderDemoApp;
454 | PRODUCT_NAME = "$(TARGET_NAME)";
455 | SWIFT_VERSION = 5.0;
456 | TARGETED_DEVICE_FAMILY = "1,2";
457 | };
458 | name = Release;
459 | };
460 | /* End XCBuildConfiguration section */
461 |
462 | /* Begin XCConfigurationList section */
463 | 848BCF7F26DE4ED800F8D967 /* Build configuration list for PBXProject "ScrollViewReactiveHeaderDemoApp" */ = {
464 | isa = XCConfigurationList;
465 | buildConfigurations = (
466 | 848BCF9126DE4EDA00F8D967 /* Debug */,
467 | 848BCF9226DE4EDA00F8D967 /* Release */,
468 | );
469 | defaultConfigurationIsVisible = 0;
470 | defaultConfigurationName = Release;
471 | };
472 | 848BCF9326DE4EDA00F8D967 /* Build configuration list for PBXNativeTarget "ScrollViewReactiveHeaderDemoApp" */ = {
473 | isa = XCConfigurationList;
474 | buildConfigurations = (
475 | 848BCF9426DE4EDA00F8D967 /* Debug */,
476 | 848BCF9526DE4EDA00F8D967 /* Release */,
477 | );
478 | defaultConfigurationIsVisible = 0;
479 | defaultConfigurationName = Release;
480 | };
481 | /* End XCConfigurationList section */
482 |
483 | /* Begin XCSwiftPackageProductDependency section */
484 | 84D4F3B826DE4FE300E6C464 /* ScrollViewReactiveHeader */ = {
485 | isa = XCSwiftPackageProductDependency;
486 | productName = ScrollViewReactiveHeader;
487 | };
488 | /* End XCSwiftPackageProductDependency section */
489 | };
490 | rootObject = 848BCF7C26DE4ED800F8D967 /* Project object */;
491 | }
492 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/ScrollViewReactiveHeaderDemoApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/ScrollViewReactiveHeaderDemoApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/ContentView.swift:
--------------------------------------------------------------------------------
1 | import ScrollViewReactiveHeader
2 | import SwiftUI
3 |
4 | // MARK: - ContentView
5 |
6 | struct ContentView: View {
7 |
8 | var body: some View {
9 |
10 | TabView {
11 |
12 | SpotifyView()
13 | .tabItem {
14 | VStack {
15 |
16 | Image(systemName: "music.note.list")
17 |
18 | Text("Music")
19 | }
20 | }
21 |
22 | StoryListView()
23 | .tabItem {
24 | VStack {
25 |
26 | Image(systemName: "book")
27 |
28 | Text("Reader")
29 | }
30 | }
31 | }
32 | .preferredColorScheme(.light)
33 | .accentColor(.black)
34 | }
35 | }
36 |
37 | // MARK: - HeaderOverlay
38 |
39 | struct HeaderOverlay: View {
40 |
41 | var body: some View {
42 |
43 | VStack {
44 |
45 | Spacer()
46 |
47 | Text("Not sure where to go?")
48 | .font(.title)
49 | .frame(maxWidth: .infinity, alignment: .center)
50 | .foregroundColor(.white)
51 |
52 | Text("Perfect")
53 | .font(.title)
54 | .frame(maxWidth: .infinity, alignment: .center)
55 | .foregroundColor(.white)
56 |
57 | Spacer()
58 | }
59 | .frame(height: 450)
60 | }
61 | }
62 |
63 | // MARK: - ScrollViewContent
64 |
65 | struct ScrollViewContent: View {
66 |
67 | var body: some View {
68 |
69 | VStack(alignment: .leading, spacing: 16) {
70 |
71 | Text("content")
72 | .font(.headline)
73 | .frame(maxWidth: .infinity, alignment: .leading)
74 |
75 | Text("content")
76 | .frame(maxWidth: .infinity, alignment: .leading)
77 |
78 | Text("content")
79 | .frame(maxWidth: .infinity, alignment: .leading)
80 |
81 | Spacer()
82 | }
83 | .frame(height: 600)
84 | }
85 | }
86 |
87 | // MARK: - ContentView_Previews
88 |
89 | struct ContentView_Previews: PreviewProvider {
90 | static var previews: some View {
91 | ContentView()
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Example2/Models/Album.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | struct Album: Hashable {
5 |
6 | var cover: String
7 | var title: String
8 | var artist: String
9 | var songs: [Song]
10 |
11 | static func == (lhs: Album, rhs: Album) -> Bool {
12 | lhs.title == rhs.title && lhs.artist == rhs.artist
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Example2/Models/HomeViewModel.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // MARK: - HomeViewSection
4 |
5 | struct HomeViewSection {
6 | var order: Int
7 | var title: String
8 | var albums: [Album]
9 | var type: HomeSectionType
10 | }
11 |
12 | // MARK: - HomeSectionType
13 |
14 | enum HomeSectionType {
15 | case albumScroll, quickShuffle
16 | }
17 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Example2/Models/Song.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct Song: Hashable {
4 | var title: String
5 | var artist: String
6 | var duration: TimeInterval
7 | }
8 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Example2/NetworkManager.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | class NetworkManager {
5 |
6 | // MARK: Internal
7 |
8 | static let shared = NetworkManager()
9 |
10 | func fetchHomeScreen() -> [HomeViewSection] {
11 | // Recently played albums.
12 | let tribe = Album(
13 | cover: "a-tribe-called-quest",
14 | title: "Midnight Marauders",
15 | artist: "A Tribe Called Quest",
16 | songs: exampleSongs)
17 | let abbeyRoad = Album(
18 | cover: "abbey-road",
19 | title: "Abbey Road",
20 | artist: "The Beatles",
21 | songs: exampleSongs)
22 | let banana = Album(
23 | cover: "banana",
24 | title: "Feel Slowly",
25 | artist: "Andy Warhol",
26 | songs: exampleSongs)
27 | let blueTrain = Album(
28 | cover: "blue-train",
29 | title: "Blue Train",
30 | artist: "John Coltrane",
31 | songs: exampleSongs)
32 |
33 | let recentlyPlayed = HomeViewSection(
34 | order: 1,
35 | title: "Recently Played",
36 | albums: [tribe, abbeyRoad, banana, blueTrain],
37 | type: .albumScroll)
38 |
39 | // Heavy rotation albums
40 | let boutique = Album(
41 | cover: "boutique",
42 | title: "Paul's Boutique",
43 | artist: "Beastie Boys",
44 | songs: exampleSongs)
45 | let flyWithMe = Album(
46 | cover: "frank-sinatra",
47 | title: "Come Fly With Me",
48 | artist: "Frank Sinatra",
49 | songs: exampleSongs)
50 | let funkadelic = Album(
51 | cover: "funkadelic",
52 | title: "Maggot Brain",
53 | artist: "Funkadelic",
54 | songs: exampleSongs)
55 | let go2 = Album(cover: "go-2", title: "XTC Go 2", artist: "XTC", songs: exampleSongs)
56 |
57 | let heavyRotation = HomeViewSection(
58 | order: 2,
59 | title: "Heavy Rotation",
60 | albums: [boutique, flyWithMe, funkadelic, go2],
61 | type: .albumScroll)
62 |
63 | // Recommended albums
64 | let heavenOrVegas = Album(
65 | cover: "heaven-or-vegas",
66 | title: "Heaven or Vegas",
67 | artist: "Cocteau Twins",
68 | songs: exampleSongs)
69 | let heroes = Album(
70 | cover: "heroes",
71 | title: "Heroes",
72 | artist: "David Bowie",
73 | songs: exampleSongs)
74 | let jesusOfCool = Album(
75 | cover: "jesus-of-cool",
76 | title: "Jesus of Cool",
77 | artist: "Nick Lowe",
78 | songs: exampleSongs)
79 | let odessa = Album(
80 | cover: "odessa",
81 | title: "Odessa",
82 | artist: "Bee Gees",
83 | songs: exampleSongs)
84 | let peppers = Album(
85 | cover: "peppers",
86 | title: "Lonely Hearts",
87 | artist: "The Beatles",
88 | songs: exampleSongs)
89 |
90 | let recommended = HomeViewSection(
91 | order: 3,
92 | title: "Recommended",
93 | albums: [heavenOrVegas, heroes, jesusOfCool, odessa, peppers],
94 | type: .albumScroll)
95 |
96 | // Summer rewind albums
97 | let rush = Album(
98 | cover: "moving-pictures",
99 | title: "Moving Pictures",
100 | artist: "Rush",
101 | songs: exampleSongs)
102 | let tongues = Album(
103 | cover: "speaking-in-tongues",
104 | title: "Speaking in Tongues",
105 | artist: "Talking Heads",
106 | songs: exampleSongs)
107 | let pleasures = Album(
108 | cover: "unknown-pleasures",
109 | title: "Unknown Pleasures",
110 | artist: "Joywave",
111 | songs: exampleSongs)
112 | let yeezus = Album(
113 | cover: "yeezus",
114 | title: "Yeezus",
115 | artist: "Kanye West",
116 | songs: exampleSongs)
117 |
118 | let summerRewind = HomeViewSection(
119 | order: 4,
120 | title: "Summer Rewind",
121 | albums: [rush, tongues, pleasures, yeezus],
122 | type: .albumScroll)
123 |
124 | // Quick shuffles
125 | let quickShuffles = HomeViewSection(
126 | order: 0,
127 | title: "Good afternoon",
128 | albums: [rush, tongues, pleasures, yeezus, peppers, odessa],
129 | type: .quickShuffle)
130 |
131 | return [quickShuffles, recentlyPlayed, heavyRotation, recommended, summerRewind]
132 | }
133 |
134 | // MARK: Private
135 |
136 | private let exampleSongs = [
137 | Song(title: "Track One", artist: "None", duration: TimeInterval(180)),
138 | Song(title: "Track Two", artist: "None", duration: TimeInterval(180)),
139 | Song(title: "Track Three", artist: "None", duration: TimeInterval(180)),
140 | Song(title: "Track Four", artist: "None", duration: TimeInterval(180)),
141 | Song(title: "Track Five", artist: "None", duration: TimeInterval(180)),
142 | Song(title: "Track Six", artist: "None", duration: TimeInterval(180)),
143 | Song(title: "Track Seven", artist: "None", duration: TimeInterval(180)),
144 | Song(title: "Track Eight", artist: "None", duration: TimeInterval(180)),
145 | Song(title: "Track Nine", artist: "None", duration: TimeInterval(180)),
146 | Song(title: "Track Ten", artist: "None", duration: TimeInterval(180)),
147 | ]
148 | }
149 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Example2/SpotifyView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - SpotifyHomeView
4 |
5 | struct SpotifyView: View {
6 |
7 | var body: some View {
8 |
9 | SpotifyHomeView(sections: NetworkManager.shared.fetchHomeScreen())
10 | }
11 | }
12 |
13 | // MARK: - SpotifyHomeView_Previews
14 |
15 | struct SpotifyHomeView_Previews: PreviewProvider {
16 |
17 | static var previews: some View {
18 | SpotifyView()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Example2/Views/AlbumScroll.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - AlbumScroll
4 |
5 | struct AlbumScroll: View {
6 | @State var section: HomeViewSection
7 |
8 | // add this later too (add spacing elements)
9 | @State var hPadding: CGFloat
10 |
11 | var body: some View {
12 | VStack(alignment: .leading) {
13 | HomeSectionHeader(header: section.title, hPadding: hPadding)
14 | ScrollView(.horizontal, showsIndicators: false) {
15 | HStack(spacing: 18) {
16 | Color.clear
17 | .frame(width: hPadding - 18)
18 | ForEach(section.albums, id: \.self) { album in
19 | AlbumThumbnail(album: album)
20 | }
21 | }
22 | }.frame(height: 150) // add after showing scroll
23 | }
24 | }
25 | }
26 |
27 | // MARK: - AlbumScroll_Previews
28 |
29 | struct AlbumScroll_Previews: PreviewProvider {
30 | static var previews: some View {
31 | AlbumScroll(section: NetworkManager.shared.fetchHomeScreen().first!, hPadding: 24)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Example2/Views/AlbumThumbnail.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - AlbumThumbnail
4 |
5 | struct AlbumThumbnail: View {
6 | @State var album: Album
7 |
8 | var body: some View {
9 | VStack(alignment: .leading) {
10 | // Image(album.name)
11 | Image(album.cover)
12 | .resizable()
13 | .frame(width: 130, height: 130)
14 | Text(album.title)
15 | .font(.caption)
16 | .foregroundColor(.white)
17 | }
18 | }
19 | }
20 |
21 | // MARK: - AlbumThumbnail_Previews
22 |
23 | struct AlbumThumbnail_Previews: PreviewProvider {
24 | static var previews: some View {
25 | AlbumThumbnail(album: NetworkManager.shared.fetchHomeScreen().first!.albums.first!)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Example2/Views/HomeHeaderView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - HomeHeaderView
4 |
5 | struct HomeHeaderView: View {
6 |
7 | @State var hPadding: CGFloat
8 | @State var vPadding: CGFloat
9 | @Binding var scrollOffset: CGFloat
10 |
11 | var opacity: Double {
12 |
13 | return Double(1 + scrollOffset * 0.01)
14 | }
15 |
16 | var body: some View {
17 | VStack(alignment: .trailing) {
18 | HStack(alignment: .top) {
19 | Spacer()
20 |
21 | Image(systemName: "gearshape")
22 | .foregroundColor(.white)
23 | }
24 | .padding(EdgeInsets(top: vPadding, leading: 0, bottom: 0, trailing: hPadding))
25 | Spacer()
26 | }
27 | .frame(height: 500)
28 | .background(
29 | RadialGradient(
30 | gradient: Gradient(colors: [Color.white, Color.clear]),
31 | center: UnitPoint(x: 0, y: -0.9),
32 | startRadius: 200,
33 | endRadius: 900))
34 | .opacity(opacity)
35 | }
36 | }
37 |
38 | // MARK: - HomeHeaderView_Previews
39 |
40 | struct HomeHeaderView_Previews: PreviewProvider {
41 | static var previews: some View {
42 | HomeHeaderView(hPadding: 24, vPadding: 0, scrollOffset: Binding.constant(.zero))
43 | .preferredColorScheme(.dark)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Example2/Views/HomeSectionHeader.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - HomeSectionHeader
4 |
5 | struct HomeSectionHeader: View {
6 | @State var header: String
7 | @State var hPadding: CGFloat
8 |
9 | var body: some View {
10 | Text(header)
11 | .font(.title)
12 | .foregroundColor(.white)
13 | .fontWeight(.bold)
14 | .padding(EdgeInsets(top: 0, leading: hPadding, bottom: 0, trailing: 0))
15 | }
16 | }
17 |
18 | // MARK: - HomeSectionHeader_Previews
19 |
20 | struct HomeSectionHeader_Previews: PreviewProvider {
21 | static var previews: some View {
22 | HomeSectionHeader(header: "Recently Played", hPadding: 24)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Example2/Views/PremiumBannerView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - PremiumBannerView
4 |
5 | struct PremiumBannerView: View {
6 |
7 | @State var isRotating = false
8 |
9 | var body: some View {
10 |
11 | ZStack {
12 |
13 | Image("Vibes")
14 | .resizable()
15 | .aspectRatio(contentMode: .fit)
16 | .scaleEffect(1.25)
17 | .rotationEffect(.init(degrees: isRotating ? 0 : 360))
18 | .animation(animation)
19 | .onAppear {
20 |
21 | isRotating = true
22 | }
23 | .mask(
24 |
25 | Text("Spotify \nPremium")
26 | .font(.system(size: 52))
27 | .fontWeight(.black)
28 | .frame(maxWidth: .infinity, alignment: .leading))
29 | .padding()
30 |
31 | VStack {
32 |
33 | Button(action: {}, label: {
34 |
35 | HStack {
36 |
37 | HStack {
38 |
39 | Text("Start the Party")
40 | .font(.system(size: 20))
41 | .foregroundColor(.black)
42 |
43 | Image(systemName: "arrow.right")
44 | .foregroundColor(.black)
45 |
46 | }
47 | .padding([.leading, .trailing], 8)
48 | .padding(8)
49 | .background(Color.white)
50 | .cornerRadius(30)
51 |
52 |
53 | Spacer()
54 | }
55 | })
56 | .buttonStyle(PlainButtonStyle())
57 |
58 | Text("All the best music. Free for 30 days.")
59 | .frame(maxWidth: .infinity, alignment: .leading)
60 | .foregroundColor(.white)
61 |
62 | }
63 | .padding()
64 | .padding(.top, 250)
65 | }
66 | }
67 |
68 | var animation: Animation {
69 |
70 | Animation.linear(duration: 20)
71 | .repeatForever(autoreverses: false)
72 | }
73 | }
74 |
75 | // MARK: - PremiumBannerView_Previews
76 |
77 | struct PremiumBannerView_Previews: PreviewProvider {
78 | static var previews: some View {
79 | PremiumBannerView()
80 | .preferredColorScheme(.dark)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Example2/Views/QuickPlayGrid.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - QuickPlayGrid
4 |
5 | struct QuickPlayGrid: View {
6 |
7 | struct QuickPlayCell: View {
8 | var album: Album
9 |
10 | var body: some View {
11 | HStack {
12 | AlbumArt
13 | AlbumInfo
14 | }
15 | .frame(height: 60)
16 | .background(Color.white.opacity(0.2))
17 | .cornerRadius(8)
18 | }
19 |
20 | var AlbumArt: some View {
21 | Image(album.cover)
22 | .resizable()
23 | .frame(width: 60, height: 60)
24 | }
25 |
26 | var AlbumInfo: some View {
27 | HStack {
28 | Text(album.title)
29 | .font(.caption)
30 | .fontWeight(.medium)
31 | .lineLimit(2)
32 | .foregroundColor(.white)
33 |
34 | Spacer()
35 | Image(systemName: "shuffle")
36 | .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 8))
37 | .imageScale(.small)
38 | .foregroundColor(.white)
39 | }
40 | }
41 | }
42 |
43 | @State var section: HomeViewSection
44 | @State var hPadding: CGFloat
45 |
46 | let columns = [
47 | GridItem(.flexible(), spacing: 16, alignment: .leading),
48 | GridItem(.flexible(), spacing: 16, alignment: .leading),
49 | ]
50 |
51 | var body: some View {
52 | VStack(alignment: .leading) {
53 | HomeSectionHeader(header: section.title, hPadding: hPadding)
54 | LazyVGrid(columns: columns, spacing: 16) {
55 | ForEach(section.albums, id: \.self) { album in
56 | QuickPlayCell(album: album)
57 | }
58 | }.padding(EdgeInsets(top: 0, leading: hPadding, bottom: 0, trailing: hPadding))
59 | }
60 | }
61 | }
62 |
63 | // MARK: - QuickPlayGrid_Previews
64 |
65 | struct QuickPlayGrid_Previews: PreviewProvider {
66 | static var previews: some View {
67 | QuickPlayGrid(section: NetworkManager.shared.fetchHomeScreen().first!, hPadding: 24)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Example2/Views/SpotifyHomeView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import ScrollViewReactiveHeader
3 |
4 | // MARK: - SpotifyHomeView
5 |
6 | struct SpotifyHomeView: View {
7 |
8 | struct TopSpacer: View {
9 | var topSpace: CGFloat
10 |
11 | var body: some View {
12 | HStack {}
13 | .frame(width: 50, height: topSpace)
14 | }
15 | }
16 |
17 | struct OffsetReader: View {
18 | var body: some View {
19 | Color.clear
20 | .background(
21 | GeometryReader { geo in
22 | Color.clear.preference(
23 | key: ScrollOffsetPreferenceKey.self,
24 | value: geo.frame(in: .named("scrollView")).minY)
25 | }).frame(height: 0)
26 | }
27 | }
28 |
29 | @State var sections: [HomeViewSection]
30 | @State var hPadding: CGFloat = 24
31 | @State var vPadding: CGFloat = 46
32 | @State var scrollViewOffset: CGFloat = .zero
33 |
34 | var body: some View {
35 |
36 | ScrollViewReactiveHeader(header: {
37 |
38 | HeaderView()
39 | .frame(height: 400)
40 | }, headerOverlay: {
41 |
42 | PremiumBannerView()
43 | }, body: {
44 |
45 | LazyVStack(spacing: vPadding) {
46 |
47 | TopSpacer(topSpace: 0)
48 | ForEach(sections, id: \.order) { section in
49 | cellView(section: section)
50 | }
51 | }
52 | }, configuration: .init(showStatusBar: true, backgroundColor: .black))
53 | .background(Color.black)
54 | }
55 |
56 | func cellView(section: HomeViewSection) -> AnyView {
57 | switch section.type {
58 | case .albumScroll:
59 | return AnyView(AlbumScroll(section: section, hPadding: 24))
60 | case .quickShuffle:
61 | return AnyView(QuickPlayGrid(section: section, hPadding: 24))
62 | }
63 | }
64 | }
65 |
66 | fileprivate struct HeaderView: View {
67 |
68 | var body: some View {
69 |
70 | Image("grainy")
71 | .resizable()
72 | .aspectRatio(contentMode: .fill)
73 | .opacity(0.35)
74 | }
75 | }
76 |
77 | // MARK: - ScrollOffsetPreferenceKey
78 |
79 | private struct ScrollOffsetPreferenceKey: PreferenceKey {
80 | static var defaultValue: CGFloat = .zero
81 |
82 | static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {}
83 | }
84 |
85 | // MARK: - HomeView_Previews
86 |
87 | struct HomeView_Previews: PreviewProvider {
88 | static var previews: some View {
89 | SpotifyHomeView(sections: NetworkManager.shared.fetchHomeScreen())
90 | .preferredColorScheme(.dark)
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Example3/StoryList/Model/Story.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct Story: Identifiable {
4 |
5 | let id: Int
6 |
7 | let title: String
8 | let description: String
9 |
10 | let reads: Int
11 | let favorites: Int
12 |
13 | static let peculiar: Story = .init(id: 0, title: "The Peculiar Guillotine", description: "Alex Parkes had always loved urban Sleepford with its dark, delicious ditches. It was a place where he felt barmy.", reads: 13, favorites: 4)
14 | static let teapot: Story = .init(id: 1, title: "The Damp Teapot", description: "James Rockatansky looked at the damp teapot in his hands and felt unstable.", reads: 8, favorites: 2)
15 | static let elixir: Story = .init(id: 2, title: "The Elixir", description: "Tristan gulped. He glanced at his own reflection. He was a giving, greedy, squash drinker with spiky fingers and skinny fingers", reads: 16, favorites: 5)
16 | static let piano: Story = .init(id: 3, title: "The Warped Piano", description: "Alex Raymond was thinking about Michelle Blast again. Michelle was a brave volcano with short arms and fiery gaze.", reads: 21, favorites: 4)
17 | static let windingPath: Story = .init(id: 4, title: "The Winding Path", description: "Alex Smart walked over to the window and reflected on his rural surroundings. He had always loved his hometown.", reads: 8, favorites: 1)
18 | static let darkWeather: Story = .init(id: 5, title: "Darker Nights", description: "The hail pounded like bopping donkeys, making Suki puzzled. Suki grabbed a bendy gun that had been strewn nearby.", reads: 24, favorites: 11)
19 | }
20 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Example3/StoryList/Model/StoryListViewModel.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct StoryListViewModel {
4 |
5 | let stories: [Story]
6 |
7 | let header: String = "Our Newest Stories"
8 |
9 | static let example: StoryListViewModel = .init(stories: [.darkWeather, .elixir, .peculiar, .piano, .teapot, .windingPath])
10 | }
11 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Example3/StoryList/Views/StoryListCell.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - StoryListCell
4 |
5 | struct StoryListCell: View {
6 |
7 | let viewModel: Story
8 | let iconWidth: CGFloat = 30
9 | let labelWidth: CGFloat = 20
10 |
11 | var body: some View {
12 |
13 | HStack {
14 |
15 | VStack {
16 |
17 | Text(viewModel.title)
18 | .font(.system(size: 16, weight: .semibold, design: .serif))
19 | .frame(maxWidth: .infinity, alignment: .leading)
20 |
21 | Text(viewModel.description)
22 | .font(.system(size: 13, weight: .regular, design: .serif))
23 | .frame(maxWidth: .infinity, alignment: .leading)
24 | }
25 |
26 | VStack(alignment: .trailing) {
27 |
28 | HStack {
29 |
30 | Image(systemName: "book")
31 | .frame(width: iconWidth)
32 | Text(String(viewModel.reads))
33 | .font(.system(size: 15))
34 | .frame(width: labelWidth)
35 | }
36 |
37 | Divider()
38 |
39 | HStack {
40 |
41 | Image(systemName: "bookmark")
42 | .frame(width: iconWidth)
43 | Text(String(viewModel.favorites))
44 | .font(.system(size: 15))
45 | .frame(width: labelWidth)
46 | }
47 | }
48 | .frame(width: 60)
49 | .opacity(0.5)
50 | }.padding()
51 | .overlay(
52 | RoundedRectangle(cornerRadius: 6, style: .continuous)
53 | .stroke(Color.black.opacity(0.1), lineWidth: 2))
54 | }
55 | }
56 |
57 | // MARK: - StoryListCell_Previews
58 |
59 | struct StoryListCell_Previews: PreviewProvider {
60 | static var previews: some View {
61 |
62 | StoryListCell(viewModel: .peculiar)
63 | .padding()
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Example3/StoryList/Views/StoryListContentView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - StoryListContentView
4 |
5 | struct StoryListContentView: View {
6 |
7 | let viewModel: StoryListViewModel
8 |
9 | var body: some View {
10 |
11 | VStack {
12 |
13 | ForEach(viewModel.stories) { story in
14 |
15 | StoryListCell(viewModel: story)
16 | }
17 |
18 | ForEach(viewModel.stories) { story in
19 |
20 | StoryListCell(viewModel: story)
21 | }
22 | }.padding()
23 | }
24 | }
25 |
26 | // MARK: - StoryListContentView_Previews
27 |
28 | struct StoryListContentView_Previews: PreviewProvider {
29 | static var previews: some View {
30 |
31 | StoryListContentView(viewModel: StoryListViewModel.example)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Example3/StoryList/Views/StoryListHeaderOverlay.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - StoryListHeaderOverlay
4 |
5 | struct StoryListHeaderOverlay: View {
6 |
7 | let header: String
8 |
9 | var body: some View {
10 |
11 | VStack(alignment: .center) {
12 |
13 | Spacer()
14 |
15 | ZStack {
16 |
17 | Rectangle()
18 | .fill(Color.yellow)
19 | .offset(x: -24, y: 0)
20 | .rotationEffect(.init(degrees: -1))
21 | .frame(width: 112, height: 30)
22 | .opacity(0.7)
23 |
24 | Text(header)
25 | .font(.system(size: 32, weight: .semibold, design: .serif))
26 | .frame(maxWidth: .infinity)
27 | }
28 |
29 | Spacer()
30 | }
31 | }
32 | }
33 |
34 | // MARK: - StoryListHeaderOverlay_Previews
35 |
36 | struct StoryListHeaderOverlay_Previews: PreviewProvider {
37 | static var previews: some View {
38 | StoryListHeaderOverlay(header: "Our Newest Stories")
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Example3/StoryListView.swift:
--------------------------------------------------------------------------------
1 | import ScrollViewReactiveHeader
2 | import SwiftUI
3 |
4 | // MARK: - StoryListView
5 |
6 | struct StoryListView: View {
7 |
8 | let viewModel: StoryListViewModel = .example
9 |
10 | var body: some View {
11 |
12 | ScrollViewReactiveHeader(header: {
13 |
14 | HeaderView()
15 | .frame(height: 300)
16 | }, headerOverlay: {
17 |
18 | StoryListHeaderOverlay(header: viewModel.header)
19 | .frame(height: 300)
20 | }, body: {
21 |
22 | StoryListContentView(viewModel: viewModel)
23 | }, configuration: .init(showStatusBar: true, backgroundColor: .white))
24 | }
25 | }
26 |
27 | // MARK: - HeaderView
28 |
29 | fileprivate struct HeaderView: View {
30 |
31 | var body: some View {
32 |
33 | Image("background3")
34 | .resizable()
35 | .aspectRatio(contentMode: .fill)
36 | .opacity(0.35)
37 | }
38 | }
39 |
40 |
41 | // MARK: - StoryListView_Previews
42 |
43 | struct StoryListView_Previews: PreviewProvider {
44 | static var previews: some View {
45 | StoryListView()
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/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 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/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" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/Vibes.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Vibes.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/Vibes.imageset/Vibes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftui-library/scrollview-reactive-header/00fe1bb5876a982a1007e40e94c50b85aea1cc22/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/Vibes.imageset/Vibes.png
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/a-tribe-called-quest.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "a-tribe-called-quest.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/a-tribe-called-quest.imageset/a-tribe-called-quest.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftui-library/scrollview-reactive-header/00fe1bb5876a982a1007e40e94c50b85aea1cc22/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/a-tribe-called-quest.imageset/a-tribe-called-quest.jpg
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/abbey-road.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "abbey-road.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/abbey-road.imageset/abbey-road.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftui-library/scrollview-reactive-header/00fe1bb5876a982a1007e40e94c50b85aea1cc22/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/abbey-road.imageset/abbey-road.jpg
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/background3.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "background.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/background3.imageset/background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftui-library/scrollview-reactive-header/00fe1bb5876a982a1007e40e94c50b85aea1cc22/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/background3.imageset/background.jpg
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/banana.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "banana.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/banana.imageset/banana.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftui-library/scrollview-reactive-header/00fe1bb5876a982a1007e40e94c50b85aea1cc22/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/banana.imageset/banana.jpg
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/blue-train.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "blue-train.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/blue-train.imageset/blue-train.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftui-library/scrollview-reactive-header/00fe1bb5876a982a1007e40e94c50b85aea1cc22/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/blue-train.imageset/blue-train.jpg
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/boutique.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "boutique.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/boutique.imageset/boutique.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftui-library/scrollview-reactive-header/00fe1bb5876a982a1007e40e94c50b85aea1cc22/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/boutique.imageset/boutique.jpg
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/frank-sinatra.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "frank-sinatra.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/frank-sinatra.imageset/frank-sinatra.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftui-library/scrollview-reactive-header/00fe1bb5876a982a1007e40e94c50b85aea1cc22/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/frank-sinatra.imageset/frank-sinatra.jpg
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/funkadelic.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "funkadelic.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/funkadelic.imageset/funkadelic.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftui-library/scrollview-reactive-header/00fe1bb5876a982a1007e40e94c50b85aea1cc22/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/funkadelic.imageset/funkadelic.jpg
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/go-2.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "go-2.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/go-2.imageset/go-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftui-library/scrollview-reactive-header/00fe1bb5876a982a1007e40e94c50b85aea1cc22/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/go-2.imageset/go-2.jpg
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/grainy.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "grainy.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/grainy.imageset/grainy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftui-library/scrollview-reactive-header/00fe1bb5876a982a1007e40e94c50b85aea1cc22/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/grainy.imageset/grainy.jpg
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/heaven-or-vegas.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "heaven-or-vegas.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/heaven-or-vegas.imageset/heaven-or-vegas.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftui-library/scrollview-reactive-header/00fe1bb5876a982a1007e40e94c50b85aea1cc22/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/heaven-or-vegas.imageset/heaven-or-vegas.jpg
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/heroes.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "heroes.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/heroes.imageset/heroes.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftui-library/scrollview-reactive-header/00fe1bb5876a982a1007e40e94c50b85aea1cc22/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/heroes.imageset/heroes.jpg
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/jesus-of-cool.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "jesus-of-cool.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/jesus-of-cool.imageset/jesus-of-cool.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftui-library/scrollview-reactive-header/00fe1bb5876a982a1007e40e94c50b85aea1cc22/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/jesus-of-cool.imageset/jesus-of-cool.jpg
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/moving-pictures.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Rush-Moving-Pictures-Album-Cover-web-optimised-820.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/moving-pictures.imageset/Rush-Moving-Pictures-Album-Cover-web-optimised-820.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftui-library/scrollview-reactive-header/00fe1bb5876a982a1007e40e94c50b85aea1cc22/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/moving-pictures.imageset/Rush-Moving-Pictures-Album-Cover-web-optimised-820.jpg
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/night-sky.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "night-sky.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/night-sky.imageset/night-sky.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftui-library/scrollview-reactive-header/00fe1bb5876a982a1007e40e94c50b85aea1cc22/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/night-sky.imageset/night-sky.jpg
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/odessa.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "odessa.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/odessa.imageset/odessa.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftui-library/scrollview-reactive-header/00fe1bb5876a982a1007e40e94c50b85aea1cc22/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/odessa.imageset/odessa.jpg
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/peppers.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "peppers.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/peppers.imageset/peppers.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftui-library/scrollview-reactive-header/00fe1bb5876a982a1007e40e94c50b85aea1cc22/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/peppers.imageset/peppers.jpg
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/speaking-in-tongues.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "speaking-in-tongues.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/speaking-in-tongues.imageset/speaking-in-tongues.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftui-library/scrollview-reactive-header/00fe1bb5876a982a1007e40e94c50b85aea1cc22/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/speaking-in-tongues.imageset/speaking-in-tongues.jpg
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/unknown-pleasures.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "unknown-pleasures.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/unknown-pleasures.imageset/unknown-pleasures.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftui-library/scrollview-reactive-header/00fe1bb5876a982a1007e40e94c50b85aea1cc22/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/unknown-pleasures.imageset/unknown-pleasures.jpg
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/yeezus.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "yeezus.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/yeezus.imageset/yeezus.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftui-library/scrollview-reactive-header/00fe1bb5876a982a1007e40e94c50b85aea1cc22/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Assets.xcassets/yeezus.imageset/yeezus.jpg
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 |
28 | UIApplicationSupportsIndirectInputEvents
29 |
30 | UILaunchScreen
31 |
32 | UIRequiredDeviceCapabilities
33 |
34 | armv7
35 |
36 | UISupportedInterfaceOrientations
37 |
38 | UIInterfaceOrientationPortrait
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UISupportedInterfaceOrientations~ipad
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationPortraitUpsideDown
46 | UIInterfaceOrientationLandscapeLeft
47 | UIInterfaceOrientationLandscapeRight
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/Resource Files/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ScrollViewReactiveHeaderDemoApp/Sources/ScrollViewReactiveHeaderDemoAppApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct ScrollViewReactiveHeaderDemoAppApp: App {
5 | var body: some Scene {
6 | WindowGroup {
7 | ContentView()
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/Model/HeaderPreferenceKey.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | // MARK: - ScrollViewHeaderKey
5 |
6 | struct ScrollViewHeaderKey: PreferenceKey {
7 |
8 | typealias Value = ContentPreferenceData
9 |
10 | // MARK: Internal
11 |
12 | static var defaultValue: ContentPreferenceData = .init(rect: .zero)
13 |
14 | static func reduce(
15 | value: inout Value,
16 | nextValue: () -> Value) {
17 |
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/Model/ScrollViewConfiguration.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | public struct ScrollViewConfiguration {
5 |
6 | // MARK: Lifecycle
7 |
8 | public init(showStatusBar: Bool, backgroundColor: Color? = nil) {
9 |
10 | self.showStatusBar = showStatusBar
11 | self.backgroundColor = backgroundColor
12 | }
13 |
14 | // MARK: Internal
15 |
16 | /// If set to true, will fade in the status bar as your content reaches the top safe area.
17 | let showStatusBar: Bool
18 |
19 | /// Used to set the `ScrollView` and status bar background color.
20 | let backgroundColor: Color?
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Model/ScrollViewPreferenceKey.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | // MARK: - ScrollViewHeaderKey
5 |
6 | struct ScrollViewBodyKey: PreferenceKey {
7 |
8 | typealias Value = ContentPreferenceData
9 |
10 | // MARK: Internal
11 |
12 | static var defaultValue: ContentPreferenceData = .init(rect: .zero)
13 |
14 | static func reduce(
15 | value: inout Value,
16 | nextValue: () -> Value) {
17 |
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/ScrollViewReactiveHeader.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | public struct ScrollViewReactiveHeader: View where A: View, B: View, C: View {
5 |
6 | // MARK: Lifecycle
7 |
8 | public init(
9 | @ViewBuilder header: @escaping () -> A,
10 | @ViewBuilder headerOverlay: @escaping () -> B,
11 | @ViewBuilder body: @escaping () -> C,
12 | configuration: ScrollViewConfiguration) {
13 |
14 | self.header = header
15 | self.headerOverlay = headerOverlay
16 | bodyContent = body
17 | self.configuration = configuration
18 | }
19 |
20 | // MARK: Public
21 |
22 | public var body: some View {
23 |
24 | ZStack(alignment: .top) {
25 |
26 | GeometryReader { geometry in
27 |
28 | VStack(content: header)
29 | .overlay(GeometryReaderOverlay(key: ScrollViewHeaderKey.self))
30 | .offset(x: .zero, y: headerOffset)
31 | .scaleEffect(headerScale)
32 | .opacity(Double(headerOpacity))
33 | // Workaround to cover safe are without .edgesIgnoringSafeArea
34 | .background(backgroundColor.offset(x: 0, y: -topSafeArea * 2))
35 |
36 | VStack(content: headerOverlay)
37 |
38 | ScrollView {
39 |
40 | VStack {}
41 | .frame(height: headerHeight)
42 | .overlay(GeometryReaderOverlay(key: ScrollViewBodyKey.self))
43 |
44 | VStack(content: bodyContent)
45 | .background(backgroundColor)
46 | }
47 |
48 | if configuration.showStatusBar {
49 |
50 | Rectangle()
51 | .fill(backgroundColor)
52 | .opacity(statusBarOpacity)
53 | .frame(height: geometry.safeAreaInsets.top)
54 | .edgesIgnoringSafeArea(.top)
55 | .onAppear {
56 |
57 | topSafeArea = geometry.safeAreaInsets.top
58 | }
59 | }
60 | }
61 | .onPreferenceChange(ScrollViewHeaderKey.self, perform: { preference in
62 |
63 | guard preference.rect != .zero,
64 | headerHeight == .none else { return }
65 |
66 | headerHeight = preference.rect.height
67 | })
68 | }
69 | .background(backgroundColor)
70 | .coordinateSpace(name: "ReactiveHeader")
71 | .onPreferenceChange(ScrollViewBodyKey.self, perform: { preference in
72 |
73 | setStatusBarOpacity(offset: preference.rect.minY)
74 |
75 | setHeaderOpacity(preferenceRect: preference.rect)
76 | })
77 | }
78 |
79 | // MARK: Internal
80 |
81 | @Environment(\.colorScheme) var colorScheme
82 |
83 | var backgroundColor: Color {
84 |
85 | guard let backgroundColor = configuration.backgroundColor else {
86 |
87 | return colorScheme == .dark ? .black : .white
88 | }
89 |
90 | return backgroundColor
91 | }
92 |
93 | // MARK: Private
94 |
95 | private var header: () -> A
96 | private var headerOverlay: () -> B
97 | private var bodyContent: () -> C
98 | private var configuration: ScrollViewConfiguration
99 |
100 | @State private var headerHeight: CGFloat?
101 | @State private var headerOffset: CGFloat = .zero
102 | @State private var headerScale: CGFloat = 1
103 | @State private var headerOpacity: CGFloat = 1
104 |
105 | @State private var statusBarOpacity: Double = 0
106 | @State private var topSafeArea: CGFloat = 40
107 |
108 | private func setHeaderOpacity(preferenceRect: CGRect) {
109 |
110 | headerOffset = min(0, preferenceRect.minY / 10)
111 |
112 | headerScale = max(1, 1 + preferenceRect.minY / 500)
113 |
114 | guard let headerHeight = headerHeight else { return }
115 |
116 | let startingY = headerHeight / 2
117 |
118 | if abs(preferenceRect.minY) > startingY {
119 |
120 | headerOpacity = (1 - (abs(preferenceRect.minY) - startingY) / startingY)
121 | } else {
122 |
123 | headerOpacity = 1
124 | }
125 | }
126 |
127 | private func setStatusBarOpacity(offset: CGFloat) {
128 |
129 | guard let headerOffset = headerHeight else { return }
130 |
131 | let scrollOffset = offset + headerOffset
132 |
133 | switch scrollOffset {
134 |
135 | case topSafeArea ... topSafeArea + 20: statusBarOpacity = Double(-scrollOffset / 100.0)
136 | case ...topSafeArea: statusBarOpacity = 1
137 | default: statusBarOpacity = 0
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/Sources/Views/GeometryReaderOverlay.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | struct ContentPreferenceData: Equatable {
5 |
6 | let rect: CGRect
7 | }
8 |
9 | struct GeometryReaderOverlay: View where Key.Value == ContentPreferenceData {
10 |
11 | // MARK: Lifecycle
12 |
13 | public init(key: Key.Type, coordinateSpace: String = "ReactiveHeader") {
14 |
15 | self.coordinateSpace = coordinateSpace
16 | self.preferenceKey = key.self
17 | }
18 |
19 | // MARK: Public
20 |
21 | public var body: some View {
22 |
23 | GeometryReader { geometry in
24 |
25 | Rectangle().fill(Color.clear)
26 | .preference(
27 | key: preferenceKey.self,
28 | value: ContentPreferenceData(
29 | rect: geometry.frame(in: .named(coordinateSpace))))
30 | }
31 | }
32 |
33 | // MARK: Internal
34 |
35 | let coordinateSpace: String
36 | let preferenceKey: Key.Type
37 | }
38 |
--------------------------------------------------------------------------------
/Tests/ScrollViewReactiveHeaderTests/ScrollViewReactiveHeaderTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import ScrollViewReactiveHeader
3 |
4 | final class ScrollViewReactiveHeaderTests: XCTestCase {
5 | func testExample() {
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 | // XCTAssertEqual(ScrollViewReactiveHeader().text, "Hello, World!")
10 | }
11 | }
12 |
--------------------------------------------------------------------------------