Apple-Inspired-Puller

develop

Bottom sheet for iOS that I call puller. It has Apple's native API on iOS 11+, works like in Apple Music app and supports interactive pop gesture like in Telegram app. Supports SwiftUI.
zsergey/Apple-Inspired-Puller

๐Ÿฅ‡ Apple-Inspired Puller

Apple has released a sheet presentation controller in iOS 15, which works great. In iOS 16, developers can customize the detents of the sheet. However, we would like to use these features in iOS 14 as well. That's why I have developed a puller presentation controller with the same API as Apple's. The puller works even in iOS 11. Please feel free to use the puller and give it a star if you find it useful.

๐ŸŒˆ Features

  • Extremely easy to use with a native API-like interface.
  • Customizable puller presentation animation with two presets: default and spring.
  • Compatible with iOS 11+, the puller offers four preset detents: medium, large, full and fitsContent. In the full state, the puller supports device corners, like the Apple Music app. When using the fitsContent state, the puller adapts its height to the default height of the presenting view controller's view. Additionally, you can set as many custom detents you want.
  • Supports inside and outside drag indicators.
  • Supports interactive pop gesture.
  • Works seamlessly with ScrollView.
  • Compatible with keyboard and device rotation.
  • Can be opened in a similar style to the AirPods Pro sheet as shown by Apple.
  • Includes various settings for customization and control.
  • Unfortunately, the puller doesnโ€™t support view controllers like UIColorPickerViewController cause they are running in another process and using UIRemoteView under the hood.

๐Ÿ’ก Attention

The puller uses a specific approach to obtain the screen corner radius by adding a displayCornerRadius property to UIScreen, which reads the private _displayCornerRadius. The selector somewhat obscured, which usually means it will get past app review. However, use at your own risk!

๐Ÿ›  Examples

Default puller

Default puller with three detents: custom, medium and large. Drag indicator is inside.

Source Code
let viewController = UIViewController()

let pullerModel = PullerModel(detents: [.custom(0.25), .medium, .full], 
                              dragIndicator: .inside(.black))

presentAsPuller(viewController, model: pullerModel)

Spring puller

Spring puller in full state. Drag indicator is outside.

Source Code
let viewController = UIViewController()

let pullerModel = PullerModel(animator: .spring, 
                              detents: [.full], 
                              dragIndicator: .outside(.black))

presentAsPuller(viewController, model: pullerModel)

Supporting interactive pop gesture

Spring puller in full state that supports interactive pop gesture.

Source Code
let viewController = UIViewController()

let pullerModel = PullerModel(animator: .spring, 
                              detents: [.full], 
                              supportsInteractivePopGesture: true) // it's true by default

presentAsPuller(viewController, model: pullerModel)

Dialog style

Dialog style puller resembles Apple's AirPods Pro sheet.

Source Code
let viewController = UIViewController()

let pullerModel = PullerModel(animator: .spring, 
                              detents: [.medium], 
                              hasDynamicHeight: false)

presentAsPuller(viewController, model: pullerModel)

How to use fitsContent detent

The intrinsicContentSize property of the view controller being presented must be set for the fitsContent detent to work properly in the puller. If the intrinsicContentSize property is not set, the puller will default to the large detent.

Source Code
class YourViewController: UIViewController {

    override func loadView() {
        view = ResizableView()
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        (view as? ResizableView)?.defaultHeight = 300
    }
}

class ResizableView: UIView {
    
    var defaultHeight: CGFloat? 
    
    override var intrinsicContentSize: CGSize {
        if let defaultHeight = defaultHeight {
            return CGSize(width: UIView.noIntrinsicMetric, height: defaultHeight)
        }
        return super.intrinsicContentSize
    }
}

let viewController = YourViewController()

let pullerModel = PullerModel(animator: .spring, 
                              detents: [.fitsContent])

presentAsPuller(viewController, model: pullerModel)

Settings in Demo

Demo app includes settings for adjusting animation speed and customizing puller behavior.

Description

  • Swift Tools
View More Packages from this Author

Dependencies

  • None
Last updated: Thu Mar 28 2024 23:16:18 GMT-0900 (Hawaii-Aleutian Daylight Time)