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