|
import UIKit |
|
|
|
class ShadowedSwitch: UISwitch { |
|
override init(frame: CGRect) { |
|
super.init(frame: frame) |
|
configure() |
|
} |
|
|
|
required init?(coder: NSCoder) { |
|
super.init(coder: coder) |
|
configure() |
|
} |
|
|
|
func configure() { |
|
backgroundColor = .systemBackground.withAlphaComponent(0.95) |
|
layer.shadowColor = UIColor.black.cgColor |
|
layer.shadowOpacity = 0.5 |
|
layer.shadowOffset = CGSize(width: 0, height: 2) |
|
layer.shadowRadius = 4 |
|
setContentCompressionResistancePriority(.required, for: .vertical) |
|
setContentHuggingPriority(.required, for: .vertical) |
|
} |
|
|
|
override func layoutSubviews() { |
|
super.layoutSubviews() |
|
layer.cornerRadius = bounds.height / 2 |
|
} |
|
} |
|
|
|
class ViewController: UIViewController { |
|
private lazy var floatingButton: UISwitch = { |
|
let button = ShadowedSwitch() |
|
button.translatesAutoresizingMaskIntoConstraints = false |
|
|
|
button.addTarget(self, action: #selector(openSheet(_:)), for: .touchUpInside) |
|
|
|
return button |
|
}() |
|
|
|
override func viewDidLoad() { |
|
super.viewDidLoad() |
|
configureView() |
|
} |
|
|
|
private func configureView() { |
|
view.backgroundColor = .systemMint |
|
view.addSubview(floatingButton) |
|
|
|
let buttonBottomConstraint = floatingButton.bottomAnchor.constraint( |
|
equalTo: view.layoutMarginsGuide.bottomAnchor, |
|
constant: -10 |
|
) |
|
buttonBottomConstraint.priority = .defaultHigh |
|
|
|
NSLayoutConstraint.activate([ |
|
buttonBottomConstraint, |
|
floatingButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10) |
|
]) |
|
} |
|
|
|
var childSheet: UISheetPresentationController? |
|
|
|
@objc |
|
private func openSheet(_ sender: Any) { |
|
if let sheet = childSheet { |
|
let current = sheet.selectedDetentIdentifier |
|
sheet.animateChanges { |
|
sheet.selectedDetentIdentifier = current == .hidden ? .medium : .hidden |
|
} |
|
return |
|
} |
|
|
|
let childViewController = ChildViewController() |
|
|
|
if let sheet = childViewController.sheetPresentationController { |
|
sheet.detents = [.hidden(), .small(), .medium(), .large()] |
|
sheet.largestUndimmedDetentIdentifier = .medium |
|
sheet.selectedDetentIdentifier = .medium |
|
sheet.prefersGrabberVisible = true |
|
sheet.delegate = self |
|
childSheet = sheet |
|
} |
|
|
|
let constraint = childViewController.view.topAnchor.constraint(equalTo: floatingButton.bottomAnchor, constant: 10) |
|
|
|
childViewController.isModalInPresentation = true |
|
childViewController.animateAlongside = { [self] _ in |
|
constraint.isActive = true |
|
view.layoutIfNeeded() |
|
} |
|
present(childViewController, animated: true) |
|
} |
|
} |
|
|
|
extension ViewController: UISheetPresentationControllerDelegate { |
|
func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_ sheetPresentationController: UISheetPresentationController) { |
|
floatingButton.isOn = sheetPresentationController.selectedDetentIdentifier != .hidden |
|
} |
|
} |
|
|
|
extension UISheetPresentationController.Detent.Identifier { |
|
static var small: Self { |
|
Self("small") |
|
} |
|
|
|
static var hidden: Self { |
|
Self("hidden") |
|
} |
|
} |
|
|
|
extension UISheetPresentationController.Detent { |
|
static func small() -> UISheetPresentationController.Detent { |
|
.custom(identifier: .small) { $0.maximumDetentValue * 0.15 } |
|
} |
|
|
|
static func hidden() -> UISheetPresentationController.Detent { |
|
.custom(identifier: .hidden) { _ in 0 } |
|
} |
|
} |
|
|
|
class ChildViewController: UIViewController { |
|
var animateAlongside: ((any UIViewControllerTransitionCoordinatorContext) -> Void)? |
|
|
|
override func viewDidLoad() { |
|
super.viewDidLoad() |
|
view.backgroundColor = .systemBlue |
|
} |
|
|
|
override func viewWillAppear(_ animated: Bool) { |
|
super.viewWillAppear(animated) |
|
|
|
transitionCoordinator?.animate(alongsideTransition: { [self] context in |
|
animateAlongside?(context) |
|
animateAlongside = nil |
|
}) |
|
} |
|
} |
Given that I changed the button to a
UISwitch, the hardcoded corner radius is no longer prudent (and resulted in a weird artifact in the UI when the corner radius was set to 20 and the switch didn't happen to be 40pt tall). So I moved this corner rounding into a separate class, so it would adjust the rounding for the shadow dynamically.This also has the virtue of abstracting the button layout code to a separate class and out of the view controller.