// // LNPopupView.swift // Lanu // // Created by OneeChan on 2025/11/14. // import Foundation import UIKit import SnapKit enum LNPopupViewHeight { case auto case percent(CGFloat) case height(CGFloat) } class LNPopupView: UIView { let backgroundView = UIView() let container = UIView() var containerHeight: LNPopupViewHeight = .auto { didSet { container.snp.makeConstraints { make in switch containerHeight { case .percent(let percent): make.height.equalToSuperview().multipliedBy(percent) case .height(let height): make.height.equalTo(height) case .auto: break } } } } var touchOutsideToCancel = true var ignoreKeyboardToDismiss = false var onTouchOutside: (() -> Void)? private var bottomConstraint: Constraint? override init(frame: CGRect) { super.init(frame: frame) setupViews() } func popup(_ targetView: UIView? = nil, animated: Bool = true) { let parentView: UIView? = if let window = targetView as? UIWindow { window } else if let view = targetView?.viewController?.view { view } else if let window = UIView.appKeyWindow { window } else { nil } guard let parentView else { return } frame = parentView.bounds parentView.addSubview(self) snp.makeConstraints { make in make.edges.equalToSuperview() } layoutIfNeeded() moveToShowupPosition() window?.endEditing(true) if animated { backgroundView.backgroundColor = .clear UIView.animate(withDuration: 0.25) { self.backgroundView.backgroundColor = .black.withAlphaComponent(0.4) self.layoutIfNeeded() } } else { backgroundView.backgroundColor = .black.withAlphaComponent(0.4) layoutIfNeeded() } } func dismiss(animated: Bool = true) { endEditing(true) moveToHiddenPosition() if animated { UIView.animate(withDuration: 0.25) { self.backgroundView.backgroundColor = .clear self.layoutIfNeeded() } completion: { [weak self] _ in guard let self else { return } self.removeFromSuperview() } } else { removeFromSuperview() } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension LNPopupView { private func setupViews() { backgroundView.onTap { [weak self] in guard let self else { return } if !ignoreKeyboardToDismiss, LNKeyboardManager.shared.isEditing { endEditing(true) } else if touchOutsideToCancel { if let onTouchOutside { onTouchOutside() } else { dismiss() } } else { endEditing(true) } } insertSubview(backgroundView, at: 0) backgroundView.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() make.bottom.equalToSuperview() make.top.equalToSuperview().offset(-(UIView.navigationBarHeight + UIView.statusBarHeight)) } container.backgroundColor = .white container.layer.cornerRadius = 20 container.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] addSubview(container) container.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() make.top.equalTo(snp.bottom).priority(.medium) bottomConstraint = make.bottom.equalToSuperview().priority(.low).constraint } let dismissView = UIView() container.addSubview(dismissView) dismissView.snp.makeConstraints { make in make.edges.equalToSuperview() } dismissView.onTap { [weak self] in guard let self else { return } endEditing(true) } LNEventDeliver.addObserver(self) } private func moveToHiddenPosition() { bottomConstraint?.update(priority: .low).update(offset: 0) } private func moveToShowupPosition() { bottomConstraint?.update(priority: .high).update(offset: 0) } private func onKeyboardShowup(_ keyboardHeight: CGFloat) { bottomConstraint?.update(offset: -keyboardHeight) } } extension LNPopupView: LNKeyboardNotify { func onKeyboardWillShow(curInput: UIView?, keyboardHeight: CGFloat) { guard let curInput, curInput.isDescendant(of: self) == true else { return } let containerY = bounds.height - container.bounds.height let editViewY = curInput.convert(curInput.bounds, to: container).maxY let offset = bounds.height - (containerY + editViewY) - keyboardHeight - 16 if offset < 0 { onKeyboardShowup(-offset) } } func onKeyboardShow(curInput: UIView?, keyboardHeight: CGFloat) { guard curInput?.isDescendant(of: self) == true else { return } layoutIfNeeded() } func onKeyboardWillHide(curInput: UIView?) { guard curInput?.isDescendant(of: self) == true else { return } moveToShowupPosition() } func onKeyboardHide(curInput: UIView?) { guard curInput?.isDescendant(of: self) == true else { return } layoutIfNeeded() } }