Last active
May 23, 2022 07:46
BottomAlertViewController
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// BottomAlertViewController.swift | |
// | |
// | |
// Created by MinKyungJun on 2022/04/26. | |
// | |
import Foundation | |
import UIKit | |
import SnapKit | |
class BottomAlertViewController: UIViewController { | |
final class AlertView: UIView { | |
} | |
// MARK: Must Override | |
open func setConstraint() { } | |
open var alertHeight: CGFloat { | |
get { | |
return 0.0 | |
} | |
} | |
// MARK: Can Override | |
open var backgrounColor: UIColor { | |
get { | |
return .clear | |
} | |
} | |
// MARK: Public | |
public var contentView: AlertView = AlertView() | |
public func dispose(animated: Bool = true, completionHandler: (() -> Void)? = nil) { | |
self.release(animated: animated, completionHandler: completionHandler) | |
} | |
// MARK: Fileprivate | |
fileprivate var isKeyboardUprise: Bool = false | |
fileprivate var topConstraint: Constraint? | |
fileprivate var backgroundView = UIView() | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
self.modalPresentationStyle = .overFullScreen | |
self.view.backgroundColor = self.backgrounColor | |
/// 백그라운드 화면의 색상 지정과 UITapGestureRecognizer를 등록해준다. | |
self.backgroundView.backgroundColor = self.backgrounColor | |
let gesture = UITapGestureRecognizer( | |
target: self, action: #selector(self.didTapBackground(_:)) | |
) | |
self.backgroundView.addGestureRecognizer(gesture) | |
self.backgroundView.isUserInteractionEnabled = true | |
/// alertView가 추가되기 이전에 viewDidLoad에서 backgrounView를 먼저 추가한다. | |
self.view.addSubview(self.backgroundView) | |
self.backgroundView.snp.makeConstraints({ | |
$0.top.bottom.left.right.equalTo(self.view) | |
}) | |
NotificationCenter.default.addObserver( | |
self, | |
selector: #selector(self.willKeyboardShowUp(_:)), | |
name: UIResponder.keyboardWillShowNotification, | |
object: nil | |
) | |
NotificationCenter.default.addObserver( | |
self, | |
selector: #selector(self.willKeyboardHideDown(_:)), | |
name: UIResponder.keyboardWillHideNotification, | |
object: nil | |
) | |
} | |
override func viewWillAppear(_ animated: Bool) { | |
super.viewWillAppear(animated) | |
self.retain(animated: true) | |
} | |
@objc | |
fileprivate func willKeyboardShowUp(_ notification: Notification) { | |
/// Keyboard의 높이는 notification의 userInfo 값 안에 담겨져서 들어온다. | |
/// key값은 UIResponder.keyboardFrameEnduserInfoKey로 세팅하면 된다. | |
guard let height = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return } | |
self.setAlertViewOffset(height.cgRectValue.height) | |
} | |
@objc | |
fileprivate func willKeyboardHideDown(_ notification: Notification) { | |
self.setAlertViewOffset(0.0) | |
} | |
@objc | |
fileprivate func didTapBackground(_ gesture: UITapGestureRecognizer) { | |
/// 백그라운드 화면을 tap 했을 때 실행할 내용을 적어준다. | |
guard !self.isKeyboardUprise else { | |
self.isKeyboardUprise = false | |
self.contentView.endEditing(true) | |
return | |
} | |
self.release(animated: true) | |
} | |
fileprivate func setAlertViewOffset(_ offest: CGFloat) { | |
/// 1. offset의 값이 0보다 크면 keyboard가 올라와 있다는 의미이다. | |
self.isKeyboardUprise = offset > 0 | |
/// 2. offset의 값에 alertView의 높이를 더해야 topConstraint의 위치를 구할 수 있다. | |
let yOffset = offset + self.alertHeight | |
UIView.animate(withDuration: 0.5) { [weak self] in | |
/// 3. topConstraint의 기준이 view의 바닥이기 때문에 | |
/// offset + alertHeight 값을 마이너스 해줘야 view의 바닥으로부터 | |
/// 키보드 높이만큼 올라가는 효과를 볼 수 있다. | |
self?.topConstraint?.layoutConstraints[0].constant = -yOffset | |
self?.view.layoutIfNeeded() | |
} | |
} | |
fileprivate func setAlertViewTapGesture(_ alertView: AlertView) { | |
alertView.rx.tapGesture(configuration: { rec, delegate in | |
/// TapGesture가 View의 객체로 전달되도록 허용 | |
rec.cancelsTouchesInView = false | |
/// touchReceptionPolicy는 gestureRecognizer(_:shouldReceive:)와 같은 역할을 한다. | |
delegate.touchReceptionPolicy = .custom { gesture, touch in | |
/// touch한 view가 UITextField가 아닐 때에만 | |
/// view에 gesture를 전달하도록 한다. | |
return !(touch.view is UITextField) | |
} | |
}) | |
.when(.recognized) | |
.subscribe(onNext: { [weak self] recognizer in | |
self?.isKeyboardUprise = false | |
self?.contentView.endEditing(true) | |
}) | |
.disposed(by: self.disposeBag) | |
} | |
fileprivate func retain(animated: Bool = true) { | |
self.setConstraint() | |
self.view.alpha = 0.0 | |
self.backgroundView.backgroundColor = self.backgrounColor | |
self.view.addSubview(self.backgroundView) | |
self.backgroundView.snp.makeConstraints({ | |
$0.top.bottom.left.right.equalTo(self.view) | |
}) | |
let size = CGSize(width: 414.0, height: self.alertHeight) | |
self.view.addSubview(self.contentView) | |
self.contentView.backgroundColor = .white | |
self.contentView.isUserInteractionEnabled = true | |
self.contentView.frame.origin = CGPoint(x: 0, y: self.view.frame.height) | |
self.contentView.frame.size = size | |
self.contentView.snp.makeConstraints({ | |
self.topConstraint = $0.top.equalTo(self.view.snp.bottom).inset(size.height).constraint | |
$0.size.equalTo(size) | |
$0.centerX.equalTo(self.view) | |
}) | |
self.topConstraint?.activate() | |
self.contentView.roundCorners(corners: [.topLeft, .topRight], radius: 14) | |
self.contentView.layoutIfNeeded() | |
if animated { | |
UIView.animated(withDuration: 0.5, animations: { [weak self] in | |
self?.view.layoutIfNeeded() | |
}, completion: { [weak self] _ in | |
guard let self = self else { return } | |
self.setAlertViewTapGesture(self.contentView) | |
}) | |
} else { | |
CATransaction.begin() | |
CATransaction.setCompletionBlock { [weak self] in | |
guard let self = self else { return } | |
self.setAlertViewTapGesture(self.contentView) | |
} | |
self.view.layoutIfNeeded() | |
CATransaction.commit() | |
} | |
} | |
fileprivate func release(animated: Bool = true, completionHandler: (() -> Void)? = nil) { | |
guard let topConstraint = self.topConstraint else { return } | |
topConstraint.layoutConstraints[0].constant = 0 | |
let animatedHandler: () -> Void = { [weak self] in | |
guard let self = self else { return } | |
self.contentView.removeFromSuperview() | |
self.topConstraint = nil | |
self.dismiss(animated: false, completion: completionHandler) | |
} | |
if animated { | |
UIView.animate(withDuration: 0.5, animations: { [weak self] in | |
guard let self = self else { return } | |
self.view.layoutIfNeeded() | |
self.view.alpha = 0.0 | |
}, completion: { _ in | |
animatedHandler() | |
}) | |
} else { | |
CATransaction.begin() | |
CATransaction.setCompletionBlock { | |
animatedHandler() | |
} | |
self.view.layoutIfNeeded() | |
self.view.alpha = 0.0 | |
CATransaction.commit() | |
} | |
} | |
deinit { | |
print("DEINIT BOTTOM ALERT VIEW CONTROLLER") | |
NotificationCenter.default.removeObserver( | |
self, | |
name: UIResponder.keyboardWillShowNotification, | |
object: nil | |
) | |
NotificationCenter.default.removeObserver( | |
self, | |
name: UIResponder.keyboardWillHideNotification, | |
object: nil | |
) | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment