BottomSheetDemo - Bottom sheet modal view controller with swift

Overview

使用了 bottom sheet view 的 apps

当我们想弹出一个预览视图,bottom sheet modal view controller 非常实用。在 iOS 中,长按拖拽手势可以让 controller 上滑或者向下消失。

最终效果

实现原理是,通过监听拖拽事件,动态改变 view 之间的 auto layout 约束,并加上少许动画。

下面看源码:

第一个页面 ViewController.swift:

import UIKit

class ViewController: UIViewController {
    
    // Defined UI views
    lazy var titleLabel: UILabel = {
        let label = UILabel()
        label.text = "百字令·偶忆"
        label.font = .boldSystemFont(ofSize: 32)
        return label
    }()

    
    lazy var textView: UITextView = {
        let textView = UITextView(frame: .zero)
        textView.font = UIFont.systemFont(ofSize: 22)
        textView.isEditable = false
        textView.text = "横街南巷,记钿车小小,翠帘徐揭。绿酒分曹人散后,心事低徊潜说。莲子湖头,枇杷花下,绾就同心结。明珠未斛,朔风千里催别。\n同是沦落天涯,青青柳色,争忍先攀折。红浪香温围夜玉,堕我怀中明月。暮雨空归,秋河不动,虬箭丁丁咽。十年一梦,鬓丝今已如雪。 "
        return textView
    }()
    
    lazy var registerButton: UIButton = {
        let button = UIButton()
        button.setTitle("Get Started", for: .normal)
        button.setTitleColor(.white, for: .normal)
        button.backgroundColor = view.tintColor
        button.layer.cornerRadius = 8
        button.clipsToBounds = true
        return button
    }()
    
    lazy var containerStackView: UIStackView = {
        let spacer = UIView()
        let stackView = UIStackView(arrangedSubviews: [titleLabel, textView, spacer, registerButton])
        stackView.axis = .vertical
        stackView.spacing = 16.0
        return stackView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupView()
        setupConstraints()
        // 3. Add action
        registerButton.addTarget(self, action: #selector(presentModalController), for: .touchUpInside)

    }
    
    func setupView() {
        // cosmetics
        view.backgroundColor = .systemBackground
    }
    
    // Add subviews and set constraints
    func setupConstraints() {
        view.addSubview(containerStackView)
        containerStackView.translatesAutoresizingMaskIntoConstraints = false
        
        let safeArea = view.safeAreaLayoutGuide
        // Call .activate method to enable the defined constraints
        NSLayoutConstraint.activate([
            // Set containerStackView edges to superview with 24 spacing
            containerStackView.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: 24),
            containerStackView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor, constant: -24),
            containerStackView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor, constant: 24),
            containerStackView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor, constant: -24),
            // Set button height
            registerButton.heightAnchor.constraint(equalToConstant: 50)
        ])
    }
    
    // To be updated
    @objc func presentModalController() {
        let vc = CustomModalViewController()
        vc.modalPresentationStyle = .overCurrentContext
        // Keep animated value as false
        // Custom Modal presentation animation will be handled in VC itself
        self.present(vc, animated: false)
    } 
}

第二个页面 CustomModalViewController.swift:

import UIKit

class CustomModalViewController: UIViewController {
    
    // define lazy views
    lazy var titleLabel: UILabel = {
        let label = UILabel()
        label.text = "Get Started"
        label.font = .boldSystemFont(ofSize: 20)
        return label
    }()
    
    lazy var notesLabel: UILabel = {
        let label = UILabel()
        label.text = "横街南巷,记钿车小小,翠帘徐揭。绿酒分曹人散后,心事低徊潜说。莲子湖头,枇杷花下,绾就同心结。明珠未斛,朔风千里催别。\n同是沦落天涯,青青柳色,争忍先攀折。红浪香温围夜玉,堕我怀中明月。暮雨空归,秋河不动,虬箭丁丁咽。十年一梦,鬓丝今已如雪。 "
        label.font = .systemFont(ofSize: 16)
        label.textColor = .darkGray
        label.numberOfLines = 0
        return label
    }()
    
    lazy var contentStackView: UIStackView = {
        let spacer = UIView()
        let stackView = UIStackView(arrangedSubviews: [titleLabel, notesLabel, spacer])
        stackView.axis = .vertical
        stackView.spacing = 12.0
        return stackView
    }()
    
    lazy var containerView: UIView = {
        let view = UIView()
        view.backgroundColor = .white
        view.layer.cornerRadius = 16
        view.clipsToBounds = true
        return view
    }()
    
    let maxDimmedAlpha: CGFloat = 0.6
    lazy var dimmedView: UIView = {
        let view = UIView()
        view.backgroundColor = .black
        view.alpha = maxDimmedAlpha
        return view
    }()
    
