Customize and resize sheets in SwiftUI with SheeKit. Utilise the power of `UISheetPresentationController` and other UIKit features.

Related tags

Utility ios uikit swiftui
Overview

SheeKit

Customize and resize sheets in SwiftUI with SheeKit. Utilise the power of UISheetPresentationController and other UIKit features.

Overview

SheeKit is a bridge between SwiftUI and UIKit which enriches the modal presentations in SwiftUI with the features available in UIKit.

SheeKit provides two modifiers for presenting the sheet, similar to SwiftUI.sheet(...):

  • controlled by isPresented boolean flag
  • controlled by the optional Identifiable item

Additionally, SheeKit allows to:

  • customize sheet detents to present half-screen sheets
  • define different modal presentation styles for different Identifiable items
  • customize the preferred presented view controller properties via UIViewControllerProxy
  • utilise the UIPopoverPresentationController.adaptiveSheetPresentationController and customize adaptive sheet for popover which will be used on iPhone and in compact horizontal size class of the scene on iPad.

Customizing sheet detents to present half-screen sheets

With iOS 15, sheets can be resizable between large() and medium() detents and animate the size transitions. In order to customize detents, provide SheetProperties to the ModalPresentationStyle/pageSheet(properties:) or ModalPresentationStyle/formSheet(properties:).

struct ShowLicenseAgreement: View {
    @State private var isShowingSheet = false
    @State private var selectedDetentIdentifier = UISheetPresentationController.Detent.Identifier.medium 
    var body: some View {
        Button(action: {
            isShowingSheet.toggle()
        }) {
            Text("Show License Agreement")
        }
        .shee(isPresented: $isShowingSheet,
              presentationStyle: .formSheet(properties: .init(detents: [ .medium(), .large() ], selectedDetentIdentifier: $selectedDetentIdentifier, animatesSelectedDetentIdentifierChange: true)),
              onDismiss: didDismiss) {
            VStack {
                Text("License Agreement")
                    .font(.title)
                    .padding(50)
                Text("""
                        Terms and conditions go here.
                    """)
                    .padding(50)
                Button("Dismiss",
                       action: { isShowingSheet.toggle() })
            }
        }
    }

    func didDismiss() {
        // Handle the dismissing action.
    }
}

Define different modal presentation styles for different Identifiable items

In SwiftUI, there are three different modifiers for popover, fullScreenCover and sheet, which don't allow the developer to show different styles of the dialog based on the same source of truth (provided by item).

With SheeKit, it's possible - just provide presentationStyle which corresponds to your item.

100500 ? [ .large() ] : [ .medium() ] return .formSheet(properties: sheetProperties) } } struct InventoryItem: Identifiable { var id: String let partNumber: String let quantity: Int let name: String } ">
struct ShowPartDetail: View {
    @State var sheetDetail: InventoryItem?
    var body: some View {
        Button("Show Part Details") {
            sheetDetail = InventoryItem(
                id: "0123456789",
                partNumber: "Z-1234A",
                quantity: 100,
                name: "Widget")
        }
        .shee(item: $sheetDetail,
              presentationStyle: presentationStyle,
              onDismiss: didDismiss) { detail in
            VStack(alignment: .leading, spacing: 20) {
                Text("Part Number: \(detail.partNumber)")
                Text("Name: \(detail.name)")
                Text("Quantity On-Hand: \(detail.quantity)")
            }
            .onTapGesture {
                sheetDetail = nil
            }
        }
    }

    func didDismiss() {
        // Handle the dismissing action.
    }

    var presentationStyle: ModalPresentationStyle {
        var sheetProperties = SheetProperties()
        sheetProperties.detents = sheetDetail?.quantity ?? 0 > 100500 ? [ .large() ] : [ .medium() ]
        return .formSheet(properties: sheetProperties)
    }
}

struct InventoryItem: Identifiable {
    var id: String
    let partNumber: String
    let quantity: Int
    let name: String
}

Customize the preferred presented view controller properties via UIViewControllerProxy

In UIKit, UIViewController class has many properties which allow to alter the user experience depending on the use case, like forbidding of interactive dismiss of the sheets via isModalInPresentation, customizing status bar appearance, preferred content size, or modal transition style. Unfortunately, this functionality is not exposed in SwiftUI. SheeKit solves this problem by allowing the consumer to provide UIViewControllerProxy which defines preferred parameters of the presented view controller.

