Skip to content

Commit 89b91d6

Browse files
author
Mark Pospesel
authored
[Issue-21] Expose minimumTopOffset (#24)
* mark init(coder:) unavailable * Better testing of typography and appearances * Add minimum top offset to appearance * Apply new sizing behavior and refactor to reduce file size * Rename memory leak method * Remove stray comment
1 parent 7aff3f6 commit 89b91d6

15 files changed

+304
-185
lines changed

Sources/YBottomSheet/BottomSheetController+Appearance.swift

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ extension BottomSheetController {
2828
public var presentAnimationCurve: UIView.AnimationOptions
2929
/// Animation type during dismissing. Default is `curveEaseOut`.
3030
public var dismissAnimationCurve: UIView.AnimationOptions
31+
/// Minimum top offset of sheet from safe area top. Default is `44`.
32+
///
33+
/// The top of the sheet will not move beyond this gap from the top of the safe area.
34+
public var minimumTopOffset: CGFloat
3135
/// (Optional) Minimum content view height. Default is `nil`.
3236
///
3337
/// Only applicable for resizable sheets. `nil` means to use the content view's intrinsic height as the minimum.
@@ -44,16 +48,17 @@ extension BottomSheetController {
4448

4549
/// Initializes an `Appearance`.
4650
/// - Parameters:
47-
/// - indicatorAppearance: Appearance of the drag indicator or pass `nil` to hide.
48-
/// - headerAppearance: Appearance of the sheet header view or pass `nil` to hide.
49-
/// - layout: Bottom sheet layout properties such as corner radius.
50-
/// - elevation: Bottom sheet's shadow or pass `nil` to hide
51-
/// - dimmerColor: Dimmer view color or pass `nil` to hide.
52-
/// - animationDuration: Animation duration for bottom sheet. Default is `0.3`.
53-
/// - presentAnimationCurve: Animaiton during presenting.
54-
/// - dismissAnimationCurve: Animation during dismiss.
55-
/// - minimumContentHeight: Optional) Minimum content view height.
56-
/// - isDismissAllowed: Whether the sheet can be dismissed by swiping down or tapping on the dimmer.
51+
/// - indicatorAppearance: appearance of the drag indicator or pass `nil` to hide.
52+
/// - headerAppearance: appearance of the sheet header view or pass `nil` to hide.
53+
/// - layout: bottom sheet layout properties such as corner radius.
54+
/// - elevation: bottom sheet's shadow or pass `nil` to hide
55+
/// - dimmerColor: dimmer view color or pass `nil` to hide.
56+
/// - animationDuration: animation duration for bottom sheet. Default is `0.3`.
57+
/// - presentAnimationCurve: animation type during presenting.
58+
/// - dismissAnimationCurve: animation type during dismiss.
59+
/// - minimumTopOffset: minimum top offset. Default is `44`
60+
/// - minimumContentHeight: (optional) minimum content view height.
61+
/// - isDismissAllowed: whether the sheet can be dismissed by swiping down or tapping on the dimmer.
5762
public init(
5863
indicatorAppearance: DragIndicatorView.Appearance? = nil,
5964
headerAppearance: SheetHeaderView.Appearance? = .default,
@@ -63,6 +68,7 @@ extension BottomSheetController {
6368
animationDuration: TimeInterval = 0.3,
6469
presentAnimationCurve: UIView.AnimationOptions = .curveEaseIn,
6570
dismissAnimationCurve: UIView.AnimationOptions = .curveEaseOut,
71+
minimumTopOffset: CGFloat = 44,
6672
minimumContentHeight: CGFloat? = nil,
6773
isDismissAllowed: Bool = true
6874
) {
@@ -74,6 +80,7 @@ extension BottomSheetController {
7480
self.animationDuration = animationDuration
7581
self.presentAnimationCurve = presentAnimationCurve
7682
self.dismissAnimationCurve = dismissAnimationCurve
83+
self.minimumTopOffset = minimumTopOffset
7784
self.minimumContentHeight = minimumContentHeight
7885
self.isDismissAllowed = isDismissAllowed
7986
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//
2+
// BottomSheetController+build.swift
3+
// YBottomSheet
4+
//
5+
// Created by Mark Pospesel on 4/26/23.
6+
// Copyright © 2023 Y Media Labs. All rights reserved.
7+
//
8+
9+
import UIKit
10+
11+
internal extension BottomSheetController {
12+
func build() {
13+
switch content {
14+
case .view(title: let title, view: let childView):
15+
build(childView, title: title)
16+
case .controller(let childController):
17+
build(childController)
18+
}
19+
}
20+
}
21+
22+
private extension BottomSheetController {
23+
func build(_ subview: UIView, title: String) {
24+
contentView.addSubview(subview)
25+
subview.constrainEdges()
26+
27+
if let backgroundColor = subview.backgroundColor,
28+
backgroundColor.rgbaComponents.alpha == 1 {
29+
// use the subview's background color for the sheet
30+
sheetView.backgroundColor = backgroundColor
31+
// but we have to set the subview's background to nil or else
32+
// it will overflow the sheet and not be cropped by the corner radius.
33+
subview.backgroundColor = nil
34+
}
35+
36+
indicatorView = DragIndicatorView(appearance: appearance.indicatorAppearance ?? .default)
37+
indicatorContainer.addSubview(indicatorView)
38+
39+
headerView = SheetHeaderView(title: title, appearance: appearance.headerAppearance ?? .default)
40+
headerView.delegate = self
41+
buildSheet()
42+
}
43+
44+
func build(_ childController: UIViewController) {
45+
addChild(childController)
46+
build(childController.view, title: childController.title ?? "")
47+
childController.didMove(toParent: self)
48+
}
49+
50+
func buildSheet() {
51+
buildViews()
52+
buildConstraints()
53+
updateViewAppearance()
54+
addGestures()
55+
}
56+
57+
func buildViews() {
58+
view.addSubview(dimmerView)
59+
view.addSubview(dimmerTapView)
60+
view.addSubview(sheetView)
61+
sheetView.addSubview(stackView)
62+
stackView.addArrangedSubview(indicatorContainer)
63+
stackView.addArrangedSubview(headerView)
64+
stackView.addArrangedSubview(contentView)
65+
}
66+
67+
func buildConstraints() {
68+
dimmerView.constrainEdges()
69+
dimmerTapView.constrainEdges(.notBottom)
70+
dimmerTapView.constrain(.bottomAnchor, to: sheetView.topAnchor)
71+
72+
sheetView.constrainEdges(.notTop)
73+
minimumTopOffsetAnchor = sheetView.constrain(
74+
.topAnchor,
75+
to: view.safeAreaLayoutGuide.topAnchor,
76+
relatedBy: .greaterThanOrEqual
77+
)
78+
79+
indicatorView.constrain(.bottomAnchor, to: indicatorContainer.bottomAnchor)
80+
indicatorTopAnchor = indicatorView.constrain(
81+
.topAnchor,
82+
to: indicatorContainer.topAnchor
83+
)
84+
indicatorView.constrainCenter(.x)
85+
86+
stackView.constrain(.topAnchor, to: sheetView.topAnchor)
87+
stackView.constrainEdges(.horizontal, to: view.safeAreaLayoutGuide)
88+
stackView.constrainEdges(.bottom, to: view.safeAreaLayoutGuide, relatedBy: .greaterThanOrEqual)
89+
stackView.constrainEdges(.bottom, to: view.safeAreaLayoutGuide, priority: Priorities.sheetContentHugging)
90+
91+
contentView.constrainEdges(.horizontal)
92+
headerView.constrainEdges(.horizontal)
93+
}
94+
}

Sources/YBottomSheet/BottomSheetController.swift

Lines changed: 38 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,24 @@ import UIKit
1010

1111
/// A view controller that manages a bottom sheet.
1212
public class BottomSheetController: UIViewController {
13-
// Holds the sheet content until the view is loaded
14-
private let content: Content
13+
internal let content: Content
1514
private var shadowSize: CGSize = .zero
16-
private let minimumTopOffset: CGFloat = 44
17-
private let minimumContentHeight: CGFloat = 88
15+
internal var minimumTopOffsetAnchor: NSLayoutConstraint?
1816
private var topAnchor: NSLayoutConstraint?
19-
private var indicatorTopAnchor: NSLayoutConstraint?
17+
internal var indicatorTopAnchor: NSLayoutConstraint?
2018
private var childHeightAnchor: NSLayoutConstraint?
2119
private var panGesture: UIPanGestureRecognizer?
2220
internal lazy var lastYOffset: CGFloat = { sheetView.frame.origin.y }()
2321

22+
/// Absolute minimum height of sheet content
23+
///
24+
/// Only used when `appearance.minimumContentHeight == nil`.
25+
public var minimumContentHeight: CGFloat = 88 {
26+
didSet {
27+
updateChildView()
28+
}
29+
}
30+
2431
/// Minimum downward velocity beyond which we interpret a pan gesture as a downward swipe.
2532
public var dismissThresholdVelocity: CGFloat = 1000
2633

@@ -42,11 +49,11 @@ public class BottomSheetController: UIViewController {
4249
return view
4350
}()
4451
/// Bottom sheet drag indicator view.
45-
public private(set) var indicatorView: DragIndicatorView!
52+
public internal(set) var indicatorView: DragIndicatorView!
4653
/// Container view for the drag indicator.
4754
internal let indicatorContainer = UIView()
4855
/// Bottom sheet header view.
49-
public private(set) var headerView: SheetHeaderView!
56+
public internal(set) var headerView: SheetHeaderView!
5057
/// Holds the sheet's child content (view or view controller).
5158
let contentView: UIView = {
5259
let view = UIView()
@@ -55,7 +62,7 @@ public class BottomSheetController: UIViewController {
5562
}()
5663

5764
/// Comprises the indicator view, the header view, and the content view.
58-
private let stackView: UIStackView = {
65+
let stackView: UIStackView = {
5966
let stackView = UIStackView()
6067
stackView.axis = .vertical
6168
stackView.alignment = .center
@@ -119,8 +126,11 @@ public class BottomSheetController: UIViewController {
119126
}
120127

121128
/// :nodoc:
122-
internal required init?(coder: NSCoder) { nil }
123-
129+
@available(*, unavailable)
130+
required public init?(coder: NSCoder) {
131+
fatalError("init(coder:) is not available for BottomSheetController")
132+
}
133+
124134
/// :nodoc:
125135
public override func viewDidLoad() {
126136
super.viewDidLoad()
@@ -159,105 +169,34 @@ public class BottomSheetController: UIViewController {
159169
}
160170
}
161171

162-
private extension BottomSheetController {
163-
func commonInit() {
164-
modalPresentationStyle = .custom
165-
transitioningDelegate = self
166-
}
167-
168-
func build() {
169-
switch content {
170-
case .view(title: let title, view: let childView):
171-
build(childView, title: title)
172-
case .controller(let childController):
173-
build(childController)
174-
}
175-
}
176-
177-
func build(_ subview: UIView, title: String) {
178-
contentView.addSubview(subview)
179-
subview.constrainEdges()
180-
181-
if let backgroundColor = subview.backgroundColor,
182-
backgroundColor.rgbaComponents.alpha == 1 {
183-
// use the subview's background color for the sheet
184-
sheetView.backgroundColor = backgroundColor
185-
// but we have to set the subview's background to nil or else
186-
// it will overflow the sheet and not be cropped by the corner radius.
187-
subview.backgroundColor = nil
188-
}
189-
190-
indicatorView = DragIndicatorView(appearance: appearance.indicatorAppearance ?? .default)
191-
indicatorContainer.addSubview(indicatorView)
192-
193-
headerView = SheetHeaderView(title: title, appearance: appearance.headerAppearance ?? .default)
194-
headerView.delegate = self
195-
buildSheet()
196-
}
197-
198-
func build(_ childController: UIViewController) {
199-
addChild(childController)
200-
build(childController.view, title: childController.title ?? "")
201-
childController.didMove(toParent: self)
202-
}
203-
204-
func buildSheet() {
205-
buildViews()
206-
buildConstraints()
207-
updateViewAppearance()
208-
addGestures()
209-
}
210-
211-
func buildViews() {
212-
view.addSubview(dimmerView)
213-
view.addSubview(dimmerTapView)
214-
view.addSubview(sheetView)
215-
sheetView.addSubview(stackView)
216-
stackView.addArrangedSubview(indicatorContainer)
217-
stackView.addArrangedSubview(headerView)
218-
stackView.addArrangedSubview(contentView)
219-
}
220-
221-
func buildConstraints() {
222-
dimmerView.constrainEdges()
223-
dimmerTapView.constrainEdges(.notBottom)
224-
dimmerTapView.constrain(.bottomAnchor, to: sheetView.topAnchor)
225-
226-
sheetView.constrainEdges(.notTop)
227-
sheetView.constrain(
228-
.topAnchor,
229-
to: view.safeAreaLayoutGuide.topAnchor,
230-
relatedBy: .greaterThanOrEqual,
231-
constant: minimumTopOffset
232-
)
233-
234-
indicatorView.constrain(.bottomAnchor, to: indicatorContainer.bottomAnchor)
235-
indicatorTopAnchor = indicatorView.constrain(
236-
.topAnchor,
237-
to: indicatorContainer.topAnchor
238-
)
239-
indicatorView.constrainCenter(.x)
240-
241-
stackView.constrain(.topAnchor, to: sheetView.topAnchor)
242-
stackView.constrainEdges(.horizontal, to: view.safeAreaLayoutGuide)
243-
stackView.constrainEdges(.bottom, to: view.safeAreaLayoutGuide, relatedBy: .greaterThanOrEqual)
244-
stackView.constrainEdges(.bottom, to: view.safeAreaLayoutGuide, priority: Priorities.sheetContentHugging)
245-
246-
contentView.constrainEdges(.horizontal)
247-
headerView.constrainEdges(.horizontal)
248-
}
249-
172+
internal extension BottomSheetController {
250173
func updateViewAppearance() {
251174
dimmerTapView.isAccessibilityElement = appearance.isDismissAllowed
252175
sheetView.layer.cornerRadius = appearance.layout.cornerRadius
176+
minimumTopOffsetAnchor?.constant = appearance.minimumTopOffset
253177
updateShadow()
254178
dimmerView.backgroundColor = appearance.dimmerColor
255179
updateIndicatorView()
256180
updateHeaderView()
257181
updateChildView()
258182
view.layoutIfNeeded()
259183
}
260-
184+
185+
func addGestures() {
186+
let swipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(onSwipeDown))
187+
swipeGesture.direction = .down
188+
view.addGestureRecognizer(swipeGesture)
189+
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(onDimmerTap))
190+
dimmerTapView.addGestureRecognizer(tapGesture)
191+
}
192+
}
193+
194+
private extension BottomSheetController {
195+
func commonInit() {
196+
modalPresentationStyle = .custom
197+
transitioningDelegate = self
198+
}
199+
261200
func updateIndicatorView() {
262201
indicatorContainer.isHidden = !isResizable
263202
if let indicatorAppearance = appearance.indicatorAppearance {
@@ -313,14 +252,6 @@ private extension BottomSheetController {
313252
func updateShadow() {
314253
appearance.elevation?.apply(layer: sheetView.layer)
315254
}
316-
317-
func addGestures() {
318-
let swipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(onSwipeDown))
319-
swipeGesture.direction = .down
320-
view.addGestureRecognizer(swipeGesture)
321-
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(onDimmerTap))
322-
dimmerTapView.addGestureRecognizer(tapGesture)
323-
}
324255

325256
func onDismiss() {
326257
dismiss(animated: true)

Sources/YBottomSheet/DragIndicatorView/DragIndicatorView.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ open class DragIndicatorView: UIView {
3434
}
3535

3636
/// :nodoc:
37-
required public init?(coder: NSCoder) { nil }
37+
@available(*, unavailable)
38+
required public init?(coder: NSCoder) {
39+
fatalError("init(coder:) is not available for DragIndicatorView")
40+
}
3841
}
3942

4043
private extension DragIndicatorView {

Sources/YBottomSheet/SheetHeaderView/SheetHeaderView.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,11 @@ open class SheetHeaderView: UIView {
4848
}
4949

5050
/// :nodoc:
51-
required public init?(coder: NSCoder) { nil }
52-
51+
@available(*, unavailable)
52+
required public init?(coder: NSCoder) {
53+
fatalError("init(coder:) is not available for SheetHeaderView")
54+
}
55+
5356
@objc private func closeButtonAction() {
5457
delegate?.didTapCloseButton()
5558
}

Tests/YBottomSheetTests/Animation/BottomSheetAnimatorTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ private extension BottomSheetAnimatorTests {
4646
line: UInt = #line
4747
) -> BottomSheetAnimator {
4848
let sut = BottomSheetAnimator(sheetViewController: sheetViewController)
49-
trackForMemoryLeaks(sut, file: file, line: line)
49+
trackForMemoryLeak(sut, file: file, line: line)
5050
return sut
5151
}
5252
}

0 commit comments

Comments
 (0)