    let defaultHeight: CGFloat = 300
    let dismissibleHeight: CGFloat = 200
    let maximumContainerHeight: CGFloat = UIScreen.main.bounds.height - 64
    // keep updated with new height
    var currentContainerHeight: CGFloat = 300
    
    // Dynamic container constraint
    var containerViewHeightConstraint: NSLayoutConstraint?
    var containerViewBottomConstraint: NSLayoutConstraint?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupView()
        setupConstraints()
        
        // tap gesture on dimmed view to dismiss
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.handleCloseAction))
        dimmedView.addGestureRecognizer(tapGesture)
        
        setupPanGesture()
    }
    
    @objc func handleCloseAction() {
        animateDismissView()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        animateShowDimmedView()
        animatePresentContainer()
    }
    
    func setupView() {
        view.backgroundColor = .clear
    }
    
    func setupConstraints() {
        // Add subviews
        view.addSubview(dimmedView)
        view.addSubview(containerView)
        dimmedView.translatesAutoresizingMaskIntoConstraints = false
        containerView.translatesAutoresizingMaskIntoConstraints = false
        
        containerView.addSubview(contentStackView)
        contentStackView.translatesAutoresizingMaskIntoConstraints = false
        
        // Set static constraints
        NSLayoutConstraint.activate([
            // set dimmedView edges to superview
            dimmedView.topAnchor.constraint(equalTo: view.topAnchor),
            dimmedView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            dimmedView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            dimmedView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            // set container static constraint (trailing & leading)
            containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            // content stackView
            contentStackView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 32),
            contentStackView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -20),
            contentStackView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20),
            contentStackView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -20),
        ])
        
        // Set dynamic constraints
        // First, set container to default height
        // after panning, the height can expand
        containerViewHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: defaultHeight)
        
        // By setting the height to default height, the container will be hide below the bottom anchor view
        // Later, will bring it up by set it to 0
        // set the constant to default height to bring it down again
        containerViewBottomConstraint = containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: defaultHeight)
        // Activate constraints
        containerViewHeightConstraint?.isActive = true
        containerViewBottomConstraint?.isActive = true
    
    }
    
    
    @objc func handlePanGesture(gesture: UIPanGestureRecognizer) {
        let translation = gesture.translation(in: view)
        // drag to top will be minus value and vice versa
        print("Pan gesture y offset: \(translation.y)")
        
        // get drag direction
        let isDraggingDown = translation.y > 0
        print("Dragging direction: \(isDraggingDown ? "going down" : "going up")")
        
        // New height is based on value of dragging plus current container height
        let newHeight = currentContainerHeight - translation.y
        
        // Handle based on gesture state
        switch gesture.state {
        case .changed:
            // This state will occur when user is dragging
            if newHeight < maximumContainerHeight {
                // Keep updating the height constraint
                containerViewHeightConstraint?.constant = newHeight
                // refresh layout
                view.layoutIfNeeded()
            }
        case .ended:
            // This happens when user stop drag,
            // so we will get the last height of container
            // Condition 1: If new height is below min, dismiss controller
            if newHeight < dismissibleHeight {
                self.animateDismissView()
            }
            else if newHeight < defaultHeight {
                // Condition 2: If new height is below default, animate back to default
                animateContainerHeight(defaultHeight)
            }
            else if newHeight < maximumContainerHeight && isDraggingDown {
                // Condition 3: If new height is below max and going down, set to default height
                animateContainerHeight(defaultHeight)
            }
            else if newHeight > defaultHeight && !isDraggingDown {
                // Condition 4: If new height is below max and going up, set to max height at top
                animateContainerHeight(maximumContainerHeight)
            }
        default:
            break
        }
        
    }
    
    func setupPanGesture() {
        // add pan gesture recognizer to the view controller's view (the whole screen)
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(self.handlePanGesture(gesture:)))
        // change to false to immediately listen on gesture movement
        panGesture.delaysTouchesBegan = false
        panGesture.delaysTouchesEnded = false
        view.addGestureRecognizer(panGesture)
    }
    
    func animateContainerHeight(_ height: CGFloat) {
        UIView.animate(withDuration: 0.4) {
            // Update container height
            self.containerViewHeightConstraint?.constant = height
            // Call this to trigger refresh constraint
            self.view.layoutIfNeeded()
        }
        // Save current height
        currentContainerHeight = height
    }
    
    func animatePresentContainer() {
        // Update bottom constraint in animation block
        UIView.animate(withDuration: 0.3) {
            self.containerViewBottomConstraint?.constant = 0
            // Call this to trigger refresh constraint
            self.view.layoutIfNeeded()
        }
    }
    
    func animateShowDimmedView() {
        dimmedView.alpha = 0
        UIView.animate(withDuration: 0.4) {
            self.dimmedView.alpha = self.maxDimmedAlpha
        }
    }
    
    func animateDismissView() {
        // hide blur view
        dimmedView.alpha = maxDimmedAlpha
        UIView.animate(withDuration: 0.4) {
            self.dimmedView.alpha = 0
        } completion: { _ in
            // once done, dismiss without animation
            self.dismiss(animated: false)
        }
        // hide main view by updating bottom constraint in animation block
        UIView.animate(withDuration: 0.3) {
            self.containerViewBottomConstraint?.constant = self.defaultHeight
            // call this to trigger refresh constraint
            self.view.layoutIfNeeded()
        }
    }
    
}