struct ShowLicenseAgreement: View {
    @State private var isShowingSheet = false
    @State private var selectedDetentIdentifier = UISheetPresentationController.Detent.Identifier.medium 
    var body: some View {
        Button(action: {
            isShowingSheet.toggle()
        }) {
            Text("Show License Agreement")
        }
        .shee(isPresented: $isShowingSheet,
              presentationStyle: .formSheet(properties: .init(detents: [ .medium(), .large() ], selectedDetentIdentifier: $selectedDetentIdentifier, animatesSelectedDetentIdentifierChange: true)),
              presentedViewControllerParameters: presentedViewControllerParameters,
              onDismiss: didDismiss) {
            VStack {
                Text("License Agreement")
                    .font(.title)
                    .padding(50)
                Text("""
                        Terms and conditions go here.
                    """)
                    .padding(50)
                Button("Dismiss",
                       action: { isShowingSheet.toggle() })
            }
        }
    }

    func didDismiss() {
        // Handle the dismissing action.
    }

    var presentedViewControllerParameters: UIViewControllerProxy {
        var parameters = UIViewControllerProxy()
        parameters.preferredStatusBarStyle = .darkContent
        parameters.preferredStatusBarUpdateAnimation = .fade
        parameters.isModalInPresentation = true
        parameters.modalTransitionStyle = .flipHorizontal
        return parameters
    }
}

Utilise the adaptiveSheetPresentationController of UIPopoverPresentationController and customize adaptive sheet for popover

In SwiftUI, when popover is shown as a sheet when the user minimizes the app to the smallest size on top of the other app on iPad, or when the popover is shown on iPhone as a sheet, developer can't get a medium-detent sheet in a compact size class of a scene instead of a popover. The sheet into which popover adapts, is always with .large() detent.

SheeKit allows the developer to customize this behavior and to specify the detents for the sheet in which the popover adapts to, along with the preferred popover arrow direction and the source rect.

struct ShowLicenseAgreement: View {
    @State private var isShowingSheet = false
    @State private var selectedDetentIdentifier = UISheetPresentationController.Detent.Identifier.medium 
    var body: some View {
        Button(action: {
            isShowingSheet.toggle()
        }) {
            Text("Show License Agreement")
        }
        .shee(isPresented: $isShowingSheet,
              presentationStyle: .popover(permittedArrowDirections: .top, 
                                          sourceRectTransform: { $0.offsetBy(dx: 16, dy: 16) }, 
                                          adaptiveSheetProperties: .init(detents: [ .medium(), .large() ], 
                                                                         selectedDetentIdentifier: $selectedDetentIdentifier, 
                                                                         animatesSelectedDetentIdentifierChange: true)),
              onDismiss: didDismiss) {
            VStack {
                Text("License Agreement")
                    .font(.title)
                    .padding(50)
                Text("""
                        Terms and conditions go here.
                    """)
                    .padding(50)
                Button("Dismiss",
                       action: { isShowingSheet.toggle() })
            }
        }
    }

    func didDismiss() {
        // Handle the dismissing action.
    }
}

Demo of the library

SheeKit Demo on YouTube

Installation

Xcode 13

  1. Select your project in File Navigator, then select the project again on top of the list of targets. You'll see list of packages.
  2. Press + button.
  3. In the appeared window, press + button in the bottom left corner.
  4. In the appeared menu, select "Add Swift Package Collection"
  5. In the appeared dialog, enter package collection URL: https://swiftpackageindex.com/edudnyk/collection.json
  6. Press "Add Collection"
  7. Select SheeKit package from the collection.
  8. In Frameworks, Libraries, and Embedded Content section of your app target change 'SheeKit' package Embed option to Embed & Sign.

If you want to use SheeKit in any other project that uses SwiftPM, add the package as a dependency in Package.swift:

dependencies: [
  .package(name: "SheeKit", url: "https://github.com/edudnyk/SheeKit.git", from: "0.0.3"),
]

Next, add SheeKit as a dependency of your test target:

targets: [
  .target(name: "MyApp", dependencies: ["SheeKit"], path: "Sources"),
]

Carthage

If you use Carthage, you can add the following dependency to your Cartfile:

0.0.3 ">
github "edudnyk/SheeKit" ~> 0.0.3

CocoaPods

If your project uses CocoaPods, add the pod to any applicable targets in your Podfile:

