개인 프로젝트를 제작하면서 Bottom Sheet를 제작해야 하는 상황이 생겼어요.
Log창의 컬러를 설정하는 기능을 Log창이 변화되는 모습을 같이 볼 수 있도록 제공하고 싶었기에 이번 기회로 Bottom Sheet를 도입하게 되었어요
저는 Bottom Sheet를 원하는 느낌으로 커스터마이징을 하고 싶었기에 라이브러리 없이 구현했어요!
저와 같은 분들을 위해서 코드를 공유하고자 작성합니다!
해당 BottomSheetViewController
는 [iOS] Auto Layout을 이용하여 페이스북의 Bottom Sheet을 만들어보자! - Part 1 블로그 글을 참고하였습니다!
Dark Mode | Light Mode |
---|---|
개인 앱에서 적용완료된 모습입니다 이미지를 클릭하시면 영상으로 표시됩니다.
뒷부분의 화면과 BottomSheet 모두 표시되어 원하는 기능을 구현할 수 있었어요!
Github에서 데모 프로젝트를 받아보실 수 있어요!
iOS BottomSheet Demo
BottomSheetViewController
를 init으로 생성 후 present
를 통해 표시합니다.
contentViewController
: 표시하고자 하는 viewControllerdefaultHeight
: Bottom Sheet의 기본 높이값cornerRadius
: 상단 좌, 우 Radius 값isPannedable
: 높이조절 활성화 여부let viewController = LogSettingVC()
let height: CGFloat = 500
let bottomSheetViewController = BottomSheetViewController(contentViewController: viewController,
defaultHeight: height,
cornerRadius: 25,
isPannedable: true)
self.present(bottomSheetViewController, animated: false, completion: nil)
구현 완료된 BottomSheetViewController
전체 코드입니다.
import UIKit
// MARK: https://velog.io/@minsang/iOS-Bottom-Sheet
final class BottomSheetViewController: UIViewController {
enum BottomSheetViewState {
case expanded
case normal
}
private lazy var dimmedView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.darkGray.withAlphaComponent(self.dimmedAlpha)
view.alpha = 0
return view
}()
private lazy var bottomSheetView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = self.cornerRedius
view.layer.cornerCurve = .continuous
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
view.clipsToBounds = true
return view
}()
private let dragIndicatorView: UIView = {
let view = UIView()
view.backgroundColor = .label
view.layer.cornerRadius = 1.5
view.alpha = 0
return view
}()
private var bottomSheetViewTopConstraint: NSLayoutConstraint!
// 열린 BottomSheet의 기본 높이를 지정하기 위한 프로퍼티
var defaultHeight: CGFloat = 500
// bottomSheetView의 상단 CornerRadius 값
var cornerRedius: CGFloat = 16
// dimmedView의 alpha값
var dimmedAlpha: CGFloat = 0.4
// Bottom Sheet과 safe Area Top 사이의 최소값을 지정하기 위한 프로퍼티
var bottomSheetPanMinTopConstant: CGFloat = 40
// pannedGesture 활성화 여부
var isPannedable: Bool = false
// 드래그 하기 전에 Bottom Sheet의 top Constraint value를 저장하기 위한 프로퍼티
private lazy var bottomSheetPanStartingTopConstant: CGFloat = bottomSheetPanMinTopConstant
private let contentViewController: UIViewController
init(contentViewController: UIViewController, defaultHeight: CGFloat, cornerRadius: CGFloat = 16, dimmedAlpha: CGFloat = 0.4, isPannedable: Bool = false) {
self.contentViewController = contentViewController
self.defaultHeight = defaultHeight
self.cornerRedius = cornerRadius
self.dimmedAlpha = dimmedAlpha
self.isPannedable = isPannedable
super.init(nibName: nil, bundle: nil)
self.modalPresentationStyle = .overFullScreen
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.configureUI()
self.configureLayout()
self.configureDimmedTapGesture()
if isPannedable {
self.configureViewPannedGesture()
self.dragIndicatorView.alpha = 1
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.showBottomSheet()
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate { [weak self] _ in
self?.showBottomSheet()
}
}
private func showBottomSheet(atState: BottomSheetViewState = .normal) {
if atState == .normal {
let safeAreaHeight: CGFloat = view.safeAreaLayoutGuide.layoutFrame.height
let bottomPadding: CGFloat = view.safeAreaInsets.bottom
let constraintValue = (safeAreaHeight + bottomPadding) - defaultHeight
if constraintValue > 0 {
self.bottomSheetViewTopConstraint.constant = constraintValue
} else {
self.bottomSheetViewTopConstraint.constant = self.bottomSheetPanMinTopConstant
}
} else {
self.bottomSheetViewTopConstraint.constant = self.bottomSheetPanMinTopConstant
}
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut, animations: {
self.dimmedView.alpha = self.dimAlphaWithBottomSheetTopConstraint(value: self.bottomSheetViewTopConstraint.constant)
self.view.layoutIfNeeded()
}, completion: nil)
}
}
// MARK: Configure
extension BottomSheetViewController {
private func configureUI() {
view.addSubview(dimmedView)
view.addSubview(bottomSheetView)
view.addSubview(dragIndicatorView)
addChild(contentViewController)
bottomSheetView.addSubview(contentViewController.view)
contentViewController.didMove(toParent: self)
bottomSheetView.clipsToBounds = true
}
private func configureLayout() {
dimmedView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
dimmedView.topAnchor.constraint(equalTo: view.topAnchor),
dimmedView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
dimmedView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
dimmedView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
// MARK: Layout 깨짐 경고를 제거하고자 하는 경우 bottomSheetView의 heightAnchor 값을 지정하면 해결된다.
bottomSheetView.translatesAutoresizingMaskIntoConstraints = false
let topConstant = view.safeAreaInsets.bottom + view.safeAreaLayoutGuide.layoutFrame.height
bottomSheetViewTopConstraint = bottomSheetView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: topConstant)
NSLayoutConstraint.activate([
bottomSheetView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
bottomSheetView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
bottomSheetView.bottomAnchor.constraint(equalTo: view.bottomAnchor), // MARK: 이부분으로 인해 Layout 깨짐 경고가 뜬다
bottomSheetViewTopConstraint,
])
contentViewController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentViewController.view.topAnchor.constraint(equalTo: bottomSheetView.topAnchor),
contentViewController.view.leadingAnchor.constraint(equalTo: bottomSheetView.leadingAnchor),
contentViewController.view.trailingAnchor.constraint(equalTo: bottomSheetView.trailingAnchor),
contentViewController.view.bottomAnchor.constraint(equalTo: bottomSheetView.bottomAnchor)
])
dragIndicatorView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
dragIndicatorView.widthAnchor.constraint(equalToConstant: 60),
dragIndicatorView.heightAnchor.constraint(equalToConstant: dragIndicatorView.layer.cornerRadius * 2),
dragIndicatorView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
dragIndicatorView.bottomAnchor.constraint(equalTo: bottomSheetView.topAnchor, constant: 12)
])
}
private func configureDimmedTapGesture() {
let dimmedTap = UITapGestureRecognizer(target: self, action: #selector(dimmedViewTapped(_:)))
dimmedView.addGestureRecognizer(dimmedTap)
dimmedView.isUserInteractionEnabled = true
}
private func configureViewPannedGesture() {
// Pan Gesture Recognizer를 view controller의 view에 추가하기 위한 코드
let viewPan = UIPanGestureRecognizer(target: self, action: #selector(viewPanned(_:)))
// 기본적으로 iOS는 터치가 드래그하였을 때 딜레이가 발생함
// 우리는 드래그 제스쳐가 바로 발생하길 원하기 때문에 딜레이가 없도록 아래와 같이 설정
viewPan.delaysTouchesBegan = false
viewPan.delaysTouchesEnded = false
view.addGestureRecognizer(viewPan)
}
}
// MARK: Gesture
extension BottomSheetViewController {
@objc private func dimmedViewTapped(_ tapRecognizer: UITapGestureRecognizer) {
self.hideBottomSheetAndGoBack()
}
// 해당 메소드는 사용자가 view를 드래그하면 실행됨
@objc private func viewPanned(_ panGestureRecognizer: UIPanGestureRecognizer) {
let translation = panGestureRecognizer.translation(in: view)
let velocity = panGestureRecognizer.velocity(in: view)
switch panGestureRecognizer.state {
case .began:
bottomSheetPanStartingTopConstant = bottomSheetViewTopConstraint.constant
case .changed:
if bottomSheetPanStartingTopConstant + translation.y > bottomSheetPanMinTopConstant {
bottomSheetViewTopConstraint.constant = bottomSheetPanStartingTopConstant + translation.y
}
dimmedView.alpha = dimAlphaWithBottomSheetTopConstraint(value: bottomSheetViewTopConstraint.constant)
case .ended:
if velocity.y > 1500 {
hideBottomSheetAndGoBack()
return
}
let safeAreaHeight = view.safeAreaLayoutGuide.layoutFrame.height
let bottomPadding = view.safeAreaInsets.bottom
let defaultPadding = safeAreaHeight+bottomPadding - defaultHeight
let nearestValue = nearest(to: bottomSheetViewTopConstraint.constant, inValues: [bottomSheetPanMinTopConstant, defaultPadding, safeAreaHeight + bottomPadding])
if nearestValue == bottomSheetPanMinTopConstant {
showBottomSheet(atState: .expanded)
} else if nearestValue == defaultPadding {
// Bottom Sheet을 .normal 상태로 보여주기
showBottomSheet(atState: .normal)
} else {
// Bottom Sheet을 숨기고 현재 View Controller를 dismiss시키기
hideBottomSheetAndGoBack()
}
default:
break
}
}
}
extension BottomSheetViewController {
private func hideBottomSheetAndGoBack() {
let safeAreaHeight = view.safeAreaLayoutGuide.layoutFrame.height
let bottomPadding = view.safeAreaInsets.bottom
bottomSheetViewTopConstraint.constant = safeAreaHeight + bottomPadding
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseIn, animations: {
self.dimmedView.alpha = 0.0
self.view.layoutIfNeeded()
}) { _ in
if self.presentingViewController != nil {
self.dismiss(animated: false, completion: nil)
}
}
}
//주어진 CGFloat 배열의 값 중 number로 주어진 값과 가까운 값을 찾아내는 메소드
private func nearest(to number: CGFloat, inValues values: [CGFloat]) -> CGFloat {
guard let nearestVal = values.min(by: { abs(number - $0) < abs(number - $1) })
else { return number }
return nearestVal
}
private func dimAlphaWithBottomSheetTopConstraint(value: CGFloat) -> CGFloat {
let fullDimAlpha: CGFloat = self.dimmedAlpha
let safeAreaHeight = view.safeAreaLayoutGuide.layoutFrame.height
let bottomPadding = view.safeAreaInsets.bottom
// bottom sheet의 top constraint 값이 fullDimPosition과 같으면
// dimmer view의 alpha 값이 dimmedAlpha가 되도록 합니다
let fullDimPosition = (safeAreaHeight + bottomPadding - defaultHeight) / 2
// bottom sheet의 top constraint 값이 noDimPosition과 같으면
// dimmer view의 alpha 값이 0.0이 되도록 합니다
let noDimPosition = safeAreaHeight + bottomPadding
// Bottom Sheet의 top constraint 값이 fullDimPosition보다 작으면
// 배경색이 가장 진해진 상태로 지정해줍니다.
if value < fullDimPosition {
return fullDimAlpha
}
// Bottom Sheet의 top constraint 값이 noDimPosition보다 크면
// 배경색이 투명한 상태로 지정해줍니다.
if value > noDimPosition {
return 0.0
}
// 그 외의 경우 top constraint 값에 따라 0.0과 dimmedAlpha 사이의 alpha 값이 Return되도록 합니다
return fullDimAlpha * (1 - ((value - fullDimPosition) / (noDimPosition - fullDimPosition)))
}
}
원하시는 대로 수정하고싶으신 분들은 설명 내용을 참고하시면 됩니다!
BottomSheet의 높이 상태 값이며, 전체화면 표시와 defaultHeight 높이 표시 상태가 있습니다.
showBottomSheet()
함수 부분을 수정하시면 됩니다.enum BottomSheetViewState {
case expanded // 전체화면 표시 상태
case normal // defaultHeight 표시 상태
}
BottomSheet의 뒷배경을 흐리게 표시하는 뷰, darkGray 컬러의 dimmedAlpha(0.4)
alpha 값으로 설정됩니다.
dimmedAlpha
값을 변경하시면 됩니다.private lazy var dimmedView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.darkGray.withAlphaComponent(self.dimmedAlpha)
view.alpha = 0
return view
}()
dimmedView 위에 BottomSheet를 표시하는 뷰 영역, init시 입력된 cornerRedius
값으로 상단 Radius값이 설정됩니다.
private lazy var bottomSheetView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.layer.cornerRadius = self.cornerRedius
view.layer.cornerCurve = .continuous
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
view.clipsToBounds = true
return view
}()
bottomSheetView가 드래그 가능하다는것을 나타내기 위한 Indicator, init시 입력된 isPannedable
값에 따라 표시, 숨김이 됩니다.
private let dragIndicatorView: UIView = {
let view = UIView()
view.backgroundColor = .label
view.layer.cornerRadius = 1.5
view.alpha = 0
return view
}()
view의 상단과 bottomSheetView간의 거리를 조절하기 위한 property, 해당 값을 통해 bottomSheetView의 높이를 조절합니다.
private var bottomSheetViewTopConstraint: NSLayoutConstraint!
BottomSheet의 기본 높이를 지정하기 위한 property, init시 전달받은 값으로 설정됩니다.
// 열린 BottomSheet의 기본 높이를 지정하기 위한 프로퍼티
var defaultHeight: CGFloat = 500
bottomSheetView의 상단 CornerRedius 값, init시 전달받은 값으로 설정되며, 기본값으로 16 입니다.
// bottomSheetView의 상단 CornerRadius 값
var cornerRedius: CGFloat = 16
dimmedView의 alpha 값이며, init시 전달받은 값으로 설정되며, 기본값으로 0.4 입니다.
// dimmedView의 alpha값
var dimmedAlpha: CGFloat = 0.4
bottomSheetView.top과 safeArea.top 사이의 최소간격값을 나타내는 property
// Bottom Sheet과 safe Area Top 사이의 최소값을 지정하기 위한 프로퍼티
var bottomSheetPanMinTopConstant: CGFloat = 40
bottomSheetView를 드래그할 수 있는지 여부값, init시 전달받은 값으로 설정되며, 기본값으로 false 입니다.
// pannedGesture 활성화 여부
var isPannedable: Bool = false
bottomSheetView를 드래그하기 전에 bottomSheetView의 top constraint 값을 저장하기 위한 property
// 드래그 하기 전에 Bottom Sheet의 top Constraint value를 저장하기 위한 프로퍼티
private lazy var bottomSheetPanStartingTopConstant: CGFloat = bottomSheetPanMinTopConstant
bottomSheetView에 표시되고자 하는 viewController
private let contentViewController: UIViewController
BottomSheetViewController를 생성하는 부분, 필요한 값들을 받아 설정합니다.
init(contentViewController: UIViewController, defaultHeight: CGFloat, cornerRadius: CGFloat = 16, dimmedAlpha: CGFloat = 0.3, isPannedable: Bool = false) {
self.contentViewController = contentViewController
self.defaultHeight = defaultHeight
self.cornerRedius = cornerRadius
self.dimmedAlpha = dimmedAlpha
self.isPannedable = isPannedable
super.init(nibName: nil, bundle: nil)
self.modalPresentationStyle = .overFullScreen
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
viewDidLoad() 함수이며, BottonSheetViewController의 view가 메모리에 로드된 후 호출되며, 해당 함수내에서 ui와 layout 등을 설정합니다.
isPannedable
값에 따라 bottomSheetView의 드래그 제스처 및 dragIndicatorView 표시를 설정합니다.override func viewDidLoad() {
super.viewDidLoad()
self.configureUI()
self.configureLayout()
self.configureDimmedTapGesture()
if isPannedable {
self.configureViewPannedGesture()
self.dragIndicatorView.alpha = 1
}
}
viewDidAppear() 함수이며, BottomSheetViewController의 view가 뷰 계층에 추가되었음을 알립니다.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.showBottomSheet()
}
viewWillTransition() 함수이며, view의 크기가 변경되는 경우를 알립니다.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate { [weak self] _ in
self?.showBottomSheet()
}
}
bottomSheetView를 나타내는 애니메이션 및 높이조절을 담당합니다.
BottomSheetViewState
에 case를 추가하신 후, 이 함수에서 추가로직을 작성하시면 됩니다.private func showBottomSheet(atState: BottomSheetViewState = .normal) {
if atState == .normal {
let safeAreaHeight: CGFloat = view.safeAreaLayoutGuide.layoutFrame.height
let bottomPadding: CGFloat = view.safeAreaInsets.bottom
let constraintValue = (safeAreaHeight + bottomPadding) - defaultHeight
if constraintValue > 0 {
self.bottomSheetViewTopConstraint.constant = constraintValue
} else {
self.bottomSheetViewTopConstraint.constant = self.bottomSheetPanMinTopConstant
}
} else {
self.bottomSheetViewTopConstraint.constant = self.bottomSheetPanMinTopConstant
}
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut, animations: {
self.dimmedView.alpha = self.dimAlphaWithBottomSheetTopConstraint(value: self.bottomSheetViewTopConstraint.constant)
self.view.layoutIfNeeded()
}, completion: nil)
}
viewDidLoad 시점에 불리며, contentViewController의 view를 추가 및 UI를 설정합니다.
contentViewController
를 addChild를 통해 bottomSheetView
내 추가합니다.private func configureUI() {
view.addSubview(dimmedView)
view.addSubview(bottomSheetView)
view.addSubview(dragIndicatorView)
addChild(contentViewController)
bottomSheetView.addSubview(contentViewController.view)
contentViewController.didMove(toParent: self)
bottomSheetView.clipsToBounds = true
}
configureUI() 후 불리며, AutoLayout을 설정하는 부분입니다.
private func dimAlphaWithBottomSheetTopConstraint(value: CGFloat) -> CGFloat {
let fullDimAlpha: CGFloat = self.dimmedAlpha
let safeAreaHeight = view.safeAreaLayoutGuide.layoutFrame.height
let bottomPadding = view.safeAreaInsets.bottom
// bottom sheet의 top constraint 값이 fullDimPosition과 같으면
// dimmer view의 alpha 값이 dimmedAlpha가 되도록 합니다
let fullDimPosition = (safeAreaHeight + bottomPadding - defaultHeight) / 2
// bottom sheet의 top constraint 값이 noDimPosition과 같으면
// dimmer view의 alpha 값이 0.0이 되도록 합니다
let noDimPosition = safeAreaHeight + bottomPadding
// Bottom Sheet의 top constraint 값이 fullDimPosition보다 작으면
// 배경색이 가장 진해진 상태로 지정해줍니다.
if value < fullDimPosition {
return fullDimAlpha
}
// Bottom Sheet의 top constraint 값이 noDimPosition보다 크면
// 배경색이 투명한 상태로 지정해줍니다.
if value > noDimPosition {
return 0.0
}
// 그 외의 경우 top constraint 값에 따라 0.0과 dimmedAlpha 사이의 alpha 값이 Return되도록 합니다
return fullDimAlpha * (1 - ((value - fullDimPosition) / (noDimPosition - fullDimPosition)))
}
private func configureLayout() {
dimmedView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
dimmedView.topAnchor.constraint(equalTo: view.topAnchor),
dimmedView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
dimmedView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
dimmedView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
// MARK: Layout 깨짐 경고를 제거하고자 하는 경우 bottomSheetView의 heightAnchor 값을 지정하면 해결된다.
bottomSheetView.translatesAutoresizingMaskIntoConstraints = false
let topConstant = view.safeAreaInsets.bottom + view.safeAreaLayoutGuide.layoutFrame.height
bottomSheetViewTopConstraint = bottomSheetView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: topConstant)
NSLayoutConstraint.activate([
bottomSheetView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
bottomSheetView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
bottomSheetView.bottomAnchor.constraint(equalTo: view.bottomAnchor), // MARK: 이부분으로 인해 Layout 깨짐 경고가 뜬다
bottomSheetViewTopConstraint,
])
contentViewController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentViewController.view.topAnchor.constraint(equalTo: bottomSheetView.topAnchor),
contentViewController.view.leadingAnchor.constraint(equalTo: bottomSheetView.leadingAnchor),
contentViewController.view.trailingAnchor.constraint(equalTo: bottomSheetView.trailingAnchor),
contentViewController.view.bottomAnchor.constraint(equalTo: bottomSheetView.bottomAnchor)
])
dragIndicatorView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
dragIndicatorView.widthAnchor.constraint(equalToConstant: 60),
dragIndicatorView.heightAnchor.constraint(equalToConstant: dragIndicatorView.layer.cornerRadius * 2),
dragIndicatorView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
dragIndicatorView.bottomAnchor.constraint(equalTo: bottomSheetView.topAnchor, constant: 12)
])
}
dimmedView의 Tap Gesture를 추가하는 함수이며, Tap을 통해 BottomSheet를 닫습니다.
dimmedViewTapped()
함수에서 동작됩니다.private func configureDimmedTapGesture() {
let dimmedTap = UITapGestureRecognizer(target: self, action: #selector(dimmedViewTapped(_:)))
dimmedView.addGestureRecognizer(dimmedTap)
dimmedView.isUserInteractionEnabled = true
}
view에 드래그 제스처를 추가하는 함수이며, 드래그를 통해 viewPanned()
함수가 동작됩니다.
isPannedable
값에 따라 해당함수가 불리지 않는 경우 드래그 제스처가 동작되지 않습니다.private func configureViewPannedGesture() {
// Pan Gesture Recognizer를 view controller의 view에 추가하기 위한 코드
let viewPan = UIPanGestureRecognizer(target: self, action: #selector(viewPanned(_:)))
// 기본적으로 iOS는 터치가 드래그하였을 때 딜레이가 발생함
// 우리는 드래그 제스쳐가 바로 발생하길 원하기 때문에 딜레이가 없도록 아래와 같이 설정
viewPan.delaysTouchesBegan = false
viewPan.delaysTouchesEnded = false
view.addGestureRecognizer(viewPan)
}
dimmedView의 Tap 액션이 발생했을 경우 불리며, hideBottomSheetAndGoBack()
함수가 동작되며 BottomSheetViewController가 닫힙니다.
@objc private func dimmedViewTapped(_ tapRecognizer: UITapGestureRecognizer) {
self.hideBottomSheetAndGoBack()
}
view의 드레그 제스처가 동작될 때 불리며, 드래그 제스처에 따라 높이조절 및 닫힘 액션이 실행됩니다.
// 해당 메소드는 사용자가 view를 드래그하면 실행됨
@objc private func viewPanned(_ panGestureRecognizer: UIPanGestureRecognizer) {
let translation = panGestureRecognizer.translation(in: view)
let velocity = panGestureRecognizer.velocity(in: view)
switch panGestureRecognizer.state {
case .began:
bottomSheetPanStartingTopConstant = bottomSheetViewTopConstraint.constant
case .changed:
if bottomSheetPanStartingTopConstant + translation.y > bottomSheetPanMinTopConstant {
bottomSheetViewTopConstraint.constant = bottomSheetPanStartingTopConstant + translation.y
}
dimmedView.alpha = dimAlphaWithBottomSheetTopConstraint(value: bottomSheetViewTopConstraint.constant)
case .ended:
if velocity.y > 1500 {
hideBottomSheetAndGoBack()
return
}
let safeAreaHeight = view.safeAreaLayoutGuide.layoutFrame.height
let bottomPadding = view.safeAreaInsets.bottom
let defaultPadding = safeAreaHeight+bottomPadding - defaultHeight
let nearestValue = nearest(to: bottomSheetViewTopConstraint.constant, inValues: [bottomSheetPanMinTopConstant, defaultPadding, safeAreaHeight + bottomPadding])
if nearestValue == bottomSheetPanMinTopConstant {
showBottomSheet(atState: .expanded)
} else if nearestValue == defaultPadding {
// Bottom Sheet을 .normal 상태로 보여주기
showBottomSheet(atState: .normal)
} else {
// Bottom Sheet을 숨기고 현재 View Controller를 dismiss시키기
hideBottomSheetAndGoBack()
}
default:
break
}
}
BottomSheetViewController를 닫습니다.
private func hideBottomSheetAndGoBack() {
let safeAreaHeight = view.safeAreaLayoutGuide.layoutFrame.height
let bottomPadding = view.safeAreaInsets.bottom
bottomSheetViewTopConstraint.constant = safeAreaHeight + bottomPadding
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseIn, animations: {
self.dimmedView.alpha = 0.0
self.view.layoutIfNeeded()
}) { _ in
if self.presentingViewController != nil {
self.dismiss(animated: false, completion: nil)
}
}
}
dimmedViewTapped()
함수내에서 불리며, bottomSheetPanMinTopConstant (최대화면 상태), defaultPadding (defaultHeight 높이 상태), 화면 닫는 상태 중 하나를 반환합니다.
//주어진 CGFloat 배열의 값 중 number로 주어진 값과 가까운 값을 찾아내는 메소드
private func nearest(to number: CGFloat, inValues values: [CGFloat]) -> CGFloat {
guard let nearestVal = values.min(by: { abs(number - $0) < abs(number - $1) })
else { return number }
return nearestVal
}
bottomSheetView의 높이에 따라 dimmedView의 alpha 값을 조절합니다.
private func dimAlphaWithBottomSheetTopConstraint(value: CGFloat) -> CGFloat {
let fullDimAlpha: CGFloat = self.dimmedAlpha
let safeAreaHeight = view.safeAreaLayoutGuide.layoutFrame.height
let bottomPadding = view.safeAreaInsets.bottom
// bottom sheet의 top constraint 값이 fullDimPosition과 같으면
// dimmer view의 alpha 값이 dimmedAlpha가 되도록 합니다
let fullDimPosition = (safeAreaHeight + bottomPadding - defaultHeight) / 2
// bottom sheet의 top constraint 값이 noDimPosition과 같으면
// dimmer view의 alpha 값이 0.0이 되도록 합니다
let noDimPosition = safeAreaHeight + bottomPadding
// Bottom Sheet의 top constraint 값이 fullDimPosition보다 작으면
// 배경색이 가장 진해진 상태로 지정해줍니다.
if value < fullDimPosition {
return fullDimAlpha
}
// Bottom Sheet의 top constraint 값이 noDimPosition보다 크면
// 배경색이 투명한 상태로 지정해줍니다.
if value > noDimPosition {
return 0.0
}
// 그 외의 경우 top constraint 값에 따라 0.0과 dimmedAlpha 사이의 alpha 값이 Return되도록 합니다
return fullDimAlpha * (1 - ((value - fullDimPosition) / (noDimPosition - fullDimPosition)))
}
이 귀한걸.. 감사합니다 😊