Swift Bottom Sheet Implementation in |UIKit & SwiftUI

Jerry PM
5 min readMar 6, 2024

Building Bottom Sheets: A Deep Dive into Swift vs. SwiftUI Code

Swift and SwiftUI have revolutionized the way developers create user interfaces for iOS applications. With the advent of SwiftUI, building dynamic and interactive UI components has become more accessible and efficient. One such versatile UI element is the Bottom Sheet, a popular design pattern that enhances user experience by presenting contextual information or actions from the bottom of the screen. In this article, we’ll delve into the implementation of Bottom Sheets in both Swift and SwiftUI, exploring the techniques and best practices associated with each.

Understanding Bottom Sheets:

A Bottom Sheet is a UI component that slides up from the bottom of the screen, partially covering the content, to reveal additional information or functionality. It is commonly used to display supplementary details, actions, or options relevant to the current context without obstructing the entire screen.

Implementation in Swift:

Implementing a Bottom Sheet in Swift involves creating a custom view controller or utilizing third-party libraries. Developers can use techniques such as animations and gestures to achieve a smooth and intuitive user experience. We’ll explore the step-by-step process of creating a basic Bottom Sheet in Swift, discussing key components like UIViewControllers, UIPanGestureRecognizer, and UIViewPropertyAnimator.

import UIKit

protocol CustomBottomSheetDelegate: AnyObject {
func dismiss()
func didSelectItem(item: String)
}

class CustomBottomSheetView: UIView {
private let sheetHeight: CGFloat = 400
private let animationDuration: TimeInterval = 0.3
private var isSheetVisible = false

weak var delegate: CustomBottomSheetDelegate?
var dataKeys: [String]?
var dataItems: [String]?

lazy var sheetContentView: UIView = {
let view = UIView()
view.backgroundColor = .clear

let viewTop = UIView()
viewTop.backgroundColor = .white
view.addSubview(viewTop)

let viewCenterLine = UIView()
viewCenterLine.backgroundColor = .lightGray.withAlphaComponent(0.5)
viewTop.addSubview(viewCenterLine)

let tableView = UITableView(frame: bounds, style: .plain)
tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
tableView.contentInset = UIEdgeInsets(top: 16, left: 0, bottom: 44, right: 0)
tableView.delegate = self
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
view.addSubview(tableView)

// Set up constraints for viewTop
viewTop.translatesAutoresizingMaskIntoConstraints = false
viewTop.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
viewTop.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
viewTop.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
viewTop.heightAnchor.constraint(equalToConstant: 44).isActive = true

let cornerRadius: CGFloat = 10.0
viewTop.layer.cornerRadius = cornerRadius
viewTop.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]

// Set up constraints for viewCenterLine within viewTop
viewCenterLine.translatesAutoresizingMaskIntoConstraints = false
viewCenterLine.centerXAnchor.constraint(equalTo: viewTop.centerXAnchor).isActive = true
viewCenterLine.centerYAnchor.constraint(equalTo: viewTop.centerYAnchor).isActive = true
viewCenterLine.widthAnchor.constraint(equalTo: viewTop.widthAnchor, multiplier: 1 / 4).isActive = true
viewCenterLine.heightAnchor.constraint(equalToConstant: 4).isActive = true

// Set up constraints for Table View
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: viewTop.bottomAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true

return view
}()

override init(frame: CGRect) {
super.init(frame: frame)
setupSheetView()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
setupSheetView()
}

func setupSheetView() {
addSubview(sheetContentView)
sheetContentView.translatesAutoresizingMaskIntoConstraints = false
sheetContentView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
sheetContentView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
sheetContentView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: sheetHeight).isActive = true
sheetContentView.heightAnchor.constraint(equalToConstant: sheetHeight).isActive = true

let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(hideBottomSheet))
tapGestureRecognizer.cancelsTouchesInView = false
addGestureRecognizer(tapGestureRecognizer)
}

func showBottomSheet() {
UIView.animate(withDuration: animationDuration) {
self.sheetContentView.transform = CGAffineTransform(translationX: 0, y: -self.sheetHeight)
}
isSheetVisible = true
}

