Creating a Bottom Sheet Using FloatingPanel in Swift

Can Kürtür
Plus Minus One
Published in
8 min readDec 12, 2022

In this article, I will show you how to create a bottom sheet using FloatingPanel and how I fixed some problems I faced.
(If your application version is equal or higher than iOS 15, you can also use UISheetPresentationController as an option)

FloatingPanel is a framework used to make and manage custom bottom sheets. You can reach the details about the framework from the link below.
https://github.com/scenee/FloatingPanel

In PlusMinusOne, we develop, maintain, and deploy many iOS applications. One of these applications needed a bottom sheet to present different information about our new feature. We thought that it would be a good choice to use FloatingPanel, because minimum deployment target of our application is iOS 13 and FloatingPanel gives us flexibility and has the features we need.

First of all, I want to introduce the bottom sheet that we want to develop. There are three different types of bottom sheet that can be shown to users. And the transitions between these types need to be animated.

FloatingPanel takes a UIViewController as a child view controller. It also provides us the three types as “tip”, “half” and “full”.

When we scroll the bottom sheet up or down, the UIViewController inside the bottom sheet appears as the height of it. However, what we want to do is to show two different UIs for the “half” and “full” types.

We start by creating a UIViewController that we can control these three types to be used as a child view controller. The UIStackView contains “Half View” and “Full View”. In this way, we can show one of the views and hide the other views for different types. UIStackView works very efficiently for such hidden operations and can adjust its own height.

We created a method called “updateView()” inside the child view controller. This method will be triggered according to bottom sheet types and update the UI. For the “tip” type of the bottom sheet, we set the views as “hide” since we do not want them to appear, but in the “half” type, we set the hidden property of “Half View” as false and “Full View” as true. Here we can make some adjustments according to what we want to see.

To provide an animated transition between the types, we change the alpha values of the views. When the alpha value of the hidden view is 0, we set the alpha value of the displayed view to 1. We create Constant struct to keep the alpha values.
(Constant.AnimateProperties.noAlphaValue = 0)
(Constant.AnimateProperties.fullAlphaValue = 1)

func updateView(viewType: DiagnoseViewTypes) {
switch viewType {
case .tip:
self.halfView.isHidden(true)
self.fullView.isHidden(true)
case .half:
self.halfView.isHidden(false)
self.fullView.isHidden(true)
case .full:
self.halfView.isHidden(true)
self.fullView.isHidden(false)

}

UIView.animate(withDuration: Constant.AnimateProperties.duration, delay: Constant.AnimateProperties.delay, options: .curveEaseInOut) {
self.view.layoutIfNeeded()

switch viewType {
case .tip:
self.halfView.alpha(Constant.AnimateProperties.noAlphaValue)
self.fullView.alpha(Constant.AnimateProperties.noAlphaValue)
case .half:
self.halfView.alpha(Constant.AnimateProperties.fullAlphaValue)
self.fullView.alpha(Constant.AnimateProperties.noAlphaValue)
case .full:
self.halfView.alpha(Constant.AnimateProperties.noAlphaValue)
self.fullView.alpha(Constant.AnimateProperties.fullAlphaValue)
}
}
}

We also created an enum for the types provided by the FloatingPanel. We pass this enum as a parameter to the “updateView()” function. Then we are done with the child view controller.

enum DiagnoseViewTypes {
case tip
case half
case full
}

The FloatingPanel itself is actually a UIViewController. We need to create the “FloatingPanelController” object within the main view controller where we want to show the bottom sheet. We can set the delegate and content view over this object we have created. We can use “set(contentViewController: )” method of the fpc passing the child view controller as a parameter in it and we will be able to make changes according to the movements of the bottom sheet.

We did not want to keep the configurations of the FloatingPanel in the main view controller. For this reason we create a UIViewController named as “BaseBottomSheetController” and inherit it into our main view controller. In this way, if we want to show the bottom sheet in another UIViewController in the future, all we have to do is inherit this “BaseBottomSheetController”.

Let’s create the BaseBottomSheetController.
We created our “BaseBottomSheetController” class and imported the FloatingPanel. We inherited “BaseViewController” that we used into this class. (We did this because we used the “BaseViewController” in our project, but if you are not using it, you can ignore it.)

import FloatingPanel

