Skip to content

Instantly share code, notes, and snippets.

@jxxnnee
Last active May 23, 2022 07:46
Show Gist options
  • Save jxxnnee/88923392b8e7caa56f92b8975b7e3726 to your computer and use it in GitHub Desktop.
Save jxxnnee/88923392b8e7caa56f92b8975b7e3726 to your computer and use it in GitHub Desktop.
BottomAlertViewController
//
// 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