Signal-iOS/SignalUI/Views/InputAccessoryViewPlaceholder.swift
Evan Hahn 370ff654e7
Change license to AGPL
Change license to AGPL

This commit:

- Updates the `LICENSE` file

- Start every file with something like:

      // Copyright YEAR_FIRST_PUBLISHED Signal Messenger, LLC
      // SPDX-License-Identifier: AGPL-3.0-only

---

First, I removed existing license headers with this Ruby 3.1.2 script:

    require 'set'

    EXTENSIONS_TO_CHECK = Set['.h', '.hpp', '.cpp', '.m', '.mm', '.pch', '.swift']

    same = 0
    different = 0

    all_files = `git ls-files`.lines.map { |line| line.strip }
    all_files.each do |relative_path|
      if relative_path == 'Pods'
        next
      end

      unless EXTENSIONS_TO_CHECK.include? File.extname(relative_path)
        next
      end

      path = File.expand_path(relative_path)

      contents = File.read(path)
      new_contents = contents.sub(/\/\/\n\/\/  Copyright .*\n\/\/\n\n/, '')

      if contents == new_contents
        same += 1
      else
        different += 1
      end

      File.write(path, new_contents)
    end

    puts "updated #{different} file(s), left #{same} untouched"

I'm sure this script could be improved, but it worked well enough.

Then, I created `Scripts/lint/lint-license-headers` and ran it to auto-
fix a lot of files. This changed the mode of some files, but I think
that's actually desirable. For example,
`SignalServiceKit/src/Util/AppContext.m` previously had a mode of
`0755/-rwxr-xr-x`, and it's now `0644/-rw-r--r--`.

Then I fixed some stragglers and updated the precommit script.

See [a similar change in the Desktop app][0].

[0]: 8bfaf598af
2022-10-13 08:25:37 -05:00

246 lines
8.9 KiB
Swift