class BaseBottomSheetController: BaseViewController {
private var fpc = FloatingPanelController()
}

We can access the delegate property of the “FloatingPanelController” through the “fpc” object we have created. Thanks to this delegate, we can use FloatingPanel’s methods such as “didMove()” and “shouldBegin()”.

import FloatingPanel

class BaseBottomSheetController: BaseViewController {
private var fpc = FloatingPanelController()

private var floatingPanelDelegate: FloatingPanelControllerDelegate? {
didSet {
fpc.delegate = floatingPanelDelegate
}
}
}

We create a private method to pass the content view to “fpc”. We will also set the bottom sheet configurations within this method.

We set the corner radius of the bottom sheet and set the content mode as “.fitToBounds”. This content mode is used to scale the content to fit the bounds of the root view by changing the bottom sheet position. In this way, our “Start Diagnosing” button is visible and clickable even in different types.

Here we use the “set()” method taking the “contentVC” parameter and “addPanel()” method to add the “fpc” to panel.

private func prepareDiagnoseBottomSheet(contentVC: UIViewController?) {
fpc.surfaceView.appearance.cornerRadius = Constant.cornerRadius
fpc.contentMode = .fitToBounds
fpc.set(contentViewController: contentVC)
fpc.addPanel(toParent: self)
}

Finally, we create a “setupBottomSheet()” method which takes the child view controller and delegate as a parameter. The final version of the class is as below.

class BaseBottomSheetController: BaseViewController {
private var fpc = FloatingPanelController()

private weak var contentVC: UIViewController? {
didSet {
guard let contentVC = contentVC else { return }

prepareDiagnoseBottomSheet(contentVC: contentVC)
}
}

private var floatingPanelDelegate: FloatingPanelControllerDelegate? {
didSet {
fpc.delegate = floatingPanelDelegate
}
}

func setupBottomSheet(contentVC: UIViewController?, floatingPanelDelegate: FloatingPanelControllerDelegate) {
self.floatingPanelDelegate = floatingPanelDelegate
prepareDiagnoseBottomSheet(contentVC: contentVC)
}

private func prepareDiagnoseBottomSheet(contentVC: UIViewController?) {
fpc.surfaceView.appearance.cornerRadius = Constant.cornerRadius
fpc.contentMode = .fitToBounds
fpc.set(contentViewController: contentVC)
fpc.addPanel(toParent: self)
}
}

Next, we have to create a class where we configure the behavior and layout of the bottom sheet. This class must conform the “FloatingPanelControllerDelegate” protocol. In this way, we will be able to access the delegate methods of the FloatingPanel.

We will use the child view controller that we created before in this class. That is why we create an “init()” method that takes “DiagnoseBottomSheet”, which is our child view controller, as a parameter.

import Foundation
import FloatingPanel

final class DiagnoseBottomSheetDelegateController: FloatingPanelControllerDelegate {
weak var contentVC: DiagnoseBottomSheet?

init(vc: DiagnoseBottomSheet) {
self.contentVC = vc
}
}

In order to configure the layout of the bottom sheet, we need a class that conforms the “FloatingPanelLayout” protocol of the FloatingPanel. In this class, we can set the initial state of the bottom sheet, anchors and the backdrop alpha values.

class FloatingPanelStocksLayout: FloatingPanelLayout {
let position: FloatingPanelPosition = .bottom
let initialState: FloatingPanelState = .tip

let anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] = [
.full: FloatingPanelLayoutAnchor(absoluteInset: FloatingConstant.full.inset, edge: .bottom, referenceGuide: .safeArea),
.half: FloatingPanelLayoutAnchor(absoluteInset: FloatingConstant.half.inset, edge: .bottom, referenceGuide: .safeArea),
.tip: FloatingPanelLayoutAnchor(absoluteInset: FloatingConstant.tip.inset, edge: .bottom, referenceGuide: .safeArea)
]

func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
switch state {
case .full:
return Constant.halfAlphaValue
default:
return Constant.noAlphaValue
}
}
}

After creating our “FloatingPanelStocksLayout” class, we call the delegate method of the FloatingPanel inside the “DiagnoseBottomSheetDelegateController” and return the layout class we created. In this way, we were able to apply custom layouts on the bottom sheet.

import Foundation
import FloatingPanel

