Skip to content

Instantly share code, notes, and snippets.

@kyungpyoda
Last active January 31, 2024 04:15
Show Gist options
  • Save kyungpyoda/6876b8acdb6e6e5e1d9c0e2172d9ef1d to your computer and use it in GitHub Desktop.
Save kyungpyoda/6876b8acdb6e6e5e1d9c0e2172d9ef1d to your computer and use it in GitHub Desktop.
[iOS, Swift] Presentation Customizable ViewController
//
// PartialPresentationController.swift
// PartialViewControllerSample
//
// Created by 홍경표 on 2021/11/16.
//
import UIKit
/**
`present` 방식의 커스텀을 위한 것.
사이드 메뉴나 위, 아래에서 나타내도록 하기 위함.
- VC의 초기화 시점(init)에서 아래 프로퍼티들이 설정 되어야 함.
```
modalPresentationStyle = .custom
modalTransitionStyle = .crossDissolve
transitioningDelegate = self
```
- UIViewControllerTransitioningDelegate 채택해야함.
```
extension SomeVC: UIViewControllerTransitioningDelegate {
func presentationController(
forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController
) -> UIPresentationController? {
return PartialPresentationController(
direction: .top,
viewSize: (.full, .compressed),
presentedViewController: presented,
presenting: presenting
)
}
}
```
- 혹은 이러한 번거로운 설정 과정을 미리 해놓은 PartialVC을 상속하여 사용
*/
public final class PartialPresentationController: UIPresentationController {
// MARK: Constants
public enum Direction {
case top
case bottom
case left
case right
case center
var swipeDirection: UISwipeGestureRecognizer.Direction {
switch self {
case .top: return .up
case .bottom: return .down
case .left: return .left
case .right: return .right
case .center: return .init()
}
}
}
public enum SizeType {
case full // 화면 채움
case fit // contentSize에 딱 맞게
case absolute(CGFloat) // 인자값 크기대로
}
public typealias SizePair = (width: SizeType, height: SizeType)
// MARK: Properties
/// presentedView 방향
private let direction: Direction
/// presentedView 크기
private let viewSize: SizePair
/// presentedView의 preferredContentSize
private var contentSize: CGSize = .zero
/// swipe로 dismiss on / off
private let isSwipeEnabled: Bool
private lazy var tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(onTapBackground))
private var swipeRecognizer: UISwipeGestureRecognizer {
let swipe = UISwipeGestureRecognizer(target: self, action: #selector(onSwiped))
swipe.direction = direction.swipeDirection
return swipe
}
private lazy var dimmingView: UIView = {
let dimmingView = UIView()
dimmingView.backgroundColor = .black.withAlphaComponent(0.52)
dimmingView.alpha = 0
dimmingView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
dimmingView.addGestureRecognizer(tapRecognizer)
if isSwipeEnabled {
dimmingView.addGestureRecognizer(swipeRecognizer)
}
return dimmingView
}()
// MARK: Initialize
init(
direction: Direction,
viewSize: SizePair,
isSwipeEnabled: Bool = true,
presentedViewController: UIViewController,
presenting presentingViewController: UIViewController?
) {
presentedViewController.modalPresentationStyle = .custom
presentedViewController.modalTransitionStyle = .crossDissolve
self.direction = direction
self.viewSize = viewSize
self.isSwipeEnabled = isSwipeEnabled
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
/// present 애니메이션
public override func presentationTransitionWillBegin() {
super.presentationTransitionWillBegin()
containerView?.addSubview(dimmingView)
dimmingView.frame.size = containerView?.bounds.size ?? .zero
// container(UITransitionView) 관련 애니메이션
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { [weak self] context in
self?.dimmingView.alpha = 1
let transform: CGAffineTransform
switch self?.direction {
case .top:
transform = .init(translationX: 0, y: self?.presentedView?.frame.height ?? 0)
case .bottom:
transform = .init(translationX: 0, y: -(self?.presentedView?.frame.height ?? 0))
case .left:
transform = .init(translationX: (self?.presentedView?.frame.width ?? 0), y: 0)
case .right:
transform = .init(translationX: -(self?.presentedView?.frame.width ?? 0), y: 0)
case .center:
transform = .identity
case .none:
transform = .identity
}
self?.presentedView?.transform = transform
}, completion: nil)
// swipe to dismiss
if isSwipeEnabled {
presentedView?.addGestureRecognizer(swipeRecognizer)
}
}
/// dismiss 애니메이션
public override func dismissalTransitionWillBegin() {
super.dismissalTransitionWillBegin()
// container(UITransitionView) 관련 애니메이션
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { [weak self] context in
self?.dimmingView.alpha = 0
self?.presentedView?.transform = .identity
if context.transitionDuration <= 0 {
self?.dimmingView.removeFromSuperview()
}
}, completion: { [weak self] _ in
self?.dimmingView.removeFromSuperview()
})
}
/// presentedView frame 계산
public override var frameOfPresentedViewInContainerView: CGRect {
guard let containerView = containerView,
let presentedView = presentedView else { return .zero }
let compressedSize = presentedView.systemLayoutSizeFitting(
UIView.layoutFittingCompressedSize,
withHorizontalFittingPriority: .defaultLow,
verticalFittingPriority: .defaultLow
)
var frame = containerView.bounds
switch viewSize.width {
case .full:
frame.size.width = frame.width
case .fit:
frame.size.width = compressedSize.width
case .absolute(let value):
frame.size.width = value
}
switch viewSize.height {
case .full:
frame.size.height = frame.height
case .fit:
frame.size.height = compressedSize.height
case .absolute(let value):
frame.size.height = value
}
if case .fit = viewSize.width {
let temp = CGSize(width: UIView.layoutFittingCompressedSize.width, height: frame.size.height)
frame.size.width = presentedView.systemLayoutSizeFitting(temp, withHorizontalFittingPriority: .defaultLow, verticalFittingPriority: .required).width
}
if case .fit = viewSize.height {
let temp = CGSize(width: frame.size.width, height: UIView.layoutFittingCompressedSize.height)
frame.size.height = presentedView.systemLayoutSizeFitting(temp, withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow).height
}
switch direction {
case .top:
frame.origin.y = 0 - frame.height
frame.origin.x = (containerView.bounds.width - frame.width) / 2
case .bottom:
frame.origin.y = containerView.bounds.height - frame.height + frame.height
frame.origin.x = (containerView.bounds.width - frame.width) / 2
case .left:
frame.origin.x = 0 - frame.width
frame.origin.y = (containerView.bounds.height - frame.height) / 2
case .right:
frame.origin.x = containerView.bounds.width - frame.width + frame.width
frame.origin.y = (containerView.bounds.height - frame.height) / 2
case .center:
frame.origin.x = containerView.center.x - frame.width / 2
frame.origin.y = containerView.center.y - frame.height / 2
}
return frame
}
// tap background to dismiss
@objc private func onTapBackground() {
presentedViewController.dismiss(animated: true, completion: nil)
}
// swipe to dismiss
@objc private func onSwiped() {
presentedViewController.dismiss(animated: true, completion: nil)
}
}
//
// PartialViewController.swift
// PartialViewControllerSample
//
// Created by 홍경표 on 2021/11/16.
//
import UIKit
open class PartialViewController: UIViewController {
private let direction: PartialPresentationController.Direction
private let viewSize: PartialPresentationController.SizePair
private let isSwipeEnabled: Bool
public init(
direction: PartialPresentationController.Direction,
viewSize: PartialPresentationController.SizePair,
isSwipeEnabled: Bool = true
) {
self.direction = direction
self.viewSize = viewSize
self.isSwipeEnabled = isSwipeEnabled
super.init(nibName: nil, bundle: nil)
// Custom PresentationController를 사용하기 위함
modalPresentationStyle = .custom
modalTransitionStyle = .crossDissolve // crossDissolve로 해놓고 디테일한 애니메이션은 직접 하는게 나음
transitioningDelegate = self
// statusBar를 presentedViewController 기준으로 설정할지
modalPresentationCapturesStatusBarAppearance = true
}
required public init?(coder: NSCoder) {
fatalError("All UIs are code based, not nib.")
}
deinit {
print("\(type(of: self)): \(#function)")
}
}
extension PartialViewController: UIViewControllerTransitioningDelegate {
public func presentationController(
forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController
) -> UIPresentationController? {
return PartialPresentationController(
direction: direction,
viewSize: viewSize,
isSwipeEnabled: isSwipeEnabled,
presentedViewController: presented,
presenting: presenting
)
}
}
@kyungpyoda
Copy link
Author

kyungpyoda commented Nov 16, 2021

btn1 btn2 btn3 btn4
image image image image
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment