You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-ios/SignalUtilitiesKit/Shared View Controllers/OWSViewController.swift

181 lines
6.6 KiB
Swift

// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
open class OWSViewController: UIViewController {
public var shouldUseTheme: Bool = true
public var shouldIgnoreKeyboardChanges: Bool = false
public var shouldAnimateBottomLayout: Bool = false
/// If `true`, the bottom view never "reclaims" layout space if the keyboard is dismissed.
/// Defaults to `false`.
public var shouldBottomViewReserveSpaceForKeyboard: Bool = false
private weak var bottomLayoutView: UIView?
private var bottomLayoutConstraint: NSLayoutConstraint?
open override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return (UIDevice.current.isIPad ? .all : .portrait)
}
// MARK: - Initialization
public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nil, bundle: nil)
self.observeActivation()
}
required public init?(coder: NSCoder) {
super.init(coder: coder)
self.observeActivation()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: - Lifecycle
open override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.shouldAnimateBottomLayout = true
}
open override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.shouldAnimateBottomLayout = false
}
// MARK: - Functions
public func pinViewToBottomOfViewControllerOrKeyboard(_ view: UIView, avoidNotch: Bool) {
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillShow(_:)),
name: UIResponder.keyboardWillShowNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardDidShow(_:)),
name: UIResponder.keyboardDidShowNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillHide(_:)),
name: UIResponder.keyboardWillHideNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardDidHide(_:)),
name: UIResponder.keyboardDidHideNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillChangeFrame(_:)),
name: UIResponder.keyboardWillChangeFrameNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardDidChangeFrame(_:)),
name: UIResponder.keyboardDidChangeFrameNotification,
object: nil
)
self.bottomLayoutView = view
self.bottomLayoutConstraint = view.pin(
.bottom,
to: .bottom,
of: (avoidNotch ? self.view.safeAreaLayoutGuide : self.view)
)
}
// MARK: - Observations
private func observeActivation() {
NotificationCenter.default.addObserver(
self,
selector: #selector(appDidBecomeActive(_:)),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
}
@objc private func appDidBecomeActive(_ notification: NSNotification) {
self.setNeedsStatusBarAppearanceUpdate()
}
@objc private func keyboardWillShow(_ notification: NSNotification) {
self.handleKeyboardNotificationBase(notification)
}
@objc private func keyboardDidShow(_ notification: NSNotification) {
self.handleKeyboardNotificationBase(notification)
}
@objc private func keyboardWillHide(_ notification: NSNotification) {
self.handleKeyboardNotificationBase(notification)
}
@objc private func keyboardDidHide(_ notification: NSNotification) {
self.handleKeyboardNotificationBase(notification)
}
@objc private func keyboardWillChangeFrame(_ notification: NSNotification) {
self.handleKeyboardNotificationBase(notification)
}
@objc private func keyboardDidChangeFrame(_ notification: NSNotification) {
self.handleKeyboardNotificationBase(notification)
}
// We use the name `handleKeyboardNotificationBase` instead of
// `handleKeyboardNotification` to avoid accidentally
// calling similarly methods with that name in subclasses,
// e.g. ConversationViewController.
private func handleKeyboardNotificationBase(_ notification: NSNotification) {
guard !shouldIgnoreKeyboardChanges else { return }
let userInfo: [AnyHashable: Any] = (notification.userInfo ?? [:])
let keyboardRect: CGRect = ((userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect) ?? CGRect.zero)
let convertedKeyboardRect: CGRect = view.convert(keyboardRect, from: nil)
/// Adjust the position of the bottom view to account for the keyboard's intrusion into the view.
///
/// On iPhoneX, when no keyboard is present, we include a buffer at the bottom of the screen so the bottom view clears the
/// floating "home button". But because the keyboard includes it's own buffer, we subtract the length (height) of the bottomLayoutGuide,
/// else we'd have an unnecessary buffer between the popped keyboard and the input bar.
let offset: CGFloat = -max(0, (self.view.bounds.height - (self.view.window?.safeAreaInsets.bottom ?? 0) - convertedKeyboardRect.minY))
let updateLayout: () -> () = { [weak self] in
guard self?.shouldBottomViewReserveSpaceForKeyboard == false || offset < 0 else {
/// To avoid unnecessary animations / layout jitter, some views never reclaim layout space when the keyboard is dismissed.
///
/// They _do_ need to relayout if the user switches keyboards.
return
}
self?.bottomLayoutConstraint?.constant = offset
self?.bottomLayoutView?.superview?.layoutIfNeeded()
}
/// UIKit by default animates all changes in response to keyboard events.
/// We want to suppress those animations if the view isn't visible, otherwise presentation animations don't work properly.
guard shouldAnimateBottomLayout else {
return UIView.performWithoutAnimation {
updateLayout()
}
}
updateLayout()
}
}