final class DiagnoseBottomSheetDelegateController: FloatingPanelControllerDelegate {
weak var contentVC: DiagnoseBottomSheet?

init(vc: DiagnoseBottomSheet) {
self.contentVC = vc
}

func floatingPanel(_ fpc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
return FloatingPanelStocksLayout()
}
}

We call the “updateView()” method inside the “floatingPanelDidChangeState()” method. In this way, the child view controller knows which state we are in and adjusts itself accordingly. The final state of our class is as follows:

import Foundation
import FloatingPanel

final class DiagnoseBottomSheetDelegateController: FloatingPanelControllerDelegate {
weak var contentVC: DiagnoseBottomSheet?

init(vc: DiagnoseBottomSheet) {
self.contentVC = vc
}

func floatingPanel(_ fpc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
return FloatingPanelStocksLayout()
}

func floatingPanelDidChangeState(_ vc: FloatingPanelController) {
switch vc.state {
case .tip:
contentVC?.updateView(viewType: .tip)
case .half:
contentVC?.updateView(viewType: .half)
case .full:
contentVC?.updateView(viewType: .full)
default:
return
}
}
}

Finally, we only have to make adjustments in the UIViewController where we want to show the bottom sheet. Let’s do that too.

We create a function called “prepareDiagnoseBottomSheet()”. Then, we call the “setupBottomSheet()” method inside it passing the parameters there. Our bottom sheet is ready to use!

final class DiagnoseViewController: BaseBottomSheetController {

override func viewDidLoad() {
super.viewDidLoad()
prepareDiagnoseBottomSheet()
}

private func prepareDiagnoseBottomSheet() {
var diagnoseBottomContent = DiagnoseBottomSheet()
var diagnoseBottomSheetDelegateController = DiagnoseBottomSheetDelegateController(vc: diagnoseBottomContent)
setupBottomSheet(contentVC: diagnoseBottomContent, floatingPanelDelegate: diagnoseBottomSheetDelegateController)
}
}

The Issues I Faced

1- UI problems due to animation

When switching between states, UI problems occurred. I realized it was because of the animation.

I noticed that I set the hidden properties of the views in the child view controller inside the “animate()” function of UIView as follows:

func updateView(viewType: DiagnoseViewType) {    
UIView.animate(withDuration: Constant.AnimateProperties.duration, delay: Constant.AnimateProperties.delay, options: .curveEaseInOut) {
self.view.layoutIfNeeded()

switch viewType {
case .tip:
self.halfView.isHidden(true)
self.fullView.isHidden(true)
self.halfView.alpha(Constant.AnimateProperties.noAlphaValue)
self.fullView.alpha(Constant.AnimateProperties.noAlphaValue)
case .half:
self.halfView.isHidden(false)
self.fullView.isHidden(true)
self.halfView.alpha(Constant.AnimateProperties.fullAlphaValue)
self.fullView.alpha(Constant.AnimateProperties.noAlphaValue)
case .full:
self.halfView.isHidden(true)
self.fullView.isHidden(false)
self.halfView.alpha(Constant.AnimateProperties.noAlphaValue)
self.fullView.alpha(Constant.AnimateProperties.fullAlphaValue)
}
}
}

We should do the animating with the alpha values. In this example, when we remove the hidden properties of the views from the “animate()” method and set them outside on a separate switch case, the UI problem will disappear. I also put the wrong version above as an example. I showed the correct version that works without any problems in beginning of the article at “Creating the Child View Controller” step. You can go up and look again.

2- Not Recognizing Movements When Scrolling Bottom Sheet

I could not scroll bottom sheet touching the red area in the picture below. I could only scroll it using the area in the green part.

There are multiple views in the bottom sheet. If we do not need any interaction, we need to disable the user interactions of the views. After I had disabled the user interaction of all views in the bottom sheet, this problem was gone. You can disable the user interaction of the views programmatically or using a storyboard as follows:

Programatically:

view.isUserInteractionEnabled = false

Using Storyboard:

That is all I wanted to explain in this article regarding creating a bottom sheet using a FloatingPanel. I hope you could find something beneficial here which could help you solving the problems you are facing and be motivated to use it in your own projects.

To learn more about our development experiences you can look at our medium page. At Plus Minus One, we love learning and sharing our experiences. We hope this article makes your development much easier.

Thank you for reading!

--

--