├── .gitattributes
├── .gitignore
├── .spi.yml
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── LICENSE
├── Package.swift
├── README.md
├── Sources
├── .DS_Store
├── img
│ ├── wallet_01.png
│ └── wallet_02.gif
└── swiftui-bottom-sheet-drawer
│ ├── BottomSheet.swift
│ ├── enum
│ └── BottomSheetPosition.swift
│ ├── ext
│ ├── Comparable+clamped.swift
│ └── View+MessureSize.swift
│ ├── key
│ ├── BottomSheetPositionKey.swift
│ └── SizePreferenceKey.swift
│ ├── protocol
│ └── IBottomSheetView.swift
│ └── shape
│ └── RoundedCornersShape.swift
└── Tests
└── swiftui-bottom-sheet-drawerTests
└── swiftui_bottom_sheet_drawerTests.swift
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | builder:
3 | configs:
4 | - documentation_targets: [swiftui-bottom-sheet-drawer]
5 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Igor Shelopaev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.6
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "swiftui-bottom-sheet-drawer",
8 | platforms: [
9 | .macOS("12"), .iOS("15"), .watchOS("10"),
10 | ],
11 | products: [
12 | // Products define the executables and libraries a package produces, and make them visible to other packages.
13 | .library(
14 | name: "swiftui-bottom-sheet-drawer",
15 | targets: ["swiftui-bottom-sheet-drawer"]),
16 | ],
17 | dependencies: [
18 | // Dependencies declare other packages that this package depends on.
19 | // .package(url: /* package url */, from: "1.0.0"),
20 | ],
21 | targets: [
22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
23 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
24 | .target(
25 | name: "swiftui-bottom-sheet-drawer",
26 | dependencies: []),
27 | .testTarget(
28 | name: "swiftui-bottom-sheet-drawerTests",
29 | dependencies: ["swiftui-bottom-sheet-drawer"]),
30 | ]
31 | )
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Multiplatform (iOS, macOS) SwiftUI bottom sheet drawer
2 |
3 | [](https://swiftpackageindex.com/swiftuiux/swiftui-bottomsheet-drawer)
4 |
5 | ## Features
6 | - [x] It does not re-render the background content while manipulating with the sheet
7 | - [x] iOS and macOS support
8 | - [x] **dark** and **light** scheme support
9 | - [x] Observing sheet positions on change if you need to react on it
10 | - [x] Responsive for any size change It's important for macOS while window size changing
11 | - [x] Customize component with your own specs
12 |
13 | ## Creation
14 |
15 | Put the component into an absolute coordinate space like *ZStack* or *GeometryReader* and just pass a content that's it to start with sheet drawer.
16 |
17 | ```swift
18 | ZStack {
19 | BottomSheet(content: Color.clear.background(.thinMaterial))
20 | }
21 | ```
22 |
23 | ### Builder
24 | You can use builder methods to change some specs
25 |
26 | * `hideDragButton` - Hide drag button
27 | * `withoutAnimation` - Turn off animation
28 |
29 | ```swift
30 | ZStack {
31 | BottomSheet(content: Color.clear.background(.thinMaterial))
32 | .hideDragButton()
33 | .withoutAnimation()
34 | }
35 | ```
36 |
37 |
38 | ### Optional
39 |
40 | * `shift` - Visible height of the sheet drawer
41 | * `topIndentation` - Space from the very top to the max height drawer can reach
42 |
43 | * `draggerHeight` - Space sensitive for dragging
44 | * `dragThresholdToAct` - Dragging length after which trigger move to the next level depending on the direction of moving
45 |
46 |
47 |
48 | ### Observing sheet positions
49 | Observe sheet positions on change if you need to react on it in the external context of the component. For example to update layout of the drawer content according a new size of the height.
50 | Position **BottomSheetPosition** is passed with **height** of the sheet.
51 | **height** - is enum associated with value of type *CGFloat*
52 |
53 | ```swift
54 |
55 | @State private var position: BottomSheetPosition
56 |
57 | BottomSheet(
58 | content: SheetContentView(position: $position)
59 | )
60 | .onPositionChanged{
61 | position = $0
62 | }
63 | ```
64 |
65 | | Position | Description |
66 | | --- | --- |
67 | |**up(CGFloat)**| At the top |
68 | |**middle(CGFloat)**| At the middle |
69 | |**down(CGFloat)**| At the bottom |
70 |
71 | ## SwiftUI example of using package
72 |
73 | [](https://youtu.be/jLu7gbzGXTo)
74 |
75 | [](https://youtu.be/jLu7gbzGXTo)
76 |
77 |
78 |
79 | ## Documentation(API)
80 | - You need to have Xcode 13 installed in order to have access to Documentation Compiler (DocC)
81 | - Go to Product > Build Documentation or **⌃⇧⌘ D**
82 |
83 |
--------------------------------------------------------------------------------
/Sources/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftuiux/swiftui-bottomsheet-drawer/b9bac4bb2e6bb6ebea9c9c94a2184be98f6c71a6/Sources/.DS_Store
--------------------------------------------------------------------------------
/Sources/img/wallet_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftuiux/swiftui-bottomsheet-drawer/b9bac4bb2e6bb6ebea9c9c94a2184be98f6c71a6/Sources/img/wallet_01.png
--------------------------------------------------------------------------------
/Sources/img/wallet_02.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/swiftuiux/swiftui-bottomsheet-drawer/b9bac4bb2e6bb6ebea9c9c94a2184be98f6c71a6/Sources/img/wallet_02.gif
--------------------------------------------------------------------------------
/Sources/swiftui-bottom-sheet-drawer/BottomSheet.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BottomSheet.swift
3 | //
4 | //
5 | // Created by Igor Shelopaev on 20.07.2022.
6 | //
7 |
8 | import SwiftUI
9 |
10 |
11 | /// Bottom sheet drawer widget
12 | @available(iOS 15.0, macOS 12.0, watchOS 10.0, *)
13 | public struct BottomSheet: IBottomSheetView {
14 |
15 | private typealias Position = BottomSheetPosition
16 |
17 | /// Offset relatively the base
18 | @State private var offset: CGFloat = 0
19 |
20 | /// Current position
21 | @State private var position: BottomSheetPosition
22 |
23 | /// Animate move
24 | private var doAnimation: Bool = true
25 |
26 | /// Hide or show drag button
27 | private var showDragButton: Bool = true
28 |
29 | // MARK: - Config
30 |
31 | /// View
32 | let content: Content
33 |
34 | /// Visible height of the sheet drawer
35 | let shift: CGFloat
36 |
37 | /// Space from the very top to the max height drawer can reach
38 | let topIndentation: CGFloat
39 |
40 | ///Available space to do dragging
41 | let draggerHeight: CGFloat
42 |
43 | /// Dragging length after which trigger move to the next level depends on the direction of moving
44 | let dragThresholdToAct: CGFloat
45 |
46 | // MARK: - Lifecircle
47 |
48 | /// Init component
49 | /// - Parameters:
50 | /// - content: View content
51 | /// - shift: Visible height of the sheet drawer
52 | /// - topIndentation: Space from the very top to the max height drawer can reach
53 | /// - showDragButton: Hide or show drag button
54 | /// - draggerHeight: Space sensitive for dragging
55 | /// - dragThresholdToAct: Dragging length after which trigger move to the next level depends on the direction of moving
56 | /// - doAnimation: Animate move
57 | public init(
58 | content: Content,
59 | shift: CGFloat = 88,
60 | topIndentation: CGFloat = 50,
61 | draggerHeight: CGFloat = 50,
62 | dragThresholdToAct: CGFloat = 25
63 | ) {
64 | self.content = content
65 | self.topIndentation = max(topIndentation, 0)
66 | self.shift = max(shift, 25)
67 | self.draggerHeight = min(draggerHeight, 25)
68 | self.dragThresholdToAct = max(dragThresholdToAct, 0)
69 |
70 | self._position = State(initialValue: .down(max(shift, 25)))
71 | }
72 |
73 | /// The content and behavior of the view
74 | public var body: some View {
75 | GeometryReader { proxy in
76 | let h = proxy.size.height
77 |
78 | ZStack(alignment: .top) {
79 | backgroundTpl(proxy.size)
80 | content
81 | }
82 | .offset(y: h - shift)
83 | .offset(y: -offset)
84 | .overlay(dragger(h), alignment: .top)
85 | }.ignoresSafeArea(.all, edges: .bottom)
86 | .preference(key: BottomSheetPositionKey.self, value: position)
87 | .frame(minHeight: shift)
88 | .messureSize(updateSheetSize)
89 | }
90 |
91 | // MARK: - Private
92 |
93 | /// Background Tpl
94 | /// - Parameter size: Available size
95 | @ViewBuilder
96 | func backgroundTpl(_ size : CGSize) -> some View{
97 | let h = size.height
98 | let w = size.width
99 | Color.clear.background(.thinMaterial)
100 | .clipShape(RoundedCornersShape(tl: 30, tt: 30, width: w, height: h))
101 | .shadow(color: .primary.opacity(0.05), radius: 2, x: 1, y: -2)
102 | }
103 |
104 | /// Define gesture operation processing
105 | /// - Parameter height: Available height
106 | /// - Returns: Gesture
107 | private func gesture(_ height: CGFloat) -> some Gesture {
108 | DragGesture(minimumDistance: 0)
109 | .onChanged {
110 | let maxH = height - shift
111 | offset = maxH - $0.location.y.clamped(topIndentation, height - shift)
112 | }
113 | .onEnded { value in
114 | onEndDrag(value, height)
115 | }
116 | }
117 |
118 | /// Update sheet size in case of macOS window size change
119 | /// - Parameter size: Current size
120 | private func updateSheetSize(size: CGSize) {
121 | #if os(macOS)
122 | let h = size.height
123 | let limit = calculateLimit(h)
124 | let position = position.update(height: limit + shift)
125 | if limit != offset {
126 | updateOffset(width: limit)
127 | self.position = position
128 | }
129 | #endif
130 | }
131 |
132 | /// Get limit by new height
133 | /// - Window size is changed then need to recalculate limit
134 | /// - Reset drawer position if drag length is not enough to change the drawer position
135 | /// - Parameter maxH: Available height
136 | /// - Returns: Reverted offset value
137 | private func calculateLimit(_ height: CGFloat) -> CGFloat {
138 | var limit: CGFloat = 0
139 | let maxH = height - shift
140 | let half = maxH / 2
141 |
142 | if position.isUp {
143 | limit = maxH - topIndentation
144 | } else if position.isMiddle {
145 | limit = half
146 | }
147 |
148 | return limit
149 | }
150 |
151 | /// Get spect for the next position
152 | /// - Parameters:
153 | /// - maxH: Available height
154 | /// - up: Direction
155 | /// - Returns: Bunch of specs for the next position
156 | private func moveNext(_ height: CGFloat, up: Bool) -> (limit: CGFloat, position: Position) {
157 | var limit: CGFloat = 0
158 | var position: Position = .down(shift)
159 | let maxH = height - shift
160 | let half = maxH / 2
161 |
162 | if up {
163 | if offset > half {
164 | limit = maxH - topIndentation
165 | position = .up(limit + shift)
166 | } else {
167 | limit = half
168 | position = .middle(half + shift)
169 | }
170 | } else {
171 | if offset > half {
172 | limit = half
173 | position = .middle(half + shift)
174 | }
175 | }
176 |
177 | return (limit: limit, position: position)
178 | }
179 |
180 | /// Process end drag
181 | /// - Parameters:
182 | /// - value: Bunch of drag values on end drag
183 | /// - height: Available height
184 | private func onEndDrag(_ value: DragGesture.Value, _ height: CGFloat) {
185 | let start = value.startLocation.y
186 | let end = value.location.y
187 | let up = start - end > 0
188 | let delta = abs(start - end)
189 | var limit: CGFloat = 0
190 |
191 | if delta < dragThresholdToAct {
192 | limit = calculateLimit(height)
193 | } else {
194 | let next = moveNext(height, up: up)
195 | limit = next.limit
196 | position = next.position
197 | }
198 |
199 | updateOffset(width: limit)
200 | }
201 |
202 | /// Update offset with new limit
203 | /// - Parameter limit: New limit
204 | private func updateOffset(width limit: CGFloat) {
205 | if doAnimation {
206 | withAnimation(.easeInOut(duration: 0.25)) {
207 | offset = limit
208 | }
209 | } else {
210 | offset = limit
211 | }
212 | }
213 |
214 | /// Dragger button tpl
215 | @ViewBuilder
216 | private var draggerButton: some View {
217 | if showDragButton {
218 | Capsule()
219 | .fill(Color.primary)
220 | .frame(width: 80, height: 5)
221 | } else {
222 | EmptyView()
223 | }
224 | }
225 |
226 | /// Create draggable surface
227 | /// - Parameter h: The height of the draggable surface
228 | /// - Returns: Draggable surface
229 | private func dragger(_ height: CGFloat) -> some View {
230 | Color.white
231 | .opacity(0.001)
232 | .frame(height: 50)
233 | .overlay(draggerButton, alignment: .top)
234 | .offset(y: height - shift)
235 | .offset(y: -offset)
236 | .gesture(gesture(height))
237 | }
238 |
239 | }
240 |
241 | public extension BottomSheet {
242 |
243 | // MARK: - Builder methods
244 |
245 | /// Hide dragging button
246 | /// - Returns: Sheet with hidden button
247 | func hideDragButton() -> Self{
248 | var view = self
249 | view.showDragButton = false
250 | return view
251 | }
252 |
253 |
254 | /// Turn off animation
255 | /// - Returns: Sheet without animation
256 | func withoutAnimation() -> Self{
257 | var view = self
258 | view.doAnimation = false
259 | return view
260 | }
261 |
262 | // MARK: - Event methods
263 |
264 | /// Handler for changing the sheet position
265 | /// - Parameter fn: callback to react
266 | /// - Returns: View
267 | func onPositionChanged(_ fn: @escaping (BottomSheetPosition) -> ()) -> some View {
268 | self.onPreferenceChange(BottomSheetPositionKey.self) {
269 | fn($0)
270 | }
271 | }
272 | }
273 |
274 | #if DEBUG
275 | struct BottomSheet_Previews: PreviewProvider {
276 | static var previews: some View {
277 | ZStack {
278 | BottomSheet(content: Color.clear.background(.thinMaterial))
279 | }
280 | }
281 | }
282 | #endif
283 |
284 |
--------------------------------------------------------------------------------
/Sources/swiftui-bottom-sheet-drawer/enum/BottomSheetPosition.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BottomSheetPosition.swift
3 | //
4 | //
5 | // Created by Igor Shelopaev on 22.07.2022.
6 | //
7 |
8 | import SwiftUI
9 |
10 |
11 | /// Three level positions for sheet drawer
12 | @available(iOS 15.0, macOS 12.0, watchOS 10.0, *)
13 | public enum BottomSheetPosition: Comparable{
14 |
15 | case up(CGFloat)
16 |
17 | case middle(CGFloat)
18 |
19 | case down(CGFloat)
20 |
21 |
22 | /// Clone current value and return it with updated height
23 | /// - Parameter height: Current height of the sheet drawer
24 | /// - Returns: Updated value
25 | public func update(height : CGFloat) -> Self{
26 | switch self{
27 | case .up(_): return .up(height)
28 | case .middle(_): return .middle(height)
29 | case .down(_): return .down(height)
30 | }
31 | }
32 |
33 | /// Check is up position
34 | public var isUp: Bool{
35 | if case .up(_) = self{
36 | return true
37 | }
38 |
39 | return false
40 | }
41 |
42 | /// Check is middle position
43 | public var isMiddle: Bool{
44 | if case .middle(_) = self{
45 | return true
46 | }
47 |
48 | return false
49 | }
50 |
51 | /// Check is down position
52 | public var isDown: Bool{
53 | if case .down(_) = self{
54 | return true
55 | }
56 |
57 | return false
58 | }
59 |
60 | /// Get currently available height
61 | public var getHeight : CGFloat{
62 | switch self{
63 | case .up(let height): return height
64 | case .middle(let height): return height
65 | case .down(let height): return height
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Sources/swiftui-bottom-sheet-drawer/ext/Comparable+clamped.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Comparable+clamped.swift
3 | //
4 | //
5 | // Created by Igor Shelopaev on 21.07.2022.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Comparable {
11 |
12 | /// Restrain value to range limits
13 | /// - Parameters:
14 | /// - lower: lower limit
15 | /// - upper: upper limit
16 | /// - Returns: Clammed value
17 | func clamped(_ lower: Self, _ upper: Self) -> Self {
18 | return min(max(self, lower), upper)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/swiftui-bottom-sheet-drawer/ext/View+MessureSize.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View+MessureSize.swift
3 | //
4 | //
5 | // Created by Igor Shelopaev on 22.07.2022.
6 | //
7 |
8 | import SwiftUI
9 |
10 |
11 |
12 | public extension View {
13 |
14 | /// Messure a container size
15 | /// - Parameter fn: callback
16 | /// - Returns: View
17 | @available(iOS 15.0, macOS 12.0, watchOS 10.0, *)
18 | func messureSize(_ fn: @escaping (CGSize) -> Void) -> some View {
19 | overlay(
20 | GeometryReader { proxy in
21 | Color.clear.preference(key: SizePreferenceKey.self, value: proxy.size)
22 | .onPreferenceChange(SizePreferenceKey.self, perform: {
23 | fn($0)
24 | })
25 | }, alignment: .topLeading
26 | )
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/swiftui-bottom-sheet-drawer/key/BottomSheetPositionKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BottomSheetPositionKey.swift
3 | //
4 | //
5 | // Created by Igor Shelopaev on 22.07.2022.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// Key for emerging drawer sheet position
11 | @available(iOS 15.0, macOS 12.0, watchOS 10.0, *)
12 | public struct BottomSheetPositionKey: PreferenceKey {
13 |
14 | static public var defaultValue: BottomSheetPosition = .down(0)
15 |
16 | static public func reduce(value: inout BottomSheetPosition, nextValue: () -> BottomSheetPosition) {
17 | value = nextValue()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/swiftui-bottom-sheet-drawer/key/SizePreferenceKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SizePreferenceKey.swift
3 | //
4 | //
5 | // Created by Igor Shelopaev on 22.07.2022.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// Key for emerging size
11 | @available(iOS 15.0, macOS 12.0, watchOS 10.0, *)
12 | public struct SizePreferenceKey: PreferenceKey {
13 |
14 | public static var defaultValue = CGSize(width: 0, height: 0)
15 |
16 | public static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
17 | value = nextValue()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/swiftui-bottom-sheet-drawer/protocol/IBottomSheetView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IBottomSheetView.swift
3 | //
4 | //
5 | // Created by Igor Shelopaev on 26.07.2022.
6 | //
7 |
8 | import SwiftUI
9 |
10 | public protocol IBottomSheetView : View{
11 |
12 | associatedtype V : View
13 |
14 | // MARK: - Builder methods
15 |
16 | /// Hide dragging button
17 | func hideDragButton() -> Self
18 |
19 | /// Trun off animation
20 | func withoutAnimation() -> Self
21 |
22 | // MARK: - Event methods
23 |
24 | /// Handler for changing the sheet position
25 | func onPositionChanged(_ fn: @escaping (BottomSheetPosition) -> ()) -> V
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/swiftui-bottom-sheet-drawer/shape/RoundedCornersShape.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RoundedCornersShape.swift
3 | //
4 | //
5 | // Created by Igor Shelopaev on 21.07.2022.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// Create rounded conner shape with custom corner radius for every conner
11 | struct RoundedCornersShape: Shape {
12 |
13 | // MARK: - Config
14 |
15 | let tl: CGFloat //topLeading
16 |
17 | let tt: CGFloat //topTrailing
18 |
19 | let bl: CGFloat //bottomLeading
20 |
21 | let bt: CGFloat //bottomTrailing
22 |
23 | let width: CGFloat //container width
24 |
25 | let height: CGFloat //container height
26 |
27 | // MARK: - Life circle
28 |
29 | init(
30 | tl: CGFloat = 0,
31 | tt: CGFloat = 0,
32 | bl: CGFloat = 0,
33 | br: CGFloat = 0,
34 | width: CGFloat,
35 | height: CGFloat) {
36 | self.tl = tl
37 | self.tt = tt
38 | self.bl = bl
39 | self.bt = br
40 | self.width = width
41 | self.height = height
42 | }
43 |
44 |
45 | /// Create curcve with defined corner radius custom for every corner
46 | /// - Parameter rect: outer space
47 | /// - Returns: Curve path
48 | func path(in rect: CGRect) -> Path {
49 | Path { path in
50 |
51 | let w = width
52 | let h = height
53 |
54 | let tl = min(min(self.tl, h / 2), w / 2)
55 | let tt = min(min(self.tt, h / 2), w / 2)
56 | let bl = min(min(self.bl, h / 2), w / 2)
57 | let bt = min(min(self.bt, h / 2), w / 2)
58 |
59 | path.move(to: CGPoint(x: w / 2.0, y: 0))
60 | path.addLine(to: CGPoint(x: w - tt, y: 0))
61 | path.addArc(center: CGPoint(x: w - tt, y: tt), radius: tt,
62 | startAngle: Angle(degrees: -90), endAngle: Angle(degrees: 0), clockwise: false)
63 |
64 | path.addLine(to: CGPoint(x: w, y: h - bt))
65 | path.addArc(center: CGPoint(x: w - bt, y: h - bt), radius: bt,
66 | startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 90), clockwise: false)
67 |
68 | path.addLine(to: CGPoint(x: bl, y: h))
69 | path.addArc(center: CGPoint(x: bl, y: h - bl), radius: bl,
70 | startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 180), clockwise: false)
71 |
72 | path.addLine(to: CGPoint(x: 0, y: tl))
73 | path.addArc(center: CGPoint(x: tl, y: tl), radius: tl,
74 | startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 270), clockwise: false)
75 | path.closeSubpath()
76 |
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Tests/swiftui-bottom-sheet-drawerTests/swiftui_bottom_sheet_drawerTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import swiftui_bottom_sheet_drawer
3 |
4 | final class swiftui_bottom_sheet_drawerTests: XCTestCase {
5 | func testExample() throws {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 | // XCTAssertEqual(swiftui_bottom_sheet_drawer().text, "Hello, World!")
10 | }
11 | }
12 |
--------------------------------------------------------------------------------