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