@objc
func hideBottomSheet(_ sender: UITapGestureRecognizer) {
let tapLocation = sender.location(in: sheetContentView)
if !sheetContentView.bounds.contains(tapLocation) {
hideAction()
}
}

private func hideAction() {
delegate?.dismiss()

UIView.animate(withDuration: animationDuration) {
self.sheetContentView.transform = .identity
}
isSheetVisible = false
}
}

extension CustomBottomSheetView: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataKeys?.count ?? .zero
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = "\(dataKeys?[indexPath.row] ?? .empty) - \(dataItems?[indexPath.row] ?? .empty)"
return cell
}
}

extension CustomBottomSheetView: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
DispatchQueue.main.async {
if let value = self.dataKeys?[indexPath.row] {
self.delegate?.didSelectItem(item: value)
}
self.hideAction()
}
}
}

And this is how you use the full code for the UIKit Bottom Sheet. for example, set viewController

class viewController: UIViewController {
let customBottomSheetView = CustomBottomSheetView()

override func viewDidLoad() {
super.viewDidLoad()
customBottomSheetView.delegate = self

if let symbolKeys = data?.symbols {
customBottomSheetView.dataKeys = symbolKeys.keys.map { String($0) }
customBottomSheetView.dataItems = symbolKeys.values.map { String($0) }
}

DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.view.addSubview(customBottomSheetView)
customBottomSheetView.translatesAutoresizingMaskIntoConstraints = false
customBottomSheetView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
customBottomSheetView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
customBottomSheetView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
customBottomSheetView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true

customBottomSheetView.showBottomSheet()
}
}
}

extension CurrenciesViewController: CustomBottomSheetDelegate {
func dismiss() {
view.backgroundColor = UIColor.clear
dismiss(animated: true)
}

func didSelectItem(item: String) {
delegate?.didSelectCurrency(key: item)
}
}
Swift — UIKit

Implementation in SwiftUI:

SwiftUI introduces a declarative and concise way of building user interfaces. Implementing a Bottom Sheet in SwiftUI leverages the framework’s unique syntax and features. We’ll demonstrate how to integrate a Bottom Sheet seamlessly into a SwiftUI project, taking advantage of SwiftUI’s built-in animations and layout system. This section will cover concepts like @State, @GestureState, and the integration of SwiftUI with UIKit components.

// Extension View
extension View {

// MARK: For bottom Sheet

func halfSheet<SheetView: View>(showSheet: Binding<Bool>, @ViewBuilder sheeView: @escaping () -> SheetView) -> some View {
return background {
HalfSheetHelper(sheetView: sheeView(), showSheet: showSheet)
}
}
}

// HalfSheetHelper
struct HalfSheetHelper<SheetView: View>: UIViewControllerRepresentable {
var sheetView: SheetView
@Binding var showSheet: Bool
let controller = UIViewController()

func makeUIViewController(context: Context) -> UIViewController {
controller.view.backgroundColor = .clear
return controller
}

func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
if showSheet {
// presenting Modal View....
let sheetController = CustomHostingController(rootView: sheetView)
uiViewController.present(sheetController, animated: true) {
DispatchQueue.main.async {
self.showSheet.toggle()
}
}
}
}
}

// CustomHostingController
class CustomHostingController<Content: View>: UIHostingController<Content> {
override func viewDidLoad() {
// setting presentation controller properties...
if let presentationController = presentationController as?
UISheetPresentationController
{
// Note: this large for full screen and medium for half bottom sheet
presentationController.detents = [
.medium()
// .large()
]

// To show grab portion...
presentationController.prefersGrabberVisible = true
}
}
}
SwiftUI

Conclusion:

In this article, we explored the implementation of Bottom Sheets in both Swift and SwiftUI, uncovering the unique approaches and considerations associated with each. Whether you’re developing a traditional Swift application or embracing the declarative power of SwiftUI, integrating a Bottom Sheet can significantly enhance the user interface and overall user experience of your iOS app. Armed with the knowledge gained from this exploration, developers can confidently implement Bottom Sheets tailored to their project’s needs.

--

--