target 'MyApp' do
  pod 'SheeKit', '~> 0.0.3'
end

Topics

  • DismissAction
  • ModalPresentationStyle
  • ModalPresentationStyleCompat
  • SheetProperties
  • UIViewControllerProxy
Comments
  • Erro while building the App

    Erro while building the App

    Hi, I would like to use your library, followed all the instructions but I'm getting this compilation error:

    Screen Shot 2021-12-02 at 12 03 46 PM

    I did a new empty project to test and is working fine, but I cannot get it to work in this other project.

    Any suggestion?

    opened by gjimenezlr 11
  • Library not loaded

    Library not loaded

    I added the repository as a dependency in Xcode. It builds fine but when I got to run it on device I get the following error:

    dyld[880]: Library not loaded: @rpath/SheeKit.framework/SheeKit Referenced from: /private/var/containers/Bundle/Application/6DCB14C0-1835-447A-ADC9-55A89EFD9EA5/New.app/New Reason: tried: '/private/var/containers/Bundle/Application/6DCB14C0-1835-447A-ADC9-55A89EFD9EA5/New.app/Frameworks/SheeKit.framework/SheeKit' (no such file), '/private/var/containers/Bundle/Application/6DCB14C0-1835-447A-ADC9-55A89EFD9EA5/New.app/Frameworks/SheeKit.framework/SheeKit' (no such file), '/System/Library/Frameworks/SheeKit.framework/SheeKit' (no such file)

    opened by koedal 5
  • Application State

    Application State

    I have been playing around with this a bit (nice work BTW) and noticed that the presented view does not seem to have access to ApplicationState object. Any clue why that is? I have worked around it by isolating binding in cases where this is used but thought you might like to know that. Is this an issue, by design or just a limitation of how the concept was implemented in UIKit?

    Cheers

    -dean

    opened by DeanSCND 4
  • SplitView on macOS

    SplitView on macOS

    When I use SheeKit on my Mac Catalyst app, I noticed that the modal was always presented as a SplitView. While there was no indication in my code that could lead to such a screen.

    Here is a snippet of the code I wrote to present a modal as a form sheet:

    VStack {
        ...
    }
    .shee(isPresented: $isPresentingAddExpense, 
          presentationStyle: .formSheet(), 
          onDismiss: { isPresentingAddExpense = false }) {
        NavigationView {
            ExpenseFormView()
        }
    }
    

    Is this a known issue?

    opened by fousa 4
  • switch Toggle will adjust sheet from large to medium

    switch Toggle will adjust sheet from large to medium

    Hi,

    first of all: thank you for this great project. I discovered an issue I cannot explain me: I created a sheet with medium and large detent with the following code:

    .shee(
    isPresented: self.$sheet.isShowing,
    presentationStyle: .formSheet(properties: SheetProperties(prefersEdgeAttachedInCompactHeight: true, prefersGrabberVisible: true, detents: [.medium(), .large()], largestUndimmedDetentIdentifier: .medium, prefersScrollingExpandsWhenScrolledToEdge: false)),
    content: {self.sheetContent()}
    )
    

    I inserted some Toggles in a List to the sheet. If the sheet is pulled up to the large detent and I switch one toggle, the current detent of the sheet will immediately changed to medium. This does not happen with other controls like Buttons. Could this be a problem with Toggles because their toggle status has to be published and the whole view will be redrawn, i.e. the sheet will drawn in its start detent? Or could you imagine another reason for this behaviour?

    Best regards, Christoph

    opened by jagodki 4
  • Popover presented from a button in nav bar messes with geometry of content view (iPad)

    Popover presented from a button in nav bar messes with geometry of content view (iPad)

    I'm using a popover presented from a button in a nav bar. It works fine, but there's an issue on iPad. After the popover is presented, if you resize the app, the content view no longer resizes accordingly. I made a few simple changes to the example app that demonstrates this. Here is a screenshot and movie that shows the issue. Also attached is a modified RootView.swift with my changes. They are marked with "// MAK".

    Simulator Screen Shot - iPad

    https://user-images.githubusercontent.com/73647/153730364-536b29b3-bd94-444a-bbff-1038a29b80f5.mov

    RootView.txt

    opened by markkrenek 3
  • `onDismiss` handler is not called when the sheet dismisses itself

    `onDismiss` handler is not called when the sheet dismisses itself

    Thanks for the great library! I've noticed an issue where SheeKit's onDismiss handler is not called when the sheet dismisses itself by executing the dismiss closure provided by the SwiftUI environment. Here is a quick example:

    import SheeKit
    import SwiftUI
    
    struct ContentView: View {
        
        @State private var isPresentingSheet: Bool = false
        
        var body: some View {
            Button("Present Sheet") {
                isPresentingSheet = true
            }
            .shee(isPresented: $isPresentingSheet, onDismiss: {
                print("Dismiss")
            }, content: {
                SheetView()
            })
        }
    }
    
    struct SheetView: View {
        
        @Environment(\.dismiss) private var dismiss
        
        var body: some View {
            Button("Close") {
                dismiss()
            }
        }
    }
    

    When I replace shee with a regular sheet, the callback works fine.

    opened by FelixLisczyk 2
  • Cannot convert value of type 'Binding<UISheetPresentationController.Detent.Identifier>'

    Cannot convert value of type 'Binding'

    Hello,

    Thank you for this useful Swift package! I'm trying to integrate this into my project but I'm getting the following error:

    Screen Shot 2022-02-09 at 10 53 00 AM

    Cannot convert value of type 'Binding<UISheetPresentationController.Detent.Identifier>' to expected argument type 'Binding<UISheetPresentationController.Detent.Identifier?>'

    I have this variable set as follows: @State private var selectedDetentIdentifier = UISheetPresentationController.Detent.Identifier.medium

    Thanks for your time

    opened by jtansley 2
  • Enhance Presenter Proxy

    Enhance Presenter Proxy

    Before this change, a "SheetHost" view controller might not be presented if the presenter already presented another view controller. This might have happened when a sheet will be presented due to a state change originating from an app state, rather from a user intent, and when there is already a presented view or a presented alert.

    This change walks the presented view controller list until it finds a view controller which has no presented view controller and uses this as the presenter. As a result, a sheet now presents on top of any other presented view, even on top of alerts.

    opened by couchdeveloper 0
  • Support all UISheetPresentationController customization options

    Support all UISheetPresentationController customization options

    Could you support all UISheetPresentationController customization options ?

    https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller

    opened by X901 0
  • Optionally override interface style of presented view controller.

    Optionally override interface style of presented view controller.

    This PR allows overriding the interface style of the presented view controller. Specifically, it adds the ability to set the overrideUserInterfaceStyle property on the view controller before it is presented. This allows omitting the .preferredColorScheme(.dark) modifier, which didn't seem to survive the presentation and retroactively caused interface style changes in the view calling .shee(....

    opened by lightandshadow68 0
  • Feature: Ability to change background color

    Feature: Ability to change background color

    Hey @edudnyk Great package. I'm loving it.

    One small thing, is it possible to enable us to have our own background color? The only way to achieve this is by doing:

    
    .shee(isPresented: $isShowingSheet,
                  presentationStyle: .formSheet(properties: .init(detents: [ .medium(), .large() ], selectedDetentIdentifier: $selectedDetentIdentifier, animatesSelectedDetentIdentifierChange: true)),
                  presentedViewControllerParameters: presentedViewControllerParameters,
                  onDismiss: didDismiss) {
                VStack {
                    Text("License Agreement")
                        .font(.title)
                        .padding(50)
                    Text("""
                            Terms and conditions go here.
                        """)
                        .padding(50)
                    Button("Dismiss",
                           action: { isShowingSheet.toggle() })
                }
            }
            // Background color
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color.gray.edgesIgnoringSafeArea(.all))
    
    opened by SylarRuby 0
  • How to dim background view when open view ?

    How to dim background view when open view ?

    I notice you disable background view under sheet , after opened sheet could you allow dim background view ? so the view under sheet become darker color ?

    opened by X901 0
