├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Example └── Example.swiftpm │ ├── .swiftpm │ └── xcode │ │ └── package.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── ContentView.swift │ ├── ExampleApp.swift │ ├── Package.resolved │ ├── Package.swift │ ├── README.md │ └── SheetContentView.swift ├── LICENSE.md ├── Package.resolved ├── Package.swift ├── PageSheet.podspec ├── README.md └── Sources ├── PageSheet ├── Documentation.docc │ └── Documentation.md └── combined.swift ├── PageSheetCore ├── Documentation.docc │ ├── Documentation.md │ ├── PageSheetView.md │ ├── SheetPreference.md │ └── SheetPreferenceViewModifier.md ├── EnvironmentValues+PageSheet.swift ├── PageSheet.swift ├── PageSheetView.swift └── View+PageSheet.swift └── PageSheetPlus ├── Documentation.docc └── Documentation.md └── ViewModifierBuilder+PageSheet.swift /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | the discussion portion of this repository. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Legal 2 | 3 | By submitting a pull request, you represent that you have the right to license 4 | your contribution to Eric Lewis and the community, and agree by submitting the patch 5 | that your contributions are licensed under the MIT license (see 6 | `LICENSE.md`). 7 | 8 | ## How to submit a bug report 9 | 10 | Please ensure to specify the following: 11 | 12 | * PageSheet commit hash 13 | * Contextual information (e.g. what you were trying to achieve with PageSheet) 14 | * Simplest possible steps to reproduce 15 | * More complex the steps are, lower the priority will be. 16 | * Small project that reproduces the issue. 17 | * Anything that might be relevant in your opinion, such as: 18 | * Swift version or Xcode version 19 | * iOS version and Build Configuration 20 | * Stack Trace or Sentry output 21 | 22 | ## Writing a Patch 23 | 24 | A good PageSheet patch is: 25 | 26 | 1. Concise, and contains as few changes as needed to achieve the end result. 27 | 2. Tested, ensuring that any tests provided failed before the patch and pass after it. 28 | 3. Documented, adding API documentation as needed to cover new functions and properties. 29 | -------------------------------------------------------------------------------- /Example/Example.swiftpm/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example.swiftpm/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example.swiftpm/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 | -------------------------------------------------------------------------------- /Example/Example.swiftpm/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 | -------------------------------------------------------------------------------- /Example/Example.swiftpm/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Example.swiftpm/ContentView.swift: -------------------------------------------------------------------------------- 1 | import PageSheet 2 | import SwiftUI 3 | 4 | struct ContentView: View { 5 | @State 6 | private var sheetPresented = false 7 | 8 | var body: some View { 9 | NavigationView { 10 | List { 11 | Section { 12 | Button("Open Sheet") { 13 | sheetPresented = true 14 | } 15 | .disabled(sheetPresented) 16 | 17 | Button("Close Sheet") { 18 | sheetPresented = false 19 | } 20 | .disabled(!sheetPresented) 21 | 22 | } header: { 23 | Text("Actions") 24 | } 25 | } 26 | .pageSheet(isPresented: $sheetPresented) { 27 | SheetContentView() 28 | } 29 | .navigationTitle("PageSheet") 30 | } 31 | .navigationViewStyle(.stack) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Example/Example.swiftpm/ExampleApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct ExampleApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Example/Example.swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "ViewModifierBuilder", 6 | "repositoryURL": "https://github.com/ericlewis/ViewModifierBuilder", 7 | "state": { 8 | "branch": null, 9 | "revision": "fd502e33819caac97ee8321aaedfe7506e811794", 10 | "version": "0.1.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Example/Example.swiftpm/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.5 2 | 3 | // WARNING: 4 | // This file is automatically generated. 5 | // Do not edit it by hand because the contents will be replaced. 6 | 7 | import AppleProductTypes 8 | import PackageDescription 9 | 10 | let package = Package( 11 | name: "Example", 12 | platforms: [ 13 | .iOS("15.2") 14 | ], 15 | products: [ 16 | .iOSApplication( 17 | name: "Example", 18 | targets: ["AppModule"], 19 | bundleIdentifier: "com.Example", 20 | teamIdentifier: "F9PGNEMEHU", 21 | displayVersion: "1.0", 22 | bundleVersion: "1", 23 | iconAssetName: "AppIcon", 24 | accentColorAssetName: "AccentColor", 25 | supportedDeviceFamilies: [ 26 | .pad, 27 | .phone, 28 | ], 29 | supportedInterfaceOrientations: [ 30 | .portrait, 31 | .landscapeRight, 32 | .landscapeLeft, 33 | .portraitUpsideDown(.when(deviceFamilies: [.pad])), 34 | ] 35 | ) 36 | ], 37 | dependencies: [ 38 | .package(path: "../../") 39 | ], 40 | targets: [ 41 | .executableTarget( 42 | name: "AppModule", 43 | dependencies: [ 44 | "PageSheet", 45 | ], 46 | path: "." 47 | ) 48 | ] 49 | ) 50 | -------------------------------------------------------------------------------- /Example/Example.swiftpm/README.md: -------------------------------------------------------------------------------- 1 | # `PageSheet` Example App 2 | 3 | A simple container app to demonstrate how to use & customize `PageSheet`. 4 | -------------------------------------------------------------------------------- /Example/Example.swiftpm/SheetContentView.swift: -------------------------------------------------------------------------------- 1 | import PageSheet 2 | import SwiftUI 3 | 4 | struct SheetContentView: View { 5 | @Environment(\.dismiss) 6 | private var dismiss 7 | 8 | @State 9 | private var detents: [PageSheet.Detent] = [.medium(), .large()] 10 | 11 | @State 12 | private var grabberVisible = true 13 | 14 | @State 15 | private var dismissDisabled = false 16 | 17 | @State 18 | private var childPresented = false 19 | 20 | @State 21 | private var prefersScrollingExpandsWhenScrolledToEdge = true 22 | 23 | @State 24 | private var prefersEdgeAttachedInCompactHeight = false 25 | 26 | @State 27 | private var widthFollowsPreferredContentSizeWhenEdgeAttached = false 28 | 29 | @State 30 | private var selectedDetentId: PageSheet.Detent.Identifier? = nil 31 | 32 | @State 33 | private var largestUndimmedDetentId: PageSheet.Detent.Identifier? = nil 34 | 35 | @Environment(\.selectedDetentIdentifier) 36 | private var selectedDetent 37 | 38 | var body: some View { 39 | NavigationView { 40 | List { 41 | Section { 42 | Text(selectedDetent?.rawValue ?? "nil") 43 | Button("Set detent to nil") { 44 | selectedDetentId = nil 45 | } 46 | Button("Set detent to medium") { 47 | selectedDetentId = .medium 48 | } 49 | Button("Set detent to large") { 50 | selectedDetentId = .large 51 | } 52 | } header: { 53 | Text("Selected Detent Identifier") 54 | } 55 | Section { 56 | Toggle("Grabber visible", isOn: $grabberVisible) 57 | Toggle("Dismiss disabled", isOn: $dismissDisabled) 58 | Toggle( 59 | "Scrolling expands when scrolled to edge", 60 | isOn: $prefersScrollingExpandsWhenScrolledToEdge) 61 | Toggle("Edge attached in compact height", isOn: $prefersEdgeAttachedInCompactHeight) 62 | Toggle( 63 | "Width follows preferred content size when edge attached", 64 | isOn: $widthFollowsPreferredContentSizeWhenEdgeAttached) 65 | } header: { 66 | Text("Toggles") 67 | } 68 | Section { 69 | Button("Large detent only") { detents = [.large()] } 70 | Button("Medium detent only") { detents = [.medium()] } 71 | Button("Medium & Large detent") { detents = [.medium(), .large()] } 72 | } header: { 73 | Text("Supported Detents") 74 | } 75 | Section { 76 | Button("Default detent") { largestUndimmedDetentId = nil } 77 | Button("Medium detent") { largestUndimmedDetentId = .medium } 78 | Button("Large detent") { largestUndimmedDetentId = .large } 79 | } header: { 80 | Text("Parent View Interaction") 81 | } 82 | Section { 83 | Button("Open child sheet") { 84 | childPresented = true 85 | } 86 | } 87 | } 88 | .toolbar { 89 | ToolbarItem(placement: .cancellationAction) { 90 | Button("Dismiss") { 91 | dismiss() 92 | } 93 | } 94 | } 95 | .listStyle(.insetGrouped) 96 | .navigationTitle("Sheet View") 97 | .navigationBarTitleDisplayMode(.inline) 98 | .interactiveDismissDisabled(dismissDisabled) 99 | .sheetPreferences { 100 | .detents(detents); 101 | .grabberVisible(grabberVisible); 102 | .selectedDetent(id: selectedDetentId); 103 | .largestUndimmedDetent(id: largestUndimmedDetentId); 104 | .scrollingExpandsWhenScrolledToEdge(prefersScrollingExpandsWhenScrolledToEdge); 105 | .edgeAttachedInCompactHeight(prefersEdgeAttachedInCompactHeight); 106 | .widthFollowsPreferredContentSizeWhenEdgeAttached(widthFollowsPreferredContentSizeWhenEdgeAttached); 107 | .largestUndimmedDetent(id: largestUndimmedDetentId); 108 | } 109 | } 110 | .pageSheet(isPresented: $childPresented) { 111 | SheetContentView() 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Eric Lewis. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "ViewModifierBuilder", 6 | "repositoryURL": "https://github.com/ericlewis/ViewModifierBuilder", 7 | "state": { 8 | "branch": null, 9 | "revision": "fd502e33819caac97ee8321aaedfe7506e811794", 10 | "version": "0.1.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "PageSheet", 7 | platforms: [ 8 | .iOS("15.0") 9 | ], 10 | products: [ 11 | .library( 12 | name: "PageSheet", 13 | targets: ["PageSheet"]), 14 | .library( 15 | name: "PageSheetCore", 16 | targets: ["PageSheetCore"]), 17 | .library( 18 | name: "PageSheetPlus", 19 | targets: ["PageSheetPlus"]), 20 | ], 21 | dependencies: [ 22 | .package( 23 | name: "ViewModifierBuilder", 24 | url: "https://github.com/ericlewis/ViewModifierBuilder", .upToNextMajor(from: "0.1.0")) 25 | ], 26 | targets: [ 27 | .target( 28 | name: "PageSheet", 29 | dependencies: ["PageSheetPlus"] 30 | ), 31 | .target( 32 | name: "PageSheetCore", 33 | dependencies: [] 34 | ), 35 | .target( 36 | name: "PageSheetPlus", 37 | dependencies: [ 38 | "PageSheetCore", 39 | "ViewModifierBuilder", 40 | ] 41 | ), 42 | ] 43 | ) 44 | -------------------------------------------------------------------------------- /PageSheet.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "PageSheet" 3 | s.version = "1.2.4" 4 | s.summary = "Customizable sheets using UISheetPresentationController in SwiftUI." 5 | s.description = <<-DESC 6 | Customizable sheet presentations in SwiftUI. Using UISheetPresentationController under the hood. 7 | DESC 8 | s.homepage = "https://github.com/ericlewis/PageSheet" 9 | s.license = { :type => "MIT", :file => "LICENSE.md" } 10 | s.author = { "Eric Lewis" => "ericlewis777@gmail.com" } 11 | s.social_media_url = "https://twitter.com/ericlewis" 12 | s.ios.deployment_target = "15.0" 13 | s.source = { :git => "https://github.com/ericlewis/pagesheet.git", :tag => s.version.to_s } 14 | s.source_files = "Sources/**/*" 15 | end 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PageSheet 2 | 3 | # NOTE: This repo isn't maintained due to swiftui taking on *most* of the features we supported here. Happy to hand the repo to someone who really does need it. 4 | 5 | [![](https://img.shields.io/badge/Swift_Package_Manager-compatible-ed702d.svg?style=flat)](https://github.com/apple/swift-package-manager) 6 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fericlewis%2FPageSheet%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/ericlewis/PageSheet) 7 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fericlewis%2FPageSheet%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/ericlewis/PageSheet) 8 | 9 | #### Customizable sheet presentations in SwiftUI. Using [`UISheetPresentationController`](https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller) under the hood. 10 | 11 | ### Features 12 | - Uses the default [`sheet`](https://developer.apple.com/documentation/swiftui/view/sheet(ispresented:ondismiss:content:)) API under the hood, ensuring maximum compatibility & stability. 13 | - Exposes the *exact same* API as the default SwiftUI [`sheet`](https://developer.apple.com/documentation/swiftui/view/sheet(ispresented:ondismiss:content:)) implementation. 14 | - No hacks, follows the best practices for creating representable views in SwiftUI. 15 | - Configurable using view modifiers, can configure [`UISheetPresentationController`](https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller) 16 | from any child views in the presented sheet's content view. 17 | - Works with the [`interactiveDismissDisabled(_:Bool)`](https://developer.apple.com/documentation/swiftui/view/interactivedismissdisabled(_:)) modifier. 18 | - Exposes all of the [`UISheetPresentationController`](https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller) configuration options. 19 | - Track the currently selected detent using an [`Environment`](https://developer.apple.com/documentation/swiftui/environment) value. 20 | - [Well documented API](#documentation), following a similar approach to the Developer Documentation. 21 | - Small footprint, [`~44kB`](https://www.emergetools.com/) thin when installed via SwiftPM. 22 | 23 | ## Table of Contents 24 | * [Requirements](#requirements) 25 | * [Installation](#installation) 26 | * [Xcode](#xcode) 27 | * [Swift Package Manager](#swift-package-manager) 28 | * [Examples](#examples) 29 | * [Example Project](#example-project) 30 | * [Presentation](#presentation) 31 | * [Customization](#customization) 32 | * [Module Documentation](#module-documentation) 33 | * [PageSheet](#pagesheet) 34 | * [PageSheetCore](https://pagesheetcore.swifty.sh/documentation/) 35 | * [PageSheetPlus](https://pagesheetplus.swifty.sh/documentation/) 36 | * [License](#license) 37 | 38 | # Requirements 39 | The codebase supports iOS and requires Xcode 12.0 or newer 40 | 41 | # Installation 42 | ## Xcode 43 | Open your project. Navigate to `File > Swift Packages > Add Package Dependency`. Enter the url `https://github.com/ericlewis/PageSheet` and tap `Next`. 44 | Select the `PageSheet` target and press `Add Package`. 45 | 46 | ## Swift Package Manager 47 | Add the following line to the `dependencies` in your `Package.swift` file: 48 | ```swift 49 | .package(url: "https://github.com/ericlewis/PageSheet.git", .upToNextMajor(from: "1.0.0")) 50 | ``` 51 | Next, add `PageSheet` as a dependency for your targets: 52 | ```swift 53 | .target(name: "AppTarget", dependencies: ["PageSheet"]) 54 | ``` 55 | A completed example may look like this: 56 | ```swift 57 | // swift-tools-version:5.5 58 | 59 | import PackageDescription 60 | 61 | let package = Package( 62 | name: "App", 63 | dependencies: [ 64 | .package( 65 | url: "https://github.com/ericlewis/PageSheet.git", 66 | .upToNextMajor(from: "1.0.0")) 67 | ], 68 | targets: [ 69 | .target( 70 | name: "AppTarget", 71 | dependencies: ["PageSheet"]) 72 | ] 73 | ) 74 | ``` 75 | 76 | # Examples 77 | ## Example Project 78 | If you are using Xcode 13.2.1 you can navigate to the [`Example`](Example) folder and open the enclosed Swift App Playground to test various features (and see how they are implemented). 79 | 80 | ## Presentation 81 | `PageSheet` works similarly to a typical [`sheet`](https://developer.apple.com/documentation/SwiftUI/View/sheet(isPresented:onDismiss:content:)) view modifier. 82 | ```swift 83 | import SwiftUI 84 | import PageSheet 85 | 86 | struct ContentView: View { 87 | @State 88 | private var sheetPresented = false 89 | 90 | var body: some View { 91 | Button("Open Sheet") { 92 | sheetPresented = true 93 | } 94 | .pageSheet(isPresented: $sheetPresented) { 95 | Text("Hello!") 96 | } 97 | } 98 | } 99 | ``` 100 | 101 | `PageSheet` also supports presentation via conditional `Identifiable` objects. 102 | ```swift 103 | import SwiftUI 104 | import PageSheet 105 | 106 | struct ContentView: View { 107 | @State 108 | private var string: String? 109 | 110 | var body: some View { 111 | Button("Open Sheet") { 112 | string = "Hello!" 113 | } 114 | .pageSheet(item: $string) { unwrappedString in 115 | Text(unwrappedString) 116 | } 117 | } 118 | } 119 | 120 | extension String: Identifiable { 121 | public var id: String { self } 122 | } 123 | ``` 124 | 125 | ### Customization 126 | `PageSheet` can also be customized using a collection of view modifiers applied to the sheet's content. 127 | ```swift 128 | import SwiftUI 129 | import PageSheet 130 | 131 | struct ContentView: View { 132 | @State 133 | private var sheetPresented = false 134 | 135 | var body: some View { 136 | Button("Open Sheet") { 137 | sheetPresented = true 138 | } 139 | .pageSheet(isPresented: $sheetPresented) { 140 | Text("Hello!") 141 | .preferGrabberVisible(true) 142 | } 143 | } 144 | } 145 | ``` 146 | 147 | # Module Documentation 148 | ### PageSheet 149 | `PageSheet` is split into three different modules, with `PageSheetCore` handling implementation 150 | and `PageSheetPlus` providing a new modifier called `sheetPreferences(_:)`. 151 | The namesake module is `PageSheet`, which combines `PageSheetCore` & `PageSheetPlus` into a single import. 152 | 153 | If you want to only use PageSheet without the fancy extra modifier (and [extra dependency](https://github.com/ericlewis/ViewModifierBuilder)), then use `PageSheetCore`. 154 | ### [PageSheetCore](https://pagesheetcore.swifty.sh/documentation/) 155 | ### [PageSheetPlus](https://pagesheetplus.swifty.sh/documentation/) 156 | 157 | # License 158 | PageSheet is released under the MIT license. See [LICENSE](LICENSE.md) for details. 159 | -------------------------------------------------------------------------------- /Sources/PageSheet/Documentation.docc/Documentation.md: -------------------------------------------------------------------------------- 1 | # ``PageSheet`` 2 | 3 | Rexports ``PageSheetCore`` & ``PageSheetPlus``. 4 | -------------------------------------------------------------------------------- /Sources/PageSheet/combined.swift: -------------------------------------------------------------------------------- 1 | @_exported import PageSheetCore 2 | @_exported import PageSheetPlus 3 | -------------------------------------------------------------------------------- /Sources/PageSheetCore/Documentation.docc/Documentation.md: -------------------------------------------------------------------------------- 1 | # ``PageSheetCore`` 2 | 3 | Customizable sheet presentations in SwiftUI. 4 | 5 | ### Features 6 | 7 | - Uses [`UISheetPresentationController`](https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller) 8 | - Uses the default [`sheet`](https://developer.apple.com/documentation/swiftui/view/sheet(ispresented:ondismiss:content:)) for presentation, ensuring maximum compatibility & stability. 9 | - Exposes the *exact same* API as the default SwiftUI [`sheet`](https://developer.apple.com/documentation/swiftui/view/sheet(ispresented:ondismiss:content:)) implementation. 10 | - No hacks, follows the best practices for creating represetable views in SwiftUI. 11 | - Configurable using view modifiers, can configure [`UISheetPresentationController`](https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller) 12 | from any child views in the presented sheet's content view. 13 | - Works with the [`interactiveDismissDisabled(_:Bool)`](https://developer.apple.com/documentation/swiftui/view/interactivedismissdisabled(_:)) modifier. 14 | - Exposes all of the [`UISheetPresentationController`](https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller) configuration options. 15 | - Track the currently selected detent using an [`Environment`](https://developer.apple.com/documentation/swiftui/environment) value. 16 | - Well documented API, following a similar approach to the Developer Documentation. 17 | - Small footprint, [`~44.0kB`](https://www.emergetools.com/) thin installed via SwiftPM. 18 | 19 | ### Open Source 20 | Check out the [GitHub Repo](https://github.com/ericlewis/PageSheet) to see how everything works. 21 | 22 | ## Topics 23 | 24 | ### Customization 25 | - ``SheetPreference`` 26 | - ``SheetPreferenceViewModifier`` 27 | 28 | ### Presentation 29 | - ``PageSheetView`` 30 | 31 | ### Supporting Types 32 | - ``PageSheet`` 33 | - ``PageSheet/Preference`` 34 | -------------------------------------------------------------------------------- /Sources/PageSheetCore/Documentation.docc/PageSheetView.md: -------------------------------------------------------------------------------- 1 | # ``PageSheetCore/PageSheetView`` 2 | 3 | ### Example 4 | 5 | ```swift 6 | import SwiftUI 7 | import PageSheet 8 | 9 | struct ContentView: View { 10 | @State 11 | private var sheetPresented = false 12 | 13 | var body: some View { 14 | VStack { 15 | Button("Present Sheet") { 16 | sheetPresented = true 17 | } 18 | } 19 | .sheet(isPresented: $sheetPresented) { 20 | PageSheetView { 21 | VStack { 22 | Text("Hello, world!") 23 | } 24 | .sheetPreference(.detents([.medium(), .large()])) 25 | .sheetPreference(.grabberVisible(true)) 26 | } 27 | } 28 | } 29 | } 30 | ``` 31 | 32 | ## Topics 33 | -------------------------------------------------------------------------------- /Sources/PageSheetCore/Documentation.docc/SheetPreference.md: -------------------------------------------------------------------------------- 1 | # ``PageSheetCore/SheetPreference`` 2 | 3 | ``SheetPreference`` is a bridge to the configuration used in [`UISheetPresentationController`](https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller). 4 | 5 | ### Example 6 | 7 | ```swift 8 | import SwiftUI 9 | import PageSheet 10 | 11 | struct ContentView: View { 12 | @State 13 | private var sheetPresented = false 14 | 15 | var body: some View { 16 | VStack { 17 | Button("Present Sheet") { 18 | sheetPresented = true 19 | } 20 | .pageSheet(isPresented: $sheetPresented) { 21 | VStack { 22 | Text("Hello, world!") 23 | } 24 | .sheetPreference(.grabberVisible(true)) 25 | } 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | ## Topics 32 | 33 | ### Specifying the Height 34 | - ``detents(_:)`` 35 | - ``selectedDetent(id:)`` 36 | - ``PageSheet/Detent`` 37 | 38 | ### Managing User Interaction 39 | - ``largestUndimmedDetent(id:)`` 40 | - ``scrollingExpandsWhenScrolledToEdge(_:)`` 41 | 42 | ### Managing the Appearance 43 | - ``grabberVisible(_:)`` 44 | - ``edgeAttachedInCompactHeight(_:)`` 45 | - ``widthFollowsPreferredContentSizeWhenEdgeAttached(_:)`` 46 | - ``cornerRadius(_:)`` 47 | -------------------------------------------------------------------------------- /Sources/PageSheetCore/Documentation.docc/SheetPreferenceViewModifier.md: -------------------------------------------------------------------------------- 1 | # ``PageSheetCore/SheetPreferenceViewModifier`` 2 | 3 | ## Topics 4 | 5 | ### Applying a Preference 6 | - ``init(_:)`` 7 | - ``body(content:)`` 8 | - ``preference`` 9 | -------------------------------------------------------------------------------- /Sources/PageSheetCore/EnvironmentValues+PageSheet.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | private struct SelectedDetentIdentifier: EnvironmentKey { 4 | static let defaultValue: PageSheet.Detent.Identifier? = nil 5 | } 6 | 7 | private struct PreferenceNamespace: EnvironmentKey { 8 | static let defaultValue: Namespace.ID? = nil 9 | } 10 | 11 | extension EnvironmentValues { 12 | internal var _selectedDetentIdentifier: PageSheet.Detent.Identifier? { 13 | get { self[SelectedDetentIdentifier.self] } 14 | set { self[SelectedDetentIdentifier.self] = newValue } 15 | } 16 | 17 | internal var _preferenceNamespace: Namespace.ID? { 18 | get { self[PreferenceNamespace.self] } 19 | set { self[PreferenceNamespace.self] = newValue } 20 | } 21 | } 22 | 23 | // MARK: - Public 24 | 25 | extension EnvironmentValues { 26 | /// The current ``Detent`` identifier of the sheet presentation associated with this environment. 27 | public var selectedDetentIdentifier: PageSheet.Detent.Identifier? { 28 | self[SelectedDetentIdentifier.self] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/PageSheetCore/PageSheet.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | // MARK: - AutomaticPreferenceKey 4 | 5 | private protocol AutomaticPreferenceKey: PreferenceKey {} 6 | 7 | extension AutomaticPreferenceKey { 8 | public static func reduce(value: inout Value, nextValue: () -> Value) { 9 | value = nextValue() 10 | } 11 | } 12 | 13 | // MARK: - NamespacedAutomaticPreferenceKey 14 | 15 | private struct NamespacedAutomaticPreferenceKey: PreferenceKey { 16 | static var defaultValue: [ID: WrappedValue.Value] { [:] } 17 | 18 | static func reduce(value existingDictionary: inout [ID: WrappedValue.Value], nextValue: () -> [ID: WrappedValue.Value]) { 19 | let newValue = nextValue() 20 | for (key, value) in newValue { 21 | if var existingValue = existingDictionary[key] { 22 | WrappedValue.reduce(value: &existingValue, nextValue: { value }) 23 | existingDictionary[key] = existingValue 24 | } else { 25 | existingDictionary[key] = value 26 | } 27 | } 28 | } 29 | } 30 | 31 | extension View { 32 | func preference(namespace: ID, key _: K.Type, value: K.Value) -> some View { 33 | preference(key: NamespacedAutomaticPreferenceKey.self, value: [namespace: value]) 34 | } 35 | 36 | func onPreferenceChange(namespace: ID, key _: K.Type, perform action: @escaping (K.Value) -> Void) -> some View where K.Value: Equatable { 37 | onPreferenceChange(NamespacedAutomaticPreferenceKey.self) { newValue in 38 | action(newValue[namespace, default: K.defaultValue]) 39 | } 40 | } 41 | } 42 | 43 | // MARK: - PageSheet 44 | 45 | /// Customizable sheet presentations in SwiftUI 46 | public enum PageSheet { 47 | /// An object that represents a height where a sheet naturally rests. 48 | public typealias Detent = UISheetPresentationController.Detent 49 | 50 | // MARK: - Configuration 51 | 52 | fileprivate struct Configuration: Equatable { 53 | var prefersGrabberVisible: Bool = false 54 | var detents: [Detent] = [.large()] 55 | var largestUndimmedDetentIdentifier: Detent.Identifier? = nil 56 | var selectedDetentIdentifier: Detent.Identifier? = nil 57 | var prefersEdgeAttachedInCompactHeight: Bool = false 58 | var widthFollowsPreferredContentSizeWhenEdgeAttached: Bool = false 59 | var prefersScrollingExpandsWhenScrolledToEdge: Bool = true 60 | var preferredCornerRadius: CGFloat? = nil 61 | 62 | static var `default`: Self { .init() } 63 | } 64 | 65 | // MARK: - ConfiguredHostingView 66 | 67 | internal struct ConfiguredHostingView: View { 68 | @State 69 | private var configuration: Configuration = .default 70 | 71 | @State 72 | private var selectedDetent: Detent.Identifier? 73 | 74 | @Namespace 75 | private var namespace 76 | 77 | let content: Content 78 | 79 | var body: some View { 80 | HostingView(configuration: $configuration, selectedDetent: $selectedDetent, content: content) 81 | .onChange(of: selectedDetent) { newValue in 82 | self.configuration.selectedDetentIdentifier = newValue 83 | } 84 | .onPreferenceChange(namespace: namespace, key: Preference.SelectedDetentIdentifier.self) { newValue in 85 | self.selectedDetent = newValue 86 | } 87 | .onPreferenceChange(namespace: namespace, key: Preference.GrabberVisible.self) { newValue in 88 | self.configuration.prefersGrabberVisible = newValue 89 | } 90 | .onPreferenceChange(namespace: namespace, key: Preference.Detents.self) { newValue in 91 | self.configuration.detents = newValue 92 | } 93 | .onPreferenceChange(namespace: namespace, key: Preference.LargestUndimmedDetentIdentifier.self) { newValue in 94 | self.configuration.largestUndimmedDetentIdentifier = newValue 95 | } 96 | .onPreferenceChange(namespace: namespace, key: Preference.EdgeAttachedInCompactHeight.self) { newValue in 97 | self.configuration.prefersEdgeAttachedInCompactHeight = newValue 98 | } 99 | .onPreferenceChange(namespace: namespace, key: Preference.WidthFollowsPreferredContentSizeWhenEdgeAttached.self) { 100 | newValue in 101 | self.configuration.widthFollowsPreferredContentSizeWhenEdgeAttached = newValue 102 | } 103 | .onPreferenceChange(namespace: namespace, key: Preference.ScrollingExpandsWhenScrolledToEdge.self) { newValue in 104 | self.configuration.prefersScrollingExpandsWhenScrolledToEdge = newValue 105 | } 106 | .onPreferenceChange(namespace: namespace, key: Preference.CornerRadius.self) { newValue in 107 | self.configuration.preferredCornerRadius = newValue 108 | } 109 | .environment(\._selectedDetentIdentifier, self.selectedDetent) 110 | .environment(\._preferenceNamespace, namespace) 111 | .ignoresSafeArea() 112 | } 113 | } 114 | 115 | // MARK: - HostingController 116 | 117 | fileprivate class HostingController: UIHostingController, 118 | UISheetPresentationControllerDelegate 119 | { 120 | var configuration: Configuration = .default { 121 | didSet { 122 | if let sheet = sheetPresentationController { 123 | if sheet.delegate == nil { 124 | sheet.delegate = self 125 | } 126 | 127 | let config = configuration 128 | sheet.animateChanges { 129 | sheet.prefersGrabberVisible = config.prefersGrabberVisible 130 | sheet.detents = config.detents 131 | sheet.largestUndimmedDetentIdentifier = config.largestUndimmedDetentIdentifier 132 | sheet.prefersEdgeAttachedInCompactHeight = config.prefersEdgeAttachedInCompactHeight 133 | sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = 134 | config.widthFollowsPreferredContentSizeWhenEdgeAttached 135 | sheet.prefersScrollingExpandsWhenScrolledToEdge = 136 | config.prefersScrollingExpandsWhenScrolledToEdge 137 | sheet.preferredCornerRadius = config.preferredCornerRadius 138 | sheet.selectedDetentIdentifier = config.selectedDetentIdentifier 139 | } 140 | } 141 | } 142 | } 143 | 144 | @Binding 145 | var selectedDetent: Detent.Identifier? 146 | 147 | init(rootView: Content, selectedDetent: Binding) { 148 | _selectedDetent = selectedDetent 149 | super.init(rootView: rootView) 150 | } 151 | 152 | @available(*, unavailable) 153 | @MainActor @objc dynamic required init?(coder _: NSCoder) { 154 | fatalError("init(coder:) has not been implemented") 155 | } 156 | 157 | override func viewWillDisappear(_ animated: Bool) { 158 | super.viewWillDisappear(animated) 159 | 160 | // NOTE: Fixes an issue with largestUndimmedDetentIdentifier perpetually dimming buttons. 161 | parent?.presentingViewController?.view.tintAdjustmentMode = .normal 162 | } 163 | 164 | // MARK: UISheetPresentationControllerDelegate 165 | 166 | func sheetPresentationControllerDidChangeSelectedDetentIdentifier( 167 | _ sheet: UISheetPresentationController 168 | ) { 169 | selectedDetent = sheet.selectedDetentIdentifier 170 | } 171 | } 172 | 173 | // MARK: - HostingView 174 | 175 | fileprivate struct HostingView: UIViewControllerRepresentable { 176 | @Binding 177 | var configuration: Configuration 178 | 179 | @Binding 180 | var selectedDetent: Detent.Identifier? 181 | 182 | @State 183 | private var selectedDetentIdentifier: Detent.Identifier? 184 | 185 | let content: Content 186 | 187 | func makeUIViewController(context _: Context) -> HostingController { 188 | HostingController( 189 | rootView: content, 190 | selectedDetent: $selectedDetent 191 | ) 192 | } 193 | 194 | func updateUIViewController(_ controller: HostingController, context _: Context) { 195 | if controller.configuration != configuration, configuration != .default { 196 | controller.configuration = configuration 197 | controller.rootView = content 198 | 199 | // NOTE: Fixes safe area flickering when we throw the view up and down. 200 | controller.view.invalidateIntrinsicContentSize() 201 | 202 | // NOTE: Fixes an issue with largestUndimmedDetentIdentifier perpetually dimming buttons. 203 | if configuration.largestUndimmedDetentIdentifier != nil { 204 | controller.parent?.presentingViewController?.view.tintAdjustmentMode = .normal 205 | } else { 206 | controller.parent?.presentingViewController?.view.tintAdjustmentMode = .automatic 207 | } 208 | } 209 | } 210 | } 211 | } 212 | 213 | // MARK: - Presentation View Modifiers 214 | 215 | extension PageSheet { 216 | enum Modifier { 217 | // MARK: Presentation 218 | 219 | struct BooleanPresentation: ViewModifier { 220 | @Binding 221 | var isPresented: Bool 222 | 223 | let onDismiss: (() -> Void)? 224 | let content: () -> SheetContent 225 | 226 | func body(content: Content) -> some View { 227 | content.sheet(isPresented: $isPresented, onDismiss: onDismiss) { 228 | ConfiguredHostingView( 229 | content: self.content() 230 | ) 231 | } 232 | } 233 | } 234 | 235 | // MARK: ItemPresentation 236 | 237 | struct ItemPresentation: ViewModifier { 238 | @Binding 239 | var item: Item? 240 | 241 | let onDismiss: (() -> Void)? 242 | let content: (Item) -> SheetContent 243 | 244 | func body(content: Content) -> some View { 245 | content.sheet(item: $item, onDismiss: onDismiss) { item in 246 | ConfiguredHostingView( 247 | content: self.content(item) 248 | ) 249 | } 250 | } 251 | } 252 | } 253 | } 254 | 255 | // MARK: Preference 256 | 257 | public extension PageSheet { 258 | /// Implementations of custom [`PreferenceKeys`](https://developer.apple.com/documentation/swiftui/preferencekey). 259 | enum Preference { 260 | public struct GrabberVisible: AutomaticPreferenceKey { 261 | public static var defaultValue: Bool = Configuration.default.prefersGrabberVisible 262 | } 263 | 264 | public struct Detents: AutomaticPreferenceKey { 265 | public static var defaultValue: [PageSheet.Detent] = Configuration.default.detents 266 | } 267 | 268 | public struct LargestUndimmedDetentIdentifier: AutomaticPreferenceKey { 269 | public static var defaultValue: Detent.Identifier? = Configuration.default 270 | .largestUndimmedDetentIdentifier 271 | } 272 | 273 | public struct SelectedDetentIdentifier: AutomaticPreferenceKey { 274 | public static var defaultValue: Detent.Identifier? = Configuration.default 275 | .selectedDetentIdentifier 276 | } 277 | 278 | public struct EdgeAttachedInCompactHeight: AutomaticPreferenceKey { 279 | public static var defaultValue: Bool = Configuration.default 280 | .prefersEdgeAttachedInCompactHeight 281 | } 282 | 283 | public struct WidthFollowsPreferredContentSizeWhenEdgeAttached: AutomaticPreferenceKey { 284 | public static var defaultValue: Bool = Configuration.default 285 | .widthFollowsPreferredContentSizeWhenEdgeAttached 286 | } 287 | 288 | public struct ScrollingExpandsWhenScrolledToEdge: AutomaticPreferenceKey { 289 | public static var defaultValue: Bool = Configuration.default 290 | .prefersScrollingExpandsWhenScrolledToEdge 291 | } 292 | 293 | public struct CornerRadius: AutomaticPreferenceKey { 294 | public static var defaultValue: CGFloat? = Configuration.default.preferredCornerRadius 295 | } 296 | } 297 | } 298 | 299 | // MARK: - PresentationPreference 300 | 301 | /// Set of preferences that can be applied to a sheet presentation. 302 | @frozen public enum SheetPreference: Hashable { 303 | /// Sets a Boolean value that determines whether the presenting sheet shows a grabber at the top. 304 | /// 305 | /// The default value is `false`, which means the sheet doesn't show a grabber. A *grabber* is a visual affordance that indicates that a sheet is resizable. 306 | /// Showing a grabber may be useful when it isn't apparent that a sheet can resize or when the sheet can't dismiss interactively. 307 | /// 308 | /// Set this value to `true` for the system to draw a grabber in the standard system-defined location. 309 | /// The system automatically hides the grabber at appropriate times, like when the sheet is full screen in a compact-height size class or when another sheet presents on top of it. 310 | /// 311 | case grabberVisible(Bool) 312 | 313 | /// Sets an array of heights where the presenting sheet can rest. 314 | /// 315 | /// The default value is an array that contains the value [`large()`](https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller/detent/3801916-large). 316 | /// The array must contain at least one element. When you set this value, specify detents in order from smallest to largest height. 317 | /// 318 | case detents([PageSheet.Detent]) 319 | 320 | /// Sets the largest detent that doesn’t dim the view underneath the presenting sheet. 321 | /// 322 | /// The default value is `nil`, which means the system adds a noninteractive dimming view underneath the sheet at all detents. 323 | /// Set this property to only add the dimming view at detents larger than the detent you specify. 324 | /// For example, set this property to [`medium`](https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller/detent/identifier/3801920-medium) to add the dimming view at the [`large`](https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller/detent/identifier/3801919-large) detent. 325 | /// 326 | /// Without a dimming view, the undimmed area around the sheet responds to user interaction, allowing for a nonmodal experience. 327 | /// You can use this behavior for sheets with interactive content underneath them. 328 | /// 329 | case largestUndimmedDetent(id: PageSheet.Detent.Identifier?) 330 | 331 | /// Sets the identifier of the most recently selected detent on the presenting sheet. 332 | /// 333 | /// This property represents the most recent detent that the user selects or that you set programmatically. 334 | /// The default value is `nil`, which means the sheet displays at the smallest detent you specify in [`detents`](https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller/3801903-detents). 335 | /// Detents can be modified using the ``PageSheetCore/SheetPreference/detents(_:)`` sheet preference. 336 | /// 337 | case selectedDetent(id: PageSheet.Detent.Identifier?) 338 | 339 | /// Sets a Boolean value that determines whether the presenting sheet attaches to the bottom edge of the screen in a compact-height size class. 340 | /// 341 | /// The default value is `false`, which means the sheet defaults to a full screen appearance at compact height. 342 | /// Set this value to `true` to use an alternate appearance in a compact-height size class, causing the sheet to only attach to the screen on its bottom edge. 343 | /// 344 | case edgeAttachedInCompactHeight(Bool) 345 | 346 | /// Sets a Boolean value that determines whether the presenting sheet's width matches its view's preferred content size. 347 | /// 348 | /// The default value is `false`, which means the sheet's width equals the width of its container's safe area. 349 | /// Set this value to `true` to use your view controller's [`preferredContentSize`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621476-preferredcontentsize) to determine the width of the sheet instead. 350 | /// 351 | /// This property doesn't have an effect when the sheet is in a compact-width and regular-height size class, or when [`prefersEdgeAttachedInCompactHeight`](https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller/3801907-prefersscrollingexpandswhenscrol) is `false`. 352 | /// The scroll expansion preference can be modified using the ``PageSheetCore/SheetPreference/scrollingExpandsWhenScrolledToEdge(_:)`` sheet preference. 353 | /// 354 | case widthFollowsPreferredContentSizeWhenEdgeAttached(Bool) 355 | 356 | /// Sets a Boolean value that determines whether scrolling expands the presenting sheet to a larger detent. 357 | /// 358 | /// The default value is `true`, which means if the sheet can expand to a larger detent than [`selectedDetentIdentifier`](https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller/3801908-selecteddetentidentifier), 359 | /// scrolling up in the sheet increases its detent instead of scrolling the sheet's content. After the sheet reaches its largest detent, scrolling begins. 360 | /// 361 | /// Set this value to `false` if you want to avoid letting a scroll gesture expand the sheet. 362 | /// For example, you can set this value on a nonmodal sheet to avoid obscuring the content underneath the sheet. 363 | /// 364 | case scrollingExpandsWhenScrolledToEdge(Bool) 365 | 366 | /// Sets the preferred corner radius on the presenting sheet. 367 | /// 368 | /// The default value is `nil`. This property only has an effect when the presenting sheet is at the front of its sheet stack. 369 | /// 370 | case cornerRadius(CGFloat?) 371 | } 372 | 373 | /// Applies a ``PageSheetCore/SheetPreference`` and returns a new view. 374 | /// 375 | /// You can use this modifier directly or preferably the convenience modifier `sheetPreference(_:)`: 376 | /// 377 | /// ```swift 378 | /// struct SheetContentView: View { 379 | /// var body: some View { 380 | /// VStack { 381 | /// Text("Hello World.") 382 | /// } 383 | /// .sheetPreference(.grabberVisible(true)) 384 | /// .modifier( 385 | /// SheetPreferenceViewModifier(.cornerRadius(10)) 386 | /// ) 387 | /// } 388 | /// } 389 | /// ``` 390 | /// 391 | public struct SheetPreferenceViewModifier: ViewModifier { 392 | /// Preference to be applied. 393 | public let preference: SheetPreference 394 | 395 | @Environment(\._preferenceNamespace) private var namespace 396 | 397 | /// Gets the current body of the caller. 398 | public func body(content: Content) -> some View { 399 | if let namespace { 400 | switch preference { 401 | case let .cornerRadius(value): 402 | content.preference(namespace: namespace, key: PageSheet.Preference.CornerRadius.self, value: value) 403 | case let .detents(value): 404 | content.preference(namespace: namespace, key: PageSheet.Preference.Detents.self, value: value) 405 | case let .largestUndimmedDetent(value): 406 | content.preference(namespace: namespace, 407 | key: PageSheet.Preference.LargestUndimmedDetentIdentifier.self, value: value) 408 | case let .selectedDetent(value): 409 | content.preference(namespace: namespace, key: PageSheet.Preference.SelectedDetentIdentifier.self, value: value) 410 | case let .edgeAttachedInCompactHeight(value): 411 | content.preference(namespace: namespace, key: PageSheet.Preference.EdgeAttachedInCompactHeight.self, value: value) 412 | case let .widthFollowsPreferredContentSizeWhenEdgeAttached(value): 413 | content.preference(namespace: namespace, 414 | key: PageSheet.Preference.WidthFollowsPreferredContentSizeWhenEdgeAttached.self, 415 | value: value) 416 | case let .grabberVisible(value): 417 | content.preference(namespace: namespace, key: PageSheet.Preference.GrabberVisible.self, value: value) 418 | case let .scrollingExpandsWhenScrolledToEdge(value): 419 | content.preference(namespace: namespace, 420 | key: PageSheet.Preference.ScrollingExpandsWhenScrolledToEdge.self, value: value) 421 | } 422 | } 423 | } 424 | 425 | /// A structure that defines the ``PageSheetCore/SheetPreference`` needed to produce a new view with that preference applied. 426 | @inlinable public init(_ preference: SheetPreference) { 427 | self.preference = preference 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /Sources/PageSheetCore/PageSheetView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// A SwiftUI wrapper view for presentation controllers that manages the appearance and behavior of a sheet. 4 | /// 5 | /// Sheet presentation controllers specify a sheet's size based on a *detent*, a height where a sheet naturally rests. 6 | /// Detents allow a sheet to resize from one edge of its fully expanded frame while the other three edges remain fixed. 7 | /// You specify the detents that a sheet supports using `detents`, and monitor its most recently selected detent using `selectedDetentIdentifier`. 8 | /// 9 | /// - Note: This view makes it easier to embed `PageSheetView` in custom navigation 10 | /// solutions such as `FlowStacks` and is meant to be presented using a `sheet` modifier. 11 | /// Other ways of presenting may not work, and are not officially supported. 12 | /// 13 | public struct PageSheetView: View where Content: View { 14 | private let content: () -> Content 15 | 16 | /// Initializes and returns a presentation controller wrapped SwiftUI view. 17 | /// 18 | /// - Parameters 19 | /// - content: A closure that returns the content of the sheet. 20 | /// 21 | public init(@ViewBuilder _ content: @escaping () -> Content) { 22 | self.content = content 23 | } 24 | 25 | public var body: some View { 26 | PageSheet.ConfiguredHostingView(content: content()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/PageSheetCore/View+PageSheet.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension View { 4 | 5 | // MARK: Presentation 6 | 7 | fileprivate typealias Modifier = PageSheet.Modifier 8 | 9 | /// Presents a page sheet when a binding to a Boolean value that you 10 | /// provide is true. 11 | /// 12 | /// Use this method when you want to present a sheet view to the 13 | /// user when a Boolean value you provide is true. The example 14 | /// below displays a sheet view of the mockup for a software license 15 | /// agreement when the user toggles the `isShowingSheet` variable by 16 | /// clicking or tapping on the "Show License Agreement" button: 17 | /// 18 | /// struct ShowLicenseAgreement: View { 19 | /// @State private var isShowingSheet = false 20 | /// var body: some View { 21 | /// Button(action: { 22 | /// isShowingSheet.toggle() 23 | /// }) { 24 | /// Text("Show License Agreement") 25 | /// } 26 | /// .pageSheet(isPresented: $isShowingSheet, 27 | /// onDismiss: didDismiss) { 28 | /// VStack { 29 | /// Text("License Agreement") 30 | /// .font(.title) 31 | /// .padding(50) 32 | /// Text(""" 33 | /// Terms and conditions go here. 34 | /// """) 35 | /// .padding(50) 36 | /// Button("Dismiss", 37 | /// action: { isShowingSheet.toggle() }) 38 | /// } 39 | /// .detents([.medium(), .large()]) 40 | /// } 41 | /// } 42 | /// 43 | /// func didDismiss() { 44 | /// // Handle the dismissing action. 45 | /// } 46 | /// } 47 | /// 48 | /// - Parameters: 49 | /// - isPresented: A binding to a Boolean value that determines whether 50 | /// to present the sheet that you create in the modifier's 51 | /// `content` closure. 52 | /// - onDismiss: The closure to execute when dismissing the sheet. 53 | /// - content: A closure that returns the content of the sheet. 54 | public func pageSheet( 55 | isPresented: Binding, onDismiss: (() -> Void)? = nil, 56 | @ViewBuilder content: @escaping () -> Content 57 | ) -> some View { 58 | self.modifier( 59 | Modifier.BooleanPresentation(isPresented: isPresented, onDismiss: onDismiss, content: content) 60 | ) 61 | } 62 | 63 | /// Presents a page sheet using the given item as a data source 64 | /// for the sheet's content. 65 | /// 66 | /// Use this method when you need to present a sheet view with content 67 | /// from a custom data source. The example below shows a custom data source 68 | /// `InventoryItem` that the `content` closure uses to populate the display 69 | /// the action sheet shows to the user: 70 | /// 71 | /// struct ShowPartDetail: View { 72 | /// @State var sheetDetail: InventoryItem? 73 | /// var body: some View { 74 | /// Button("Show Part Details") { 75 | /// sheetDetail = InventoryItem( 76 | /// id: "0123456789", 77 | /// partNumber: "Z-1234A", 78 | /// quantity: 100, 79 | /// name: "Widget") 80 | /// } 81 | /// .pageSheet(item: $sheetDetail, 82 | /// onDismiss: didDismiss) { detail in 83 | /// VStack(alignment: .leading, spacing: 20) { 84 | /// Text("Part Number: \(detail.partNumber)") 85 | /// Text("Name: \(detail.name)") 86 | /// Text("Quantity On-Hand: \(detail.quantity)") 87 | /// } 88 | /// .onTapGesture { 89 | /// sheetDetail = nil 90 | /// } 91 | /// .detents([.medium(), .large()]) 92 | /// } 93 | /// } 94 | /// 95 | /// func didDismiss() { 96 | /// // Handle the dismissing action. 97 | /// } 98 | /// } 99 | /// 100 | /// struct InventoryItem: Identifiable { 101 | /// var id: String 102 | /// let partNumber: String 103 | /// let quantity: Int 104 | /// let name: String 105 | /// } 106 | /// 107 | /// - Parameters: 108 | /// - item: A binding to an optional source of truth for the sheet. 109 | /// When `item` is non-`nil`, the system passes the item's content to 110 | /// the modifier's closure. You display this content in a sheet that you 111 | /// create that the system displays to the user. If `item` changes, 112 | /// the system dismisses the sheet and replaces it with a new one 113 | /// using the same process. 114 | /// - onDismiss: The closure to execute when dismissing the sheet. 115 | /// - content: A closure returning the content of the sheet. 116 | public func pageSheet( 117 | item: Binding, onDismiss: (() -> Void)? = nil, 118 | @ViewBuilder content: @escaping (Item) -> V 119 | ) -> some View { 120 | self.modifier(Modifier.ItemPresentation(item: item, onDismiss: onDismiss, content: content)) 121 | } 122 | 123 | // MARK: Preferences 124 | 125 | public typealias Preference = PageSheet.Preference 126 | 127 | /// Sets a Boolean value that determines whether the presenting sheet shows a grabber at the top. 128 | /// 129 | /// The default value is `false`, which means the sheet doesn't show a grabber. A *grabber* is a visual affordance that indicates that a sheet is resizable. 130 | /// Showing a grabber may be useful when it isn't apparent that a sheet can resize or when the sheet can't dismiss interactively. 131 | /// 132 | /// Set this value to `true` for the system to draw a grabber in the standard system-defined location. 133 | /// The system automatically hides the grabber at appropriate times, like when the sheet is full screen in a compact-height size class or when another sheet presents on top of it. 134 | /// 135 | /// - Note: This modifier only takes effect when this view is inside of and visible within a presented ``PageSheet``. You can apply the modifier to any view in the sheet’s view hierarchy. 136 | /// 137 | /// - Parameters: 138 | /// - isVisible: Default value is `false`, set to `true` to display grabber. 139 | /// - Returns: A view that wraps this view and sets the presenting sheet's grabber visiblity. 140 | @available(*, deprecated, message: "Use `sheetPreference(_:)` instead.") 141 | @inlinable public func preferGrabberVisible(_ isVisible: Bool) -> some View { 142 | self.sheetPreference(.grabberVisible(isVisible)) 143 | } 144 | 145 | /// Sets an array of heights where the presenting sheet can rest. 146 | /// 147 | /// The default value is an array that contains the value ``large()``. 148 | /// The array must contain at least one element. When you set this value, specify detents in order from smallest to largest height. 149 | /// 150 | /// - Note: This modifier only takes effect when this view is inside of and visible within a presented ``PageSheet``. You can apply the modifier to any view in the sheet’s view hierarchy. 151 | /// 152 | /// - Parameters: 153 | /// - detents: The default value is an array that contains the value ``large()``. 154 | /// - Returns: A view that wraps this view and sets the presenting sheet's ``UISheetPresentationController/detents``. 155 | @available(*, deprecated, message: "Use `sheetPreference(_:)` instead.") 156 | @inlinable public func detents(_ detents: [PageSheet.Detent]) -> some View { 157 | self.sheetPreference(.detents(detents)) 158 | } 159 | 160 | /// Sets the largest detent that doesn’t dim the view underneath the presenting sheet. 161 | /// 162 | /// The default value is `nil`, which means the system adds a noninteractive dimming view underneath the sheet at all detents. 163 | /// Set this property to only add the dimming view at detents larger than the detent you specify. 164 | /// For example, set this property to ``medium`` to add the dimming view at the ``large`` detent. 165 | /// 166 | /// Without a dimming view, the undimmed area around the sheet responds to user interaction, allowing for a nonmodal experience. 167 | /// You can use this behavior for sheets with interactive content underneath them. 168 | /// 169 | /// - Note: This modifier only takes effect when this view is inside of and visible within a presented ``PageSheet``. You can apply the modifier to any view in the sheet’s view hierarchy. 170 | /// 171 | /// - Parameters: 172 | /// - id: A ``PageSheetCore/PageSheet/Detent`` Identifier value, the default is `nil`. 173 | /// - Returns: A view that wraps this view and sets the presenting sheet's largest undimmed `Detent` identifier. 174 | @available(*, deprecated, message: "Use `sheetPreference(_:)` instead.") 175 | @inlinable public func largestUndimmedDetent(id identifier: PageSheet.Detent.Identifier?) 176 | -> some View 177 | { 178 | self.sheetPreference(.largestUndimmedDetent(id: identifier)) 179 | } 180 | 181 | /// Sets the identifier of the most recently selected detent on the presenting sheet. 182 | /// 183 | /// This property represents the most recent detent that the user selects or that you set programmatically. 184 | /// The default value is `nil`, which means the sheet displays at the smallest detent you specify in ``detents``. 185 | /// 186 | /// - Note: This modifier only takes effect when this view is inside of and visible within a presented ``PageSheet``. You can apply the modifier to any view in the sheet’s view hierarchy. 187 | /// 188 | /// - Parameters: 189 | /// - id: A ``PageSheet/Detent`` Identifier value, the default is `nil`. 190 | /// - Returns: A view that wraps this view and sets the presenting sheet's selected `Detent` identifier. 191 | @available(*, deprecated, message: "Use `sheetPreference(_:)` instead.") 192 | @inlinable public func selectedDetent(id identifier: PageSheet.Detent.Identifier?) -> some View { 193 | self.sheetPreference(.selectedDetent(id: identifier)) 194 | } 195 | 196 | /// Sets a Boolean value that determines whether the presenting sheet attaches to the bottom edge of the screen in a compact-height size class. 197 | /// 198 | /// The default value is `false`, which means the sheet defaults to a full screen appearance at compact height. 199 | /// Set this value to `true` to use an alternate appearance in a compact-height size class, causing the sheet to only attach to the screen on its bottom edge. 200 | /// 201 | /// - Note: This modifier only takes effect when this view is inside of and visible within a presented ``PageSheet``. You can apply the modifier to any view in the sheet’s view hierarchy. 202 | /// 203 | /// - Parameters: 204 | /// - preference: Default value is `false`. 205 | /// - Returns: A view that wraps this view and sets the presenting sheet's ``prefersEdgeAttachedInCompactHeight`` property. 206 | @available(*, deprecated, message: "Use `sheetPreference(_:)` instead.") 207 | @inlinable public func preferEdgeAttachedInCompactHeight(_ preference: Bool) -> some View { 208 | self.sheetPreference(.edgeAttachedInCompactHeight(preference)) 209 | } 210 | 211 | /// Sets a Boolean value that determines whether the presenting sheet's width matches its view's preferred content size. 212 | /// 213 | /// The default value is `false`, which means the sheet's width equals the width of its container's safe area. 214 | /// Set this value to `true` to use your view controller's ``preferredContentSize`` to determine the width of the sheet instead. 215 | /// 216 | /// This property doesn't have an effect when the sheet is in a compact-width and regular-height size class, or when ``prefersEdgeAttachedInCompactHeight`` is `false`. 217 | /// 218 | /// - Note: This modifier only takes effect when this view is inside of and visible within a presented ``PageSheet``. You can apply the modifier to any view in the sheet’s view hierarchy. 219 | /// 220 | /// - Parameters: 221 | /// - preference: Default value is `false`. 222 | /// - Returns: A view that wraps this view and sets the presenting sheet's ``prefersEdgeAttachedInCompactHeight`` property. 223 | @available(*, deprecated, message: "Use `sheetPreference(_:)` instead.") 224 | @inlinable public func widthFollowsPreferredContentSizeWhenEdgeAttached(_ preference: Bool) 225 | -> some View 226 | { 227 | self.sheetPreference(.widthFollowsPreferredContentSizeWhenEdgeAttached(preference)) 228 | } 229 | 230 | /// Sets a Boolean value that determines whether scrolling expands the presenting sheet to a larger detent. 231 | /// 232 | /// The default value is `true`, which means if the sheet can expand to a larger detent than ``selectedDetentIdentifier``, 233 | /// scrolling up in the sheet increases its detent instead of scrolling the sheet's content. After the sheet reaches its largest detent, scrolling begins. 234 | /// 235 | /// Set this value to `false` if you want to avoid letting a scroll gesture expand the sheet. 236 | /// For example, you can set this value on a nonmodal sheet to avoid obscuring the content underneath the sheet. 237 | /// 238 | /// - Note: This modifier only takes effect when this view is inside of and visible within a presented ``PageSheet``. You can apply the modifier to any view in the sheet’s view hierarchy. 239 | /// 240 | /// - Parameters: 241 | /// - preference: Default value is `true`. 242 | /// - Returns: A view that wraps this view and sets the presenting sheet's ``prefersScrollingExpandsWhenScrolledToEdge`` property. 243 | @available(*, deprecated, message: "Use `sheetPreference(_:)` instead.") 244 | @inlinable public func preferScrollingExpandsWhenScrolledToEdge(_ preference: Bool) -> some View { 245 | self.sheetPreference(.scrollingExpandsWhenScrolledToEdge(preference)) 246 | } 247 | 248 | /// Sets the preferred corner radius on the presenting sheet. 249 | /// 250 | /// The default value is `nil`. This property only has an effect when the presenting sheet is at the front of its sheet stack. 251 | /// 252 | /// - Note: This modifier only takes effect when this view is inside of and visible within a presented ``PageSheet``. You can apply the modifier to any view in the sheet’s view hierarchy. 253 | /// 254 | /// - Parameters: 255 | /// - preference: Default value is `nil`. 256 | /// - Returns: A view that wraps this view and sets the presenting sheet's ``cornerRadius``. 257 | @available(*, deprecated, message: "Use `sheetPreference(_:)` instead.") 258 | @inlinable public func preferredSheetCornerRadius(_ cornerRadius: CGFloat?) -> some View { 259 | self.sheetPreference(.cornerRadius(cornerRadius)) 260 | } 261 | 262 | /// Sets the presenting sheet's preferences using the provided preference. 263 | /// 264 | /// Applies a ``PageSheetCore/SheetPreference`` to the view. 265 | /// Use this modifier instead of the modifiers that apply directly to a view. This aids in creating consistency 266 | /// and discoverability when setting a sheet's presentation preferences. 267 | /// 268 | /// - Note: This modifier only takes effect when this view is inside of and visible within a presented ``PageSheet``. You can apply the modifier to any view in the sheet’s view hierarchy. 269 | /// 270 | /// - Parameters: 271 | /// - preference: A preference that can be applied to the current sheet presentation. 272 | /// 273 | /// - Returns: A view that has the given preference applied. 274 | /// 275 | @inlinable public func sheetPreference(_ preference: SheetPreference) 276 | -> ModifiedContent 277 | { 278 | self.modifier(SheetPreferenceViewModifier(preference)) 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /Sources/PageSheetPlus/Documentation.docc/Documentation.md: -------------------------------------------------------------------------------- 1 | # ``PageSheetPlus`` 2 | 3 | Contains the integration with ``ViewModifierBuilder`` to create the ``sheetPreferences(_:)`` modifier. 4 | 5 | ## Example 6 | 7 | ```swift 8 | import SwiftUI 9 | import PageSheet 10 | import PageSheetPlus 11 | 12 | struct ContentView: View { 13 | @State 14 | private var sheetPresented = false 15 | 16 | var body: some View { 17 | VStack { 18 | Button("Present Sheet") { 19 | sheetPresented = true 20 | } 21 | } 22 | .sheet(isPresented: $sheetPresented) { 23 | PageSheetView { 24 | VStack { 25 | Text("Hello, world!") 26 | } 27 | .sheetPreferences { 28 | .detents([.medium(), .large()]); 29 | .grabberVisible(true); 30 | } 31 | } 32 | } 33 | } 34 | } 35 | ``` 36 | 37 | ### Open Source 38 | Check out the [GitHub Repo](https://github.com/ericlewis/PageSheet) to see how everything works. 39 | -------------------------------------------------------------------------------- /Sources/PageSheetPlus/ViewModifierBuilder+PageSheet.swift: -------------------------------------------------------------------------------- 1 | #if canImport(ViewModifierBuilder) 2 | import PageSheetCore 3 | import SwiftUI 4 | import ViewModifierBuilder 5 | 6 | extension ViewModifierBuilder { 7 | public static func buildExpression(_ preference: SheetPreference) -> SheetPreferenceViewModifier 8 | { 9 | .init(preference) 10 | } 11 | } 12 | 13 | extension View { 14 | public func sheetPreferences( 15 | @ViewModifierBuilder _ preferences: @escaping () -> V 16 | ) -> some View { 17 | self.modifier(preferences()) 18 | } 19 | } 20 | #endif 21 | --------------------------------------------------------------------------------