参考:

  1. How To Present a Bottom Sheet View Controller in iOS

源码下载:CustomModalVC

You might also like...
BottomSheet makes it easy to add custom bottom sheets to your SwiftUI apps.
BottomSheet makes it easy to add custom bottom sheets to your SwiftUI apps.

BottomSheet About BottomSheet BottomSheet makes it easy to add custom bottom sheets to your SwiftUI apps. The result can look like this...or completel

A Swift library to provide a bouncy action sheet
A Swift library to provide a bouncy action sheet

Hokusai is a Swift library that provides a bouncy action sheet. It will give the users a fancy experience without taking pains coding the cool animati

A Google like action sheet for iOS written in Swift.
A Google like action sheet for iOS written in Swift.

MaterialActionSheetController Lightweight and totally customizable. Create and present it the way you do with UIAlertController. Screenshots Demo | De

Dice roller, character sheet/ creator, and monster/item info app on the iphone12

DnD-LordDogMaw This file will be the start of a side project in the hopes of creating an iphone12 app for Dungeons and Dragons! This app will have 3 m

Action sheet allows including your custom views and buttons.
Action sheet allows including your custom views and buttons.

CustomizableActionSheet Action sheet allows including your custom views and buttons. Installation CocoaPods Edit your Podfile: pod 'CustomizableAction

Present a sheet ViewController easily and control ViewController height with pangesture
Present a sheet ViewController easily and control ViewController height with pangesture

PanControllerHeight is designed to present a sheet ViewController easily and control ViewController height with pangesture.

A SwiftUI Partial Sheet fully customizable with dynamic height
A SwiftUI Partial Sheet fully customizable with dynamic height

A SwiftUI Partial Sheet fully customizable with dynamic height

Share-sheet-example - A sample project that reproduces an issue with Share Sheets

Hello, DTS! This project demonstrates the issue I'm having with the Share Sheet.

An iOS library for SwiftUI to create draggable sheet experiences similar to iOS applications like Maps and Stocks.

An iOS library for SwiftUI to create draggable sheet experiences similar to iOS applications like Maps and Stocks.

Owner
null
DGBottomSheet - The lightest swift bottom sheet library

DGBottomSheet Requirements Installation Usage DGBottomSheet The lightest swift b

donggyu 9 Aug 6, 2022
SwiftUI Draggable Bottom Sheet

SwiftUI Draggable Bottom Sheet

paigeshin 2 Mar 3, 2022
Customizable Dynamic Bottom Sheet Library for iOS

DynamicBottomSheet Powerd by Witi Corp., Seoul, South Korea. Fully Customizable Dynamic Bottom Sheet Library for iOS. This library doesn't support sto

Witi Official 10 May 7, 2022
Bottom Sheet component is widely used in Joom application

Bottom Sheet Bottom Sheet component is widely used in Joom application Installation Swift Package Manager Swift Package Manager is a tool for managing

Joom 101 Dec 29, 2022
Custom-action-sheet- - Custom action sheet with swift

Custom-action-sheet- Usage let alertController: UIAlertControllerDimmed = UIAler

Girisankar G 0 Jan 19, 2022
Fully customizable and extensible action sheet controller written in Swift

XLActionController By XMARTLABS. XLActionController is an extensible library to quickly create any custom action sheet controller. Examples The action

xmartlabs 3.3k Dec 31, 2022
zekunyan 608 Dec 30, 2022
Swift UI Kit to present clean modal/alert

CleanyModal is a good way to use UI-Customised alerts with ease Features Present some kind of clean alerts (With same API as UIAlertViewController) Ad

Lory Huz 487 Dec 2, 2022
This is a small View modifier that adds detents for native .sheet representations that appeared in iOS 16

SheetDetentsModifier This is a small View modifier that adds detents for .sheet representations that appeared in iOS 16 It works starting with iOS 15

Alex Artemev 19 Oct 9, 2022
SwiftMessages is a very flexible view and view controller presentation library for iOS.

SwiftMessages Overview SwiftMessages is a very flexible view and view controller presentation library for iOS. Message views and view controllers can

SwiftKick Mobile 6.7k Jan 2, 2023