├── .github
└── FUNDING.yml
├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── SheetKit
│ ├── BottomSheetViewController.swift
│ ├── ClearBackgournd.swift
│ ├── InteractiveDismissDisabled.swift
│ ├── SheetKit.swift
│ └── UIKit++.swift
└── Tests
└── SheetKitTests
└── SheetKitTests.swift
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | custom: [https://www.buymeacoffee.com/fatbobman, https://www.fatbobman.com/support/]
4 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 东坡肘子
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.5
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: "SheetKit",
8 | platforms: [.iOS(.v15)],
9 | products: [
10 | // Products define the executables and libraries a package produces, and make them visible to other packages.
11 | .library(
12 | name: "SheetKit",
13 | targets: ["SheetKit"]),
14 | ],
15 | dependencies: [
16 | // Dependencies declare other packages that this package depends on.
17 | // .package(url: /* package url */, from: "1.0.0"),
18 | ],
19 | targets: [
20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
21 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
22 | .target(
23 | name: "SheetKit",
24 | dependencies: []),
25 | .testTarget(
26 | name: "SheetKitTests",
27 | dependencies: ["SheetKit"]),
28 | ]
29 | )
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SheetKit
2 |
3 | SheetKit is an extension library for SwiftUI sheets.
4 |
5 | [中文版说明 with Picture](https://www.fatbobman.com/posts/sheetKit/)
6 |
7 | ## What is SheetKit ##
8 |
9 | SheetKit is a library of extensions for SwiftUI modal views. It provides several convenient display and cancel methods for modal views, as well as several View Extensions for modal views.
10 |
11 | The main reasons for developing SheetKit.
12 |
13 | * Convenient Deep link calls
14 | SwiftUI provides the onOpenURL method to make it very easy for applications to respond to Deep Link, but in practice, this is not as easy as expected. The main reason for this is that the important view presentation modes in SwiftUI: NavigationView, Sheet, etc. do not have the ability to be reset quickly and easily. It is difficult to instantly set the application to the view state we want with a couple of lines of code.
15 |
16 | * Centralised management of modal views
17 | SwiftUI usually uses .sheets to create modal views, which is very intuitive for simple applications, but if the application logic is complex and requires many modal views, this can make the code very messy and difficult to organize. In this case, we usually manage all the modal views centrally and call them all together. See my previous article - Popping up different Sheets on demand in SwiftUI.
18 |
19 | * The new UISheetPresentationController
20 | In WWDC 2021, Apple brought the long-awaited half-height modal view to everyone. The SheetKit makes up for it for now, but perhaps in a bit of a hurry, as there is no SwiftUI version of this popular interaction, only UIKit support. Both sheets, fullScreenCover and bottomSheet (half-height modal view) are fully supported and managed in one place.
21 |
22 | ## System requirements##
23 |
24 | iOS 15
25 |
26 | Swift 5.5
27 |
28 | XCode 13.0 +
29 |
30 |
31 | ## How to use ##
32 |
33 | ### present ###
34 |
35 | ```swift
36 | Button("show sheet"){
37 | SheetKit().present{
38 | Text("Hello world")
39 | }
40 | }
41 | ```
42 |
43 | or
44 |
45 | ```swift
46 | @Environment(\.sheetKit) var sheetKit
47 |
48 | Button("show sheet"){
49 | sheetKit.present{
50 | Text("Hello world")
51 | }
52 | }
53 | ```
54 |
55 | support multiSheet
56 |
57 | ```swift
58 | @Environment(\.sheetKit) var sheetKit
59 |
60 | Button("show sheet"){
61 | sheetKit.present{
62 | Button("show full sheet"){
63 | sheetKit.present(with:.fullScreenCover){
64 | Text("Hello world")
65 | }
66 | }
67 | }
68 | }
69 | ```
70 |
71 | ### sheet style ###
72 |
73 | three types sytle:
74 | * sheet
75 | * fullScreenCover
76 | * bottomSheet
77 |
78 | ```swift
79 | sheetKit.present(with: .bottomSheet){
80 | Text("Hello world")
81 | }
82 | ```
83 |
84 | custom bottomSheet
85 |
86 | ```swift
87 | let configuration = SheetKit.BottomSheetConfiguration( detents: [.medium(),.large()],
88 | largestUndimmedDetentIdentifier: .medium,
89 | prefersGrabberVisible: true,
90 | prefersScrollingExpandsWhenScrolledToEdge: false,
91 | prefersEdgeAttachedInCompactHeight: false,
92 | widthFollowsPreferredContentSizeWhenEdgeAttached: true,
93 | preferredCornerRadius: 100)
94 |
95 | sheetKit.present(with: .customBottomSheet,configuration: configuration) {
96 | Text("Hello world")
97 | }
98 | ```
99 |
100 | get notice when bottomSheet modal changed
101 |
102 | ```swift
103 | @State var detent:UISheetPresentationController.Detent.Identifier = .medium
104 |
105 | Button("Show"){
106 | sheetKit.present(with: .bottomSheet,detentIdentifier: $detent){
107 | Text("Hello worl")
108 | }
109 | }
110 | .onChange(of: detent){ value in
111 | print(value)
112 | }
113 | ```
114 |
115 | or
116 |
117 | ```swift
118 | @State var publisher = NotificationCenter.default.publisher(for: .bottomSheetDetentIdentifierDidChanged, object: nil)
119 |
120 | .onReceive(publisher){ notification in
121 | guard let obj = notification.object else {return}
122 | print(obj)
123 | }
124 | ```
125 |
126 | ### dismissAllSheets ###
127 |
128 | ```swift
129 | SheetKit().dismissAllSheets(animated: false, completion: {
130 | print("sheet has dismiss")
131 | })
132 | ```
133 |
134 | ### dismiss ###
135 |
136 | ```swift
137 | SheetKit().dismiss()
138 | ```
139 |
140 | ### interactiveDismissDisabled ###
141 |
142 | SwiftUI 3.0's interactiveDismissDisabled enhancement adds the ability to be notified when a user uses a gesture to cancel, on top of the ability to control whether gesture cancellation is allowed via code.
143 |
144 | ```swift
145 | struct ContentView: View {
146 | @State var sheet = false
147 | var body: some View {
148 | VStack {
149 | Button("show sheet") {
150 | sheet.toggle()
151 | }
152 | }
153 | .sheet(isPresented: $sheet) {
154 | SheetView()
155 | }
156 | }
157 | }
158 |
159 | struct SheetView: View {
160 | @State var disable = false
161 | @State var attempToDismiss = UUID()
162 | var body: some View {
163 | VStack {
164 | Button("disable: \(disable ? "true" : "false")") {
165 | disable.toggle()
166 | }
167 | .interactiveDismissDisabled(disable, attempToDismiss: $attempToDismiss)
168 | }
169 | .onChange(of: attempToDismiss) { _ in
170 | print("try to dismiss sheet")
171 | }
172 | }
173 | }
174 | ```
175 |
176 | ### clearBackground ###
177 |
178 | Set the background of the modal view to transparent. In SwiftUI 3.0, it is already possible to generate various hair-glass effects using the native API. However, the hair glass effect is only visible if the background of the modal view is set to transparent.
179 |
180 | ```swift
181 | ZStack {
182 | Rectangle().fill(LinearGradient(colors: [.red, .green, .pink, .blue, .yellow, .cyan, .gray], startPoint: .topLeading, endPoint: .bottomTrailing))
183 | Button("Show bottomSheet") {
184 | sheetKit.present(with: .bottomSheet, afterPresent: { print("presented") }, onDisappear: { print("disappear") }, detentIdentifier: $detent) {
185 | ZStack {
186 | Rectangle()
187 | .fill(.ultraThinMaterial)
188 | VStack {
189 | Text("Hello world")
190 | Button("dismiss all") {
191 | SheetKit().dismissAllSheets(animated: true, completion: {
192 | print("sheet has dismiss")
193 | })
194 | }
195 | }
196 | }
197 | .clearBackground()
198 | .ignoresSafeArea()
199 | }
200 | }
201 | .foregroundColor(.white)
202 | .buttonStyle(.bordered)
203 | .controlSize(.large)
204 | .tint(.green)
205 | }
206 | .ignoresSafeArea()
207 | ```
208 |
--------------------------------------------------------------------------------
/Sources/SheetKit/BottomSheetViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BottomSheetViewController.swift
3 | // SheetManager
4 | //
5 | // Created by Yang Xu on 2021/9/15.
6 | //
7 | // Code from https://github.com/adamfootdev/BottomSheet
8 |
9 | import SwiftUI
10 | import UIKit
11 |
12 | final class BottomSheetViewController: UIViewController, UISheetPresentationControllerDelegate {
13 | private let detents: [UISheetPresentationController.Detent]
14 | private let largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?
15 | private let prefersGrabberVisible: Bool
16 | private let prefersScrollingExpandsWhenScrolledToEdge: Bool
17 | private let prefersEdgeAttachedInCompactHeight: Bool
18 | private let widthFollowsPreferredContentSizeWhenEdgeAttached: Bool
19 | private var detentIdentifier: Binding?
20 | private let preferredCornerRadius: CGFloat?
21 | private let notificationName: Notification.Name
22 | private let onDisappear: (() -> Void)?
23 | private let contentView: UIHostingController
24 |
25 | public init(
26 | detents: [UISheetPresentationController.Detent] = [.medium(), .large()],
27 | largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = nil,
28 | prefersGrabberVisible: Bool = false,
29 | prefersScrollingExpandsWhenScrolledToEdge: Bool = true,
30 | prefersEdgeAttachedInCompactHeight: Bool = false,
31 | widthFollowsPreferredContentSizeWhenEdgeAttached: Bool = false,
32 | detentIdentifier: Binding? = nil,
33 | preferredCornerRadius: CGFloat?,
34 | notificationName: Notification.Name = .bottomSheetDetentIdentifierDidChanged,
35 | onDisappear: (() -> Void)? = nil,
36 | content: Content
37 | ) {
38 | self.detents = detents
39 | self.largestUndimmedDetentIdentifier = largestUndimmedDetentIdentifier
40 | self.prefersGrabberVisible = prefersGrabberVisible
41 | self.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge
42 | self.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight
43 | self.widthFollowsPreferredContentSizeWhenEdgeAttached = widthFollowsPreferredContentSizeWhenEdgeAttached
44 | self.detentIdentifier = detentIdentifier
45 | self.preferredCornerRadius = preferredCornerRadius
46 | self.notificationName = notificationName
47 | self.onDisappear = onDisappear
48 | contentView = UIHostingController(rootView: content)
49 |
50 | super.init(nibName: nil, bundle: nil)
51 | }
52 |
53 | @available(*, unavailable)
54 | required init?(coder: NSCoder) {
55 | fatalError("init(coder:) has not been implemented")
56 | }
57 |
58 | override public func viewDidLoad() {
59 | super.viewDidLoad()
60 |
61 | addChild(contentView)
62 | view.addSubview(contentView.view)
63 |
64 | contentView.view.translatesAutoresizingMaskIntoConstraints = false
65 |
66 | NSLayoutConstraint.activate([
67 | contentView.view.topAnchor.constraint(equalTo: view.topAnchor),
68 | contentView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
69 | contentView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
70 | contentView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
71 | ])
72 |
73 | if let presentationController = presentationController as? UISheetPresentationController {
74 | presentationController.detents = detents
75 | presentationController.largestUndimmedDetentIdentifier = largestUndimmedDetentIdentifier
76 | presentationController.prefersGrabberVisible = prefersGrabberVisible
77 | presentationController.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge
78 | presentationController.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight
79 | presentationController.widthFollowsPreferredContentSizeWhenEdgeAttached = widthFollowsPreferredContentSizeWhenEdgeAttached
80 | presentationController.preferredCornerRadius = preferredCornerRadius
81 | presentationController.delegate = self
82 | }
83 | }
84 |
85 | override public func viewDidDisappear(_ animated: Bool) {
86 | super.viewDidDisappear(animated)
87 | onDisappear?()
88 | }
89 |
90 | func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_ sheetPresentationController: UISheetPresentationController) {
91 | guard let selectedDetentIdentifier = sheetPresentationController.selectedDetentIdentifier else { return }
92 | detentIdentifier?.wrappedValue = selectedDetentIdentifier
93 | NotificationCenter.default.post(name: .bottomSheetDetentIdentifierDidChanged, object: selectedDetentIdentifier)
94 | }
95 | }
96 |
97 | public extension Notification.Name {
98 | static let bottomSheetDetentIdentifierDidChanged = Notification.Name("bottomSheetDetentIdentifierDidChanged")
99 | }
100 |
--------------------------------------------------------------------------------
/Sources/SheetKit/ClearBackgournd.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yang Xu on 2021/9/16.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | struct BackgroundCleanerView: UIViewRepresentable {
12 | func makeUIView(context: Context) -> UIView {
13 | let view = UIView()
14 | DispatchQueue.main.async {
15 | view.superview?.superview?.backgroundColor = .clear
16 | }
17 | return view
18 | }
19 |
20 | func updateUIView(_ uiView: UIView, context: Context) {}
21 | }
22 |
23 | public extension View{
24 | @ViewBuilder
25 | func clearBackground(_ enable:Bool = true) -> some View{
26 | if enable{
27 | background(BackgroundCleanerView())
28 | }
29 | else {
30 | self
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/SheetKit/InteractiveDismissDisabled.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Yang Xu on 2021/9/16.
6 | //
7 |
8 | import SwiftUI
9 | import UIKit
10 |
11 | struct SetSheetDelegate: UIViewRepresentable {
12 | let delegate: SheetDelegate
13 |
14 | init(isDisable: Bool, attempToDismiss: Binding) {
15 | delegate = SheetDelegate(isDisable, attempToDismiss: attempToDismiss)
16 | }
17 |
18 | func makeUIView(context: Context) -> some UIView {
19 | let view = UIView()
20 | return view
21 | }
22 |
23 | func updateUIView(_ uiView: UIViewType, context: Context) {
24 | DispatchQueue.main.async {
25 | if uiView.parentViewController?.sheetPresentationController != nil {
26 | weak var sheetController = uiView.parentViewController?.sheetPresentationController
27 | delegate.originalDelegate = sheetController?.delegate
28 | sheetController?.delegate = delegate
29 | } else {
30 | uiView.parentViewController?.presentationController?.delegate = delegate
31 | }
32 | }
33 | }
34 | }
35 |
36 | final class SheetDelegate: NSObject, UIAdaptivePresentationControllerDelegate, UISheetPresentationControllerDelegate {
37 | var isDisable: Bool
38 | @Binding var attempToDismiss: UUID
39 | weak var originalDelegate:UISheetPresentationControllerDelegate?
40 |
41 | init(_ isDisable: Bool, attempToDismiss: Binding = .constant(UUID())) {
42 | self.isDisable = isDisable
43 | _attempToDismiss = attempToDismiss
44 | }
45 |
46 | func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
47 | !isDisable
48 | }
49 |
50 | func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
51 | attempToDismiss = UUID()
52 | }
53 |
54 | func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_ sheetPresentationController: UISheetPresentationController) {
55 | originalDelegate?.sheetPresentationControllerDidChangeSelectedDetentIdentifier?(sheetPresentationController)
56 | }
57 | }
58 |
59 | public extension View {
60 | func interactiveDismissDisabled(_ isDisable: Bool, attempToDismiss: Binding) -> some View {
61 | background(SetSheetDelegate(isDisable: isDisable, attempToDismiss: attempToDismiss))
62 | }
63 | }
64 |
65 | public extension UIView {
66 | var parentViewController: UIViewController? {
67 | var parentResponder: UIResponder? = next
68 | while parentResponder != nil {
69 | if let viewController = parentResponder as? UIViewController {
70 | return viewController
71 | }
72 | parentResponder = parentResponder?.next
73 | }
74 | return nil
75 | }
76 | }
77 |
78 |
--------------------------------------------------------------------------------
/Sources/SheetKit/SheetKit.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SheetKit.swift
3 | // SheetManager
4 | //
5 | // Created by Yang Xu on 2021/9/16.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import UIKit
11 |
12 | public struct SheetKit {
13 | /// dismiss all sheets
14 | /// - Parameters:
15 | /// - flag: Pass true to animate the transition.
16 | /// - completion: The block to execute after the view controller is dismissed. This block has no return value and takes no parameters. You may specify nil for this parameter.
17 | public func dismissAllSheets(animated flag: Bool = true, completion: (() -> Void)? = nil) {
18 | rootViewController?.dismiss(animated: flag, completion: completion)
19 | }
20 |
21 | /// dismiss top sheet
22 | /// - Parameters:
23 | /// - flag: Pass true to animate the transition.
24 | /// - completion: The block to execute after the view controller is dismissed. This block has no return value and takes no parameters. You may specify nil for this parameter.
25 | public func dismiss(animated flag: Bool = true, completion: (() -> Void)? = nil) {
26 | rootViewController?.topmostPresentingViewController?.dismiss(animated: flag, completion: completion)
27 | }
28 |
29 | /// present seht
30 | /// - Parameters:
31 | /// - controller: 从哪个UIViewController创建sheet。默认值即可
32 | /// - style: Sheet 的样式,目前支持 sheet fullScreenCover bottomSheet 以及 customBottomSheet(自定义)
33 | /// - animated: 是否开启动画
34 | /// - afterPresent: 展示后执行的block
35 | /// - onDisappear: viewDidDisappeare时执行的block
36 | /// - configration: 自定义bottomSheet的配置
37 | /// - detentIdentifier: 当modal状态发生变化时(高度变化)通知绑定值
38 | /// - content: 视图内容
39 | public func present(in controller: ControllerSource = .rootController,
40 | with style: SheetStyle = .sheet,
41 | animated: Bool = true,
42 | afterPresent: (() -> Void)? = nil,
43 | onDisappear:(() -> Void)? = nil,
44 | configuration: BottomSheetConfiguration? = nil,
45 | detentIdentifier: Binding? = nil,
46 | content: () -> Content)
47 | {
48 | let viewController = controller == .rootController ? rootViewController?.topmostPresentedViewController : rootViewController?.topmostViewController
49 |
50 | let contentViewController: UIViewController
51 |
52 | switch style {
53 | case .sheet:
54 | contentViewController = MyUIHostingController(rootView: content(),onDisappear: onDisappear)
55 | case .fullScreenCover:
56 | contentViewController = MyUIHostingController(rootView: content(),onDisappear: onDisappear)
57 | contentViewController.modalPresentationStyle = .fullScreen
58 | case .bottomSheet:
59 | let configuration = BottomSheetConfiguration.default
60 | contentViewController = BottomSheetViewController(detents: configuration.detents,
61 | largestUndimmedDetentIdentifier: configuration.largestUndimmedDetentIdentifier,
62 | prefersGrabberVisible: configuration.prefersGrabberVisible,
63 | prefersScrollingExpandsWhenScrolledToEdge: configuration.prefersScrollingExpandsWhenScrolledToEdge,
64 | prefersEdgeAttachedInCompactHeight: configuration.prefersEdgeAttachedInCompactHeight,
65 | widthFollowsPreferredContentSizeWhenEdgeAttached: configuration.widthFollowsPreferredContentSizeWhenEdgeAttached,
66 | detentIdentifier: detentIdentifier,
67 | preferredCornerRadius: configuration.preferredCornerRadius,
68 | onDisappear: onDisappear,
69 | content: content())
70 | case .customBottomSheet:
71 | guard let configuration = configuration else { fatalError("configuration can't be nil in customBottomSheet style.") }
72 | contentViewController = BottomSheetViewController(detents: configuration.detents,
73 | largestUndimmedDetentIdentifier: configuration.largestUndimmedDetentIdentifier,
74 | prefersGrabberVisible: configuration.prefersGrabberVisible,
75 | prefersScrollingExpandsWhenScrolledToEdge: configuration.prefersScrollingExpandsWhenScrolledToEdge,
76 | prefersEdgeAttachedInCompactHeight: configuration.prefersEdgeAttachedInCompactHeight,
77 | widthFollowsPreferredContentSizeWhenEdgeAttached: configuration.widthFollowsPreferredContentSizeWhenEdgeAttached,
78 | detentIdentifier: detentIdentifier,
79 | preferredCornerRadius: configuration.preferredCornerRadius,
80 | onDisappear: onDisappear,
81 | content: content())
82 | }
83 |
84 | viewController?.present(contentViewController, animated: animated, completion: afterPresent)
85 | }
86 |
87 | public init() {}
88 | }
89 |
90 | public extension SheetKit {
91 | var keyWindow: UIWindow? { UIApplication.shared.connectedScenes
92 | .filter { $0.activationState == .foregroundActive }
93 | .map { $0 as? UIWindowScene }
94 | .compactMap { $0 }
95 | .first?.windows
96 | .filter { $0.isKeyWindow }.first
97 | }
98 |
99 | var rootViewController: UIViewController? {
100 | keyWindow?.rootViewController
101 | }
102 | }
103 |
104 | public extension SheetKit {
105 | /// Sheet 类型
106 | enum SheetStyle {
107 | case sheet
108 | case fullScreenCover
109 | case bottomSheet
110 | case customBottomSheet
111 | }
112 |
113 | /// 在哪个ViewController上添加sheet
114 | enum ControllerSource {
115 | case rootController
116 | case topController
117 | }
118 |
119 | struct BottomSheetConfiguration {
120 | /// BottomSheet配置
121 | /// - Parameters:
122 | /// - detents: 允许的高度,默认[.medium(), .large()],第一个为sheet初次显示的位置
123 | /// - largestUndimmedDetentIdentifier: 交互遮罩尺寸。默认为nil(相当于.large),如果设置为.medium,当显示半高时,Sheet下的视图可交互
124 | /// - prefersGrabberVisible: 是否显示模态视图上方的抓取提示
125 | /// - prefersScrollingExpandsWhenScrolledToEdge: 模态视图中的滚动是否会影响模态视图高度。如果想在半高时,顺利滚动,需设置为false
126 | /// - prefersEdgeAttachedInCompactHeight: 模态视图是否以紧凑高度尺寸附加到屏幕的底部边缘
127 | /// - widthFollowsPreferredContentSizeWhenEdgeAttached: 模态视图的宽度是否于视图控制器的首选内容大小相匹配
128 | /// - preferredCornerRadius: 模态视图圆角值
129 | public init(detents: [UISheetPresentationController.Detent],
130 | largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?,
131 | prefersGrabberVisible: Bool,
132 | prefersScrollingExpandsWhenScrolledToEdge: Bool,
133 | prefersEdgeAttachedInCompactHeight: Bool,
134 | widthFollowsPreferredContentSizeWhenEdgeAttached: Bool,
135 | preferredCornerRadius: CGFloat?)
136 | {
137 | self.detents = detents
138 | self.largestUndimmedDetentIdentifier = largestUndimmedDetentIdentifier
139 | self.prefersGrabberVisible = prefersGrabberVisible
140 | self.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge
141 | self.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight
142 | self.widthFollowsPreferredContentSizeWhenEdgeAttached = widthFollowsPreferredContentSizeWhenEdgeAttached
143 | self.preferredCornerRadius = preferredCornerRadius
144 | }
145 |
146 | let detents: [UISheetPresentationController.Detent]
147 | let largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier?
148 | let prefersGrabberVisible: Bool
149 | let prefersScrollingExpandsWhenScrolledToEdge: Bool
150 | let prefersEdgeAttachedInCompactHeight: Bool
151 | let widthFollowsPreferredContentSizeWhenEdgeAttached: Bool
152 | let preferredCornerRadius: CGFloat?
153 |
154 | static let `default` = BottomSheetConfiguration(detents: [.medium(), .large()],
155 | largestUndimmedDetentIdentifier: nil,
156 | prefersGrabberVisible: false,
157 | prefersScrollingExpandsWhenScrolledToEdge: true,
158 | prefersEdgeAttachedInCompactHeight: true,
159 | widthFollowsPreferredContentSizeWhenEdgeAttached: true,
160 | preferredCornerRadius: nil)
161 | }
162 | }
163 |
164 | // MARK: - Environment
165 |
166 | public struct SheetKitKey: EnvironmentKey {
167 | public static var defaultValue = SheetKit()
168 | }
169 |
170 | public extension EnvironmentValues {
171 | var sheetKit: SheetKit { self[SheetKitKey.self] }
172 | }
173 |
174 | // MARK: - UIHostingController
175 |
176 | final class MyUIHostingController: UIHostingController {
177 | var onDisappear: (() -> Void)?
178 | override func viewDidDisappear(_ animated: Bool) {
179 | super.viewDidDisappear(animated)
180 | onDisappear?()
181 | }
182 |
183 | init(rootView: Content,onDisappear:(() -> Void)? = nil) {
184 | self.onDisappear = onDisappear
185 | super.init(rootView: rootView)
186 | }
187 |
188 | @MainActor @objc required dynamic init?(coder aDecoder: NSCoder) {
189 | fatalError("init(coder:) has not been implemented")
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/Sources/SheetKit/UIKit++.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIKit++.swift
3 | //
4 | //
5 | // Created by Yang Xu on 2021/9/16.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIViewController {
11 | var topmostPresentedViewController: UIViewController? {
12 | presentedViewController?.topmostPresentedViewController ?? self
13 | }
14 |
15 | var topmostViewController: UIViewController? {
16 | if let controller = (self as? UINavigationController)?.visibleViewController {
17 | return controller.topmostViewController
18 | } else if let controller = (self as? UITabBarController)?.selectedViewController {
19 | return controller.topmostViewController
20 | } else if let controller = presentedViewController {
21 | return controller.topmostViewController
22 | } else {
23 | return self
24 | }
25 | }
26 |
27 | var topmostPresentingViewController: UIViewController? {
28 | topmostViewController?.presentingViewController
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Tests/SheetKitTests/SheetKitTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import SheetKit
3 |
4 | final class SheetKitTests: 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(SheetKit().text, "Hello, World!")
10 | }
11 | }
12 |
--------------------------------------------------------------------------------