Owner
Eugene Dudnyk
Eugene Dudnyk
Measure the power output from a car or any moving vehicle from GPS data and weight

GPSDyno Measure the power output from a car or any moving vehicle from weight and GPS data of your iOS device. This is just a example project and shou

Marcelo Ferreira Barreto 0 Jan 7, 2022
A Swift package for rapid development using a collection of micro utility extensions for Standard Library, Foundation, and other native frameworks.

ZamzamKit ZamzamKit is a Swift package for rapid development using a collection of micro utility extensions for Standard Library, Foundation, and othe

Zamzam Inc. 261 Dec 15, 2022
Shared repository for architecture and other iOS helpers.

ArchKit A shared package for all the infrastructure required to support the Architecture styles I use in my own apps. Very shortly, this architecture

Rachel Brindle 0 Jan 8, 2022
A μframework of extensions for SequenceType in Swift 2.0, inspired by Python's itertools, Haskell's standard library, and other things.

SwiftSequence Full reference here. (If you're looking for data structures in Swift, those have been moved to here) SwiftSequence is a lightweight fram

Donnacha Oisín Kidney 376 Oct 12, 2022
Pure Declarative Programming in Swift, Among Other Things

Basis The Basis is an exploration of pure declarative programming and reasoning in Swift. It by no means contains idiomatic code, but is instead inten

TypeLift 314 Dec 22, 2022
An open source Instapaper clone that features apps and extensions that use native UI Components for Mac and iOS.

TODO: Screenshot outdated Hipstapaper - iOS and Mac Reading List App A macOS, iOS, and iPadOS app written 100% in SwiftUI. Hipstapaper is an app that

Jeffrey Bergier 51 Nov 15, 2022
Butterfly is a lightweight library for integrating bug-report and feedback features with shake-motion event.

Butterfly is a lightweight library for integrating bug-report and feedback features with shake-motion event. Goals of this project One of th

Zigii Wong 410 Sep 9, 2022
Showcase new features after an app update similar to Pages, Numbers and Keynote.

WhatsNew Description WhatsNew automatically displays a short description of the new features when users update your app. This is similar to what happe

Patrick Balestra 1.5k Jan 4, 2023
WhatsNewKit enables you to easily showcase your awesome new app features.

WhatsNewKit enables you to easily showcase your awesome new app features. It's designed from the ground up to be fully customized to your needs. Featu

Sven Tiigi 2.8k Jan 3, 2023
🟣 Verge is a very tunable state-management engine on iOS App (UIKit / SwiftUI) and built-in ORM.

Verge is giving the power of state-management in muukii/Brightroom v2 development! Verge.swift ?? An effective state management architecture for iOS -

VergeGroup 478 Dec 29, 2022
Pigeon is a SwiftUI and UIKit library that relies on Combine to deal with asynchronous data.

Pigeon ?? Introduction Pigeon is a SwiftUI and UIKit library that relies on Combine to deal with asynchronous data. It is heavily inspired by React Qu

Fernando Martín Ortiz 369 Dec 30, 2022
DGPreview - Make UIKit project enable preview feature of SwiftUI

DGPreview Make UIKit project enable preview feature of SwiftUI Requirements iOS

donggyu 5 Feb 14, 2022
Swifty closures for UIKit and Foundation

Closures is an iOS Framework that adds closure handlers to many of the popular UIKit and Foundation classes. Although this framework is a substitute f

Vinnie Hesener 1.7k Dec 21, 2022
This is a app developed in Swift, using Object Oriented Programing, UIKit user interface programmatically, API Request and Kingfisher to load remote images

iOS NOW ⭐ This is a app developed in Swift, using Object Oriented Programing, UIKit user interface programmatically, API Request and Kingfisher to loa

William Tristão de Paula 1 Dec 7, 2021
SwiftExtensionKit - SwiftExtensionKit is to contain generic extension helpers for UIKit and Foundation

RichAppz PureSwiftExtensionKit SwiftExtensionKit is to contain generic extension

Rich Mucha 0 Jan 31, 2022
FastLayout - A UIKit or AppKit package for fast UI design

FastLayout FastLayout is a UIKit or AppKit package for fast UI design. Layout Ex

null 1 Feb 19, 2022
Swift-HorizontalPickerView - Customizable horizontal picker view component written in Swift for UIKit/iOS

Horizontal Picker View Customizable horizontal picker view component written in

Afraz Siddiqui 8 Aug 1, 2022
Unit-Converter-SwiftUI - A simple Unit Converter iOS app built in the process of learning SwiftUI

SwiftUI-Unit-Converter A simple Unit Converter iOS app built in the process of l

Ishaan Bedi 2 Jul 13, 2022
Safe and fast access to SwiftUI PreviewDevice

SafePreviewDevice Motivation At WWDC 2019, Apple announced SwiftUI a new library for building UI in a simple and fast way. Xcode’s SwiftUI preview let

Antonino Francesco Musolino 11 Jun 28, 2022