material-components-ios
material-components-ios copied to clipboard
[BottomSheet] In Extended state last cell of tableview is always half visible
For usage questions: ask on Stack Overflow.
A clear and concise description of what the bug is.
BottomSheet with tableview long enough to be scrollable in Extended State (BottomSheet opened to full screen). Last cell in TableView always covered by Home Indicator on X/XR/XS devices. Other iPhone devices show only half of cell.
Reproduction steps
To reproduce standard BottomSheetTableViewExample can be used. Add 20 cells (or more) to make table view scrollable in extended state. Open BottomSheet and scroll to the end of table. Reproducible in portrait mode only. In landscape mode there is correct bottom safe area indent.
Expected behavior
Last cell must be above home indicator and bottom safe area and fully visible.
Platform (please complete the following information)
- Device: any iPhone
- OS: iOS 12
Additional context
Internal data
- Associated internal bug: b/132164759
I've been scratching my head trying to work this out for the last day! Now need to figure out how to work around this.
UPDATE: I finally have something working (after many hours of hacking at it and looking at the view hierarchy debugger). Hopefully this can help someone else.
At a high level what I did involved.
- Creating a UIScrollView and placing this as the root element in the UIViewController that was being displayed as a BottomSheet.
- Setting the
trackingScrollView
property on theMDCBottomSheetController
to the UIScrollView mentioned above - Adding the UITableView to the UIScrollView
- (In order to place a UITableView in a UIScrollView I had to use a self sizing table view which I'll include below but you can find other examples online)
- Added
additionalSafeAreaInsets.bottom
to the height of the top safe area. The MDCBottomSheetController doesn't cover the top safe area and it seems to me that this has been done by moving the whole view down which means there is a bit of the view which overlaps the bottom of the screen meaning things get drawn down off the bottom of the screen - Added
scrollView.contentInset.bottom
to the height of the bottom safe area (I think the two above points are the key to make it work)
Code
Objective C code I used to initialise & present the view controller
MDCBottomSheetController *bottomSheet = [[MDCBottomSheetController alloc] initWithContentViewController:vc];
bottomSheet.trackingScrollView = vc.scrollView;
[source presentViewController:bottomSheet animated:true completion:nil];
My swift view controller code (I've cut out a few irrelevant bits, but hopefully it still works)
import Foundation
import MaterialComponents.MaterialButtons
import MaterialComponents.MaterialButtons_Theming
import MaterialComponents.MaterialTextControls_FilledTextFields
import MaterialComponents.MaterialTextControls_FilledTextFieldsTheming
import MaterialComponents.MaterialTypography
@objc class NewSampleSectorListVC: UIViewController {
// UI
@objc let scrollView: UIScrollView = {
let view = UIScrollView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private let sectorsListTableData = SectorsListTableDelegateAndDataSource()
lazy var sectorsListTable: UITableView = {
let table = IntrinsicTableView(frame: .zero, style: UITableView.Style.plain)
table.translatesAutoresizingMaskIntoConstraints = false
table.rowHeight = UITableView.automaticDimension
table.estimatedRowHeight = 44 // need to set this as the cell is self sizing
table.isScrollEnabled = false // the tableView doesn't need to scroll as it's in a scroll view
table.delegate = sectorsListTableData
table.dataSource = sectorsListTableData
table.register(SpacedTitleCell.self, forCellReuseIdentifier: sectorsListTableData.cellIdentifier)
table.tableFooterView = UIView(frame: .zero) // set an empty footer for the table => don't show empty rows
table.separatorColor = .clear // hide the dividers
table.bounces = false // it's unnecessary for the table to bounce when the whole bottomsheet also can be dragged
return table
}()
// MARK: - UIViewController overrides
override func viewDidLoad() {
super.viewDidLoad()
setupUIComponents()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 11.0, *) {
if let window = UIApplication.shared.keyWindow {
additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: window.safeAreaInsets.top, right: 0)
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: window.safeAreaInsets.bottom, right: 0)
}
}
}
func setupUIComponents() {
view.addSubview(scrollView)
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
if #available(iOS 11.0, *) {
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
}
scrollView.addSubview(sectorsListTable)
sectorsListTable.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
sectorsListTable.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
sectorsListTable.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
sectorsListTable.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
sectorsListTable.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
}
}
Helper class to make a self sizing table view
/** A table view with an intrinsic size so that it can be placed in a scroll view
See: https://stackoverflow.com/a/34902501/238166 for more info */
class IntrinsicTableView: UITableView {
override var contentSize: CGSize {
didSet {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}