//
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
@objc
public protocol InputAccessoryViewPlaceholderDelegate: AnyObject {
func inputAccessoryPlaceholderKeyboardIsPresenting(animationDuration: TimeInterval, animationCurve: UIView.AnimationCurve)
func inputAccessoryPlaceholderKeyboardDidPresent()
func inputAccessoryPlaceholderKeyboardIsDismissing(animationDuration: TimeInterval, animationCurve: UIView.AnimationCurve)
func inputAccessoryPlaceholderKeyboardDidDismiss()
func inputAccessoryPlaceholderKeyboardIsDismissingInteractively()
}
// MARK: -
/// Input accessory views always render at the full width of the window.
/// This wrapper allows resizing the accessory view to fit within its
/// presenting view.
@objc
public class InputAccessoryViewPlaceholder: UIView {
@objc
public weak var delegate: InputAccessoryViewPlaceholderDelegate?
/// The amount of the application frame that is overlapped
/// by the keyboard.
@objc
public var keyboardOverlap: CGFloat {
// Subtract our own height as this view is not actually
// visible, but is represented in the keyboard.
let ownHeight = superview != nil ? desiredHeight : 0
return max(0, visibleKeyboardHeight - ownHeight)
}
@objc
public weak var referenceView: UIView?
private var visibleKeyboardHeight: CGFloat {
guard var keyboardFrame = transitioningKeyboardFrame ?? superview?.frame else { return 0 }
guard keyboardFrame.height > 0 else { return 0 }
let referenceFrame: CGRect
if let referenceView = referenceView {
keyboardFrame = referenceView.convert(keyboardFrame, from: nil)
referenceFrame = referenceView.frame
} else {
referenceFrame = CurrentAppContext().frame
}
// Measure how much of the keyboard is currently offscreen.
let offScreenHeight = keyboardFrame.maxY - referenceFrame.maxY
// The onscreen region represents the overlap.
return max(0, keyboardFrame.height - offScreenHeight)
}
private var transitioningKeyboardFrame: CGRect? {
switch keyboardState {
case .dismissing:
return .zero
case .presenting(let frame):
return frame
default:
return nil
}
}
/// The height that the accessory view should take up. This is
/// automatically subtracted from the keyboard overlap and is
/// intended to represent the extent to which you want the
/// accessory view to overlap the presenting view, primarily
/// for the purpose of defining the start point for interactive
/// dismissals.
@objc
public var desiredHeight: CGFloat {
get {
return heightConstraint.constant
}
set {
guard newValue != desiredHeight else { return }
heightConstraint.constant = newValue
UIView.performWithoutAnimation {
heightConstraintView.layoutIfNeeded()
self.layoutIfNeeded()
superview?.layoutIfNeeded()
}
}
}
private let heightConstraintView = UIView()
private lazy var heightConstraint: NSLayoutConstraint = {
addSubview(heightConstraintView)
heightConstraintView.autoPinHeightToSuperview()
return heightConstraintView.autoSetDimension(.height, toSize: 0)
}()
private enum KeyboardState: CustomStringConvertible {
case dismissed
case dismissing
case presented
case presenting(frame: CGRect)
public var description: String {
switch self {
case .dismissed:
return "dismissed"
case .dismissing:
return "dismissing"
case .presented:
return "presented"
case .presenting:
return "presenting"
}
}
}
private var keyboardState: KeyboardState = .dismissed
public init() {
super.init(frame: .zero)
// Disable user interaction, the accessory view
// should never actually contain any UI.
isUserInteractionEnabled = false
autoresizingMask = .flexibleHeight
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillDismiss),
name: UIResponder.keyboardWillHideNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardDidDismiss),
name: UIResponder.keyboardDidHideNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillPresent),
name: UIResponder.keyboardWillShowNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardDidPresent),
name: UIResponder.keyboardDidShowNotification,
object: nil
)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
superviewCenterObserver?.invalidate()
}
public override var intrinsicContentSize: CGSize {
return .zero
}
private var superviewCenterObserver: NSKeyValueObservation?
public override func willMove(toSuperview newSuperview: UIView?) {
super.willMove(toSuperview: newSuperview)
// By observing the .center property of our superview, we can
// follow along as the keyboard moves up and down.
superviewCenterObserver?.invalidate()
superviewCenterObserver = newSuperview?.observe(\.center, options: [.initial, .new]) { [weak self] (_, _) in
guard let self = self else { return }
// Do nothing unless the keyboard is currently presented.
// We're only checking for interactive dismissal, which
// can only happen while presented.
guard case .presented = self.keyboardState else { return }
guard self.superview != nil else { return }
// While the visible keyboard height is greater than zero,
// and the keyboard is presented, we can safely assume
// an interactive dismissal is in progress.
if self.visibleKeyboardHeight > 0 {
self.delegate?.inputAccessoryPlaceholderKeyboardIsDismissingInteractively()
}
}
}
// MARK: - Presentation / Dismissal wrangling.
@objc
private func keyboardWillPresent(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let beginFrame = userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? CGRect,
let endFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect,
let animationDuration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval,
let rawAnimationCurve = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int,
let animationCurve = UIView.AnimationCurve(rawValue: rawAnimationCurve) else {
return owsFailDebug("keyboard notification missing expected userInfo properties")
}
// We only want to do an animated presentation if either a) the height changed or b) the view is
// starting from off the bottom of the screen (a full presentation). This provides the best experience
// when canceling an interactive dismissal or changing orientations.
guard beginFrame.height != endFrame.height || beginFrame.minY == UIScreen.main.bounds.height else { return }
keyboardState = .presenting(frame: endFrame)
delegate?.inputAccessoryPlaceholderKeyboardIsPresenting(animationDuration: animationDuration, animationCurve: animationCurve)
}
@objc
private func keyboardDidPresent(_ notification: Notification) {
keyboardState = .presented
delegate?.inputAccessoryPlaceholderKeyboardDidPresent()
}
@objc
private func keyboardWillDismiss(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let animationDuration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval,
let rawAnimationCurve = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int,
let animationCurve = UIView.AnimationCurve(rawValue: rawAnimationCurve) else {
return owsFailDebug("keyboard notification missing expected userInfo properties")
}
keyboardState = .dismissing
delegate?.inputAccessoryPlaceholderKeyboardIsDismissing(animationDuration: animationDuration, animationCurve: animationCurve)
}
@objc
private func keyboardDidDismiss(_ notification: Notification) {
keyboardState = .dismissed
delegate?.inputAccessoryPlaceholderKeyboardDidDismiss()
}
}