├── .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://github.com/apple/swift-package-manager)
6 | [](https://swiftpackageindex.com/ericlewis/PageSheet)
7 | [](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 |
--------------------------------------------------------------------------------