TM-SGNL-iOS/SignalUI/SafetyNumbers/SafetyNumberConfirmationSheet.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

795 lines
31 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public import SignalServiceKit
public class SafetyNumberConfirmationSheet: UIViewController {
let stackView = UIStackView()
let contentView = UIView()
let handle = UIView()
let backdropView = UIView()
let tableView = UITableView()
struct Item {
let address: SignalServiceAddress
let displayName: String
let verificationState: OWSVerificationState
let identityKey: Data?
}
private var confirmationItems: [Item]
let confirmAction: ActionSheetAction
let cancelAction: ActionSheetAction
let completionHandler: (Bool) -> Void
public var allowsDismissal: Bool = true
public let theme: Theme.ActionSheet
public init(
addressesToConfirm: [SignalServiceAddress],
confirmationText: String,
cancelText: String = CommonStrings.cancelButton,
theme: Theme.ActionSheet = .translucentDark,
completionHandler: @escaping (Bool) -> Void
) {
assert(!addressesToConfirm.isEmpty)
self.confirmationItems = SSKEnvironment.shared.databaseStorageRef.read { transaction in
Self.buildConfirmationItems(
addressesToConfirm: addressesToConfirm,
transaction: transaction
)
}
self.confirmAction = ActionSheetAction(title: confirmationText, style: .default)
self.cancelAction = ActionSheetAction(title: cancelText, style: .cancel)
self.completionHandler = completionHandler
self.theme = theme
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .custom
transitioningDelegate = self
observeIdentityChangeNotification()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fileprivate static func buildConfirmationItems(
addressesToConfirm: [SignalServiceAddress],
transaction tx: SDSAnyReadTransaction
) -> [Item] {
let identityManager = DependenciesBridge.shared.identityManager
return addressesToConfirm.map { address in
let recipientIdentity = identityManager.recipientIdentity(for: address, tx: tx.asV2Read)
return Item(
address: address,
displayName: SSKEnvironment.shared.contactManagerRef.displayName(for: address, tx: tx).resolvedValue(),
verificationState: recipientIdentity?.verificationState ?? .default,
identityKey: recipientIdentity?.identityKey
)
}
}
// MARK: - Identity change notification
private func observeIdentityChangeNotification() {
NotificationCenter.default.addObserver(
self,
selector: #selector(identityStateDidChange),
name: .identityStateDidChange,
object: nil
)
}
/// Rebuild our confirmation items and reload the table, to ensure we
/// reflect the latest identity state after the user may have verified
/// one of the addresses we presented.
@objc
private func identityStateDidChange() {
SSKEnvironment.shared.databaseStorageRef.read { transaction in
let addressesToConfirm = confirmationItems.map { $0.address }
confirmationItems = Self.buildConfirmationItems(
addressesToConfirm: addressesToConfirm,
transaction: transaction
)
}
tableView.reloadData()
}
// MARK: - Present if necessary
public class func presentIfNecessary(address: SignalServiceAddress, confirmationText: String, completion: @escaping (Bool) -> Void) -> Bool {
return presentIfNecessary(addresses: [address], confirmationText: confirmationText, completion: completion)
}
/**
* Shows confirmation dialog if at least one of the recipient id's is not confirmed.
*
* @returns true if an alert was shown
* false if there were no unconfirmed identities
*/
public class func presentIfNecessary(
addresses: [SignalServiceAddress],
confirmationText: String,
untrustedThreshold: Date? = nil,
completion: @escaping (Bool) -> Void
) -> Bool {
return presentIfNecessary(
for: addresses,
from: CurrentAppContext().frontmostViewController(),
confirmationText: confirmationText,
untrustedThreshold: untrustedThreshold,
completion: completion
)
}
/// Presents the a `SafetyNumberConfirmationSheet` if needed.
///
/// The sheet will be presented repeatedly to handle cases where Safety
/// Numbers change while the sheet is visible.
///
/// This method will recompute `addresses` before the initial sheet
/// presentation as well as after each sheet presentation.
///
/// - Returns: True if the user accepted all Safety Number changes OR if
/// there weren't any that needed to be accepted.
public class func presentRepeatedlyAsNecessary(
for addresses: () -> [SignalServiceAddress],
from viewController: UIViewController,
confirmationText: String,
untrustedThreshold: Date? = nil
) async -> Bool {
while true {
var untrustedThreshold = untrustedThreshold
let terminalResult: Bool? = await withCheckedContinuation { continuation in
let newUntrustedThreshold = Date()
defer { untrustedThreshold = newUntrustedThreshold }
let didPresent = self.presentIfNecessary(
for: addresses(),
from: viewController,
confirmationText: confirmationText,
untrustedThreshold: untrustedThreshold,
completion: { didConfirmIdentity in
if didConfirmIdentity {
// The user said it's fine -- loop and check for more mismatches.
continuation.resume(returning: nil)
} else {
continuation.resume(returning: false)
}
}
)
if !didPresent {
continuation.resume(returning: true)
}
}
if let terminalResult {
return terminalResult
}
}
}
public class func presentIfNecessary(
for addresses: [SignalServiceAddress],
from viewController: UIViewController?,
confirmationText: String,
untrustedThreshold: Date?,
completion: @escaping (Bool) -> Void
) -> Bool {
let identityManager = DependenciesBridge.shared.identityManager
let untrustedAddresses = SSKEnvironment.shared.databaseStorageRef.read { tx in
addresses.filter { address in
identityManager.untrustedIdentityForSending(to: address, untrustedThreshold: untrustedThreshold, tx: tx.asV2Read) != nil
}
}
guard !untrustedAddresses.isEmpty else {
// No identities to confirm, no alert to present.
return false
}
let sheet = SafetyNumberConfirmationSheet(
addressesToConfirm: untrustedAddresses,
confirmationText: confirmationText,
completionHandler: completion
)
viewController?.present(sheet, animated: true)
return true
}
// MARK: -
override public func loadView() {
view = UIView()
let backgroundView = theme.createBackgroundView()
view.addSubview(backgroundView)
view.addSubview(contentView)
backgroundView.autoPinEdges(toEdgesOf: contentView)
contentView.autoHCenterInSuperview()
contentView.autoMatch(.height, to: .height, of: view, withOffset: 0, relation: .lessThanOrEqual)
// Prefer to be full width, but don't exceed the maximum width
contentView.autoSetDimension(.width, toSize: maxWidth, relation: .lessThanOrEqual)
NSLayoutConstraint.autoSetPriority(.defaultHigh) {
contentView.autoPinWidthToSuperview()
}
[backgroundView, contentView].forEach { subview in
subview.layer.cornerRadius = 16
subview.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
subview.layer.masksToBounds = true
}
stackView.axis = .vertical
contentView.addSubview(stackView)
stackView.autoPinEdgesToSuperviewSafeArea()
// Support tapping the backdrop to cancel the action sheet.
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapBackdrop(_:)))
tapGestureRecognizer.delegate = self
view.addGestureRecognizer(tapGestureRecognizer)
// Setup header
let titleLabel = UILabel()
titleLabel.textAlignment = .center
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
titleLabel.font = UIFont.dynamicTypeBody2.semibold()
titleLabel.textColor = theme.headerTitleColor
titleLabel.text = OWSLocalizedString("SAFETY_NUMBER_CONFIRMATION_TITLE",
comment: "Title for the 'safety number confirmation' view")
let messageLabel = UILabel()
messageLabel.textAlignment = .center
messageLabel.numberOfLines = 0
messageLabel.lineBreakMode = .byWordWrapping
messageLabel.font = .dynamicTypeBody2
messageLabel.textColor = theme.headerMessageColor
messageLabel.text = OWSLocalizedString("SAFETY_NUMBER_CONFIRMATION_MESSAGE",
comment: "Message for the 'safety number confirmation' view")
let headerStack = UIStackView(arrangedSubviews: [
titleLabel,
messageLabel
])
headerStack.axis = .vertical
headerStack.spacing = 2
headerStack.isLayoutMarginsRelativeArrangement = true
headerStack.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
stackView.addArrangedSubview(headerStack)
stackView.addHairline(with: theme.hairlineColor)
stackView.addArrangedSubview(tableView)
stackView.addHairline(with: theme.hairlineColor)
tableView.alwaysBounceVertical = false
tableView.delegate = self
tableView.dataSource = self
tableView.separatorStyle = .none
tableView.backgroundColor = .clear
tableView.register(SafetyNumberCell.self, forCellReuseIdentifier: SafetyNumberCell.reuseIdentifier)
tableView.setContentHuggingHigh()
tableView.setCompressionResistanceLow()
confirmAction.button.applyActionSheetTheme(theme)
stackView.addArrangedSubview(confirmAction.button)
stackView.addHairline(with: theme.hairlineColor)
confirmAction.button.releaseAction = { [weak self] in
guard let self = self else { return }
SSKEnvironment.shared.databaseStorageRef.asyncWrite(block: { tx in
let identityManager = DependenciesBridge.shared.identityManager
for item in self.confirmationItems {
guard let identityKey = item.identityKey else {
return
}
switch identityManager.verificationState(for: item.address, tx: tx.asV2Read) {
case .verified:
// We don't want to overwrite any addresses that have been verified since
// we last checked.
return
case .noLongerVerified, .implicit(isAcknowledged: _):
break
}
_ = identityManager.setVerificationState(
.implicit(isAcknowledged: true),
of: identityKey,
for: item.address,
isUserInitiatedChange: true,
tx: tx.asV2Write
)
}
}, completionQueue: .main) {
self.dismiss(animated: true) { self.completionHandler(true) }
}
}
cancelAction.button.applyActionSheetTheme(theme)
stackView.addArrangedSubview(cancelAction.button)
cancelAction.button.releaseAction = { [weak self] in
guard let self else { return }
self.dismiss(animated: true) { self.completionHandler(false) }
}
}
private var hasPreparedInitialLayout = false
public override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
guard !hasPreparedInitialLayout else { return }
hasPreparedInitialLayout = true
// Setup handle for interactive dismissal / resizing
setupInteractiveSizing()
}
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let profileFetcher = SSKEnvironment.shared.profileFetcherRef
// When the view appears, fetch profiles if it's been a while to ensure we
// have the latest identity key.
for confirmationItem in confirmationItems {
guard let serviceId = confirmationItem.address.serviceId else {
continue
}
Task {
do {
_ = try await profileFetcher.fetchProfile(for: serviceId, options: .opportunistic)
} catch {
Logger.warn("Didn't fetch profile for Safety Number change: \(error)")
}
}
}
}
@objc
private func didTapBackdrop(_ sender: UITapGestureRecognizer) {
guard allowsDismissal else { return }
dismiss(animated: true)
}
// MARK: - Resize / Interactive Dismiss
var bottomConstraint: NSLayoutConstraint?
var heightConstraint: NSLayoutConstraint?
let maxWidth: CGFloat = 414
lazy var baseStackViewHeight: CGFloat = {
tableView.isHidden = true
stackView.layoutIfNeeded()
let baseStackViewHeight = stackView.height
tableView.isHidden = false
stackView.layoutIfNeeded()
return baseStackViewHeight
}()
lazy var cellHeight = tableView.cellForRow(at: IndexPath(row: 0, section: 0))?.height ?? 72
var minimizedHeight: CGFloat {
// We want to show, at most, 3.5 rows when minimized. When we have
// less than 4 rows, we will match our size to the number of rows.
let compactTableViewHeight = min(CGFloat(confirmationItems.count), 3.5) * cellHeight
let preferredMinimizedHeight = baseStackViewHeight + compactTableViewHeight + view.safeAreaInsets.bottom
return min(maximizedHeight, preferredMinimizedHeight)
}
var maximizedHeight: CGFloat {
let tableViewHeight = CGFloat(confirmationItems.count) * cellHeight
let preferredMaximizedHeight = baseStackViewHeight + tableViewHeight + view.safeAreaInsets.bottom
let maxPermittedHeight = CurrentAppContext().frame.height - view.safeAreaInsets.top - 16
return min(preferredMaximizedHeight, maxPermittedHeight)
}
var desiredVisibleContentHeight: CGFloat = 0 {
didSet {
updateConstraints(withDesiredContentHeight: desiredVisibleContentHeight)
}
}
func updateConstraints(withDesiredContentHeight height: CGFloat) {
// To prevent views from getting compressed, if the desired appearance height is less than
// the minimized height, we translate the content off the bottom edge
let newHeightConstant = max(minimizedHeight, desiredVisibleContentHeight)
let newBottomOffset = max((minimizedHeight - desiredVisibleContentHeight), 0)
if let heightConstraint = heightConstraint {
heightConstraint.constant = newHeightConstant
} else {
heightConstraint = contentView.autoSetDimension(.height, toSize: newHeightConstant)
}
if let bottomConstraint = bottomConstraint {
bottomConstraint.constant = newBottomOffset
} else {
bottomConstraint = contentView.autoPinEdge(toSuperviewEdge: .bottom, withInset: -newBottomOffset)
}
}
var visibleContentHeight: CGFloat {
let contentRect = contentView.convert(contentView.bounds, to: view)
return view.bounds.intersection(contentRect).height
}
let maxAnimationDuration: TimeInterval = 0.2
var startingHeight: CGFloat?
var startingTranslation: CGFloat?
var pinnedContentOffset: CGPoint?
func setupInteractiveSizing() {
desiredVisibleContentHeight = minimizedHeight
// Create a pan gesture to handle when the user interacts with the
// view outside of the reactor table views.
let panGestureRecognizer = DirectionalPanGestureRecognizer(direction: .vertical, target: self, action: #selector(handlePan))
view.addGestureRecognizer(panGestureRecognizer)
panGestureRecognizer.delegate = self
// We also want to handle the pan gesture for the table
// view, so we can do a nice scroll to dismiss gesture, and
// so we can transfer any initial scrolling into maximizing
// the view.
tableView.panGestureRecognizer.addTarget(self, action: #selector(handlePan))
handle.backgroundColor = .ows_whiteAlpha80
handle.autoSetDimensions(to: CGSize(width: 56, height: 5))
handle.layer.cornerRadius = 5 / 2
view.addSubview(handle)
handle.autoHCenterInSuperview()
handle.autoPinEdge(.bottom, to: .top, of: contentView, withOffset: -8)
}
@objc
private func handlePan(_ sender: UIPanGestureRecognizer) {
switch sender.state {
case .began, .changed:
guard beginInteractiveTransitionIfNecessary(sender),
let startingHeight = startingHeight,
let startingTranslation = startingTranslation,
let pinnedContentOffset = pinnedContentOffset else {
return resetInteractiveTransition()
}
// We're in an interactive transition, so don't let the scrollView scroll.
tableView.contentOffset = pinnedContentOffset
tableView.showsVerticalScrollIndicator = false
// We may have panned some distance if we were scrolling before we started
// this interactive transition. Offset the translation we use to move the
// view by whatever the translation was when we started the interactive
// portion of the gesture.
let translation = sender.translation(in: view).y - startingTranslation
var newHeight = startingHeight - translation
if newHeight > maximizedHeight {
newHeight = maximizedHeight
}
// If the height is decreasing, adjust the relevant view's proporitionally
if newHeight < startingHeight {
backdropView.alpha = 1 - (startingHeight - newHeight) / startingHeight
}
// Update our height to reflect the new position
desiredVisibleContentHeight = newHeight
view.layoutIfNeeded()
case .ended, .cancelled, .failed:
guard let startingHeight = startingHeight else { break }
let dismissThreshold = startingHeight * 0.5
let growThreshold = (maximizedHeight - startingHeight) * 0.5
let velocityThreshold: CGFloat = 500
let currentHeight = visibleContentHeight
let currentVelocity = sender.velocity(in: view).y
enum CompletionState { case growing, dismissing, cancelling }
let completionState: CompletionState
if abs(currentVelocity) >= velocityThreshold {
if currentVelocity < 0 {
completionState = .growing
} else {
completionState = allowsDismissal ? .dismissing : .cancelling
}
} else if currentHeight - startingHeight >= growThreshold {
completionState = .growing
} else if currentHeight <= dismissThreshold, allowsDismissal {
completionState = .dismissing
} else {
completionState = .cancelling
}
let finalHeight: CGFloat
switch completionState {
case .dismissing:
finalHeight = 0
case .growing:
finalHeight = maximizedHeight
case .cancelling:
finalHeight = startingHeight
}
let remainingDistance = finalHeight - visibleContentHeight
// Calculate the time to complete the animation if we want to preserve
// the user's velocity. If this time is too slow (e.g. the user was scrolling
// very slowly) we'll default to `maxAnimationDuration`
let remainingTime = TimeInterval(abs(remainingDistance / currentVelocity))
UIView.animate(withDuration: min(remainingTime, maxAnimationDuration), delay: 0, options: .curveEaseOut, animations: {
self.desiredVisibleContentHeight = finalHeight
self.view.layoutIfNeeded()
self.backdropView.alpha = completionState == .dismissing ? 0 : 1
}) { _ in
owsAssertDebug(completionState != .dismissing || self.allowsDismissal)
self.desiredVisibleContentHeight = finalHeight
if completionState == .dismissing { self.dismiss(animated: false, completion: nil) }
}
resetInteractiveTransition()
default:
resetInteractiveTransition()
backdropView.alpha = 1
guard let startingHeight = startingHeight else { break }
desiredVisibleContentHeight = startingHeight
}
}
func beginInteractiveTransitionIfNecessary(_ sender: UIPanGestureRecognizer) -> Bool {
let tryingToDismiss = tableView.contentOffset.y <= 0 || tableView.panGestureRecognizer != sender
let tryingToMaximize = visibleContentHeight < maximizedHeight && tableView.height < tableView.contentSize.height
// If we're at the top of the scrollView, or the view is not
// currently maximized, we want to do an interactive transition.
guard tryingToDismiss || tryingToMaximize else { return false }
if startingTranslation == nil {
startingTranslation = sender.translation(in: view).y
}
if startingHeight == nil {
startingHeight = visibleContentHeight
}
if pinnedContentOffset == nil {
pinnedContentOffset = tableView.contentOffset.y < 0 ? .zero : tableView.contentOffset
}
return true
}
func resetInteractiveTransition() {
startingTranslation = nil
startingHeight = nil
if let pinnedContentOffset = pinnedContentOffset {
tableView.contentOffset = pinnedContentOffset
}
pinnedContentOffset = nil
tableView.showsVerticalScrollIndicator = true
}
public override func viewSafeAreaInsetsDidChange() {
// The minimized height is dependent on safe the current safe area insets
// If they every change, reset the content height to the new minimized height
super.viewSafeAreaInsetsDidChange()
desiredVisibleContentHeight = minimizedHeight
}
}
// MARK: -
extension SafetyNumberConfirmationSheet: UITableViewDelegate, UITableViewDataSource {
public func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return confirmationItems.count
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: SafetyNumberCell.reuseIdentifier,
for: indexPath)
guard let contactCell = cell as? SafetyNumberCell else {
return cell
}
guard let item = confirmationItems[safe: indexPath.row] else {
return cell
}
UIView.performWithoutAnimation {
contactCell.configure(item: item, theme: theme, viewController: self)
}
return contactCell
}
}
// MARK: -
private class SafetyNumberCell: ContactTableViewCell {
open override class var reuseIdentifier: String { "SafetyNumberCell" }
let button = OWSFlatButton()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
button.setTitle(
title: OWSLocalizedString("SAFETY_NUMBER_CONFIRMATION_VIEW_ACTION",
comment: "View safety number action for the 'safety number confirmation' view"),
font: UIFont.dynamicTypeBody2.semibold(),
titleColor: Theme.ActionSheet.default.safetyNumberChangeButtonTextColor
)
button.useDefaultCornerRadius()
button.contentEdgeInsets = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(item: SafetyNumberConfirmationSheet.Item, theme: Theme.ActionSheet, viewController: UIViewController) {
button.setPressedBlock {
FingerprintViewController.present(for: item.address.aci, from: viewController)
}
SSKEnvironment.shared.databaseStorageRef.read { transaction in
let configuration = ContactCellConfiguration(address: item.address, localUserDisplayMode: .asUser)
configuration.allowUserInteraction = true
configuration.forceDarkAppearance = (theme == .translucentDark)
let buttonSize = button.intrinsicContentSize
button.removeFromSuperview()
let buttonWrapper = ManualLayoutView.wrapSubviewUsingIOSAutoLayout(button)
configuration.accessoryView = ContactCellAccessoryView(accessoryView: buttonWrapper,
size: buttonSize)
switch item.verificationState {
case .noLongerVerified:
configuration.attributedSubtitle = .prefixedWithCheck(text: OWSLocalizedString(
"SAFETY_NUMBER_CONFIRMATION_PREVIOUSLY_VERIFIED",
comment: "Text explaining that the given contact previously had their safety number verified."
))
case .verified:
configuration.attributedSubtitle = .prefixedWithCheck(text: OWSLocalizedString(
"SAFETY_NUMBER_CONFIRMATION_VERIFIED",
comment: "Text explaining that the given contact has had their safety number verified."
))
case .`default`, .defaultAcknowledged:
if let phoneNumber = item.address.phoneNumber {
let formattedPhoneNumber = PhoneNumber.bestEffortLocalizedPhoneNumber(e164: phoneNumber)
if item.displayName != formattedPhoneNumber {
configuration.attributedSubtitle = NSAttributedString(string: formattedPhoneNumber)
}
}
}
self.configure(configuration: configuration, transaction: transaction)
}
}
override func configure(configuration: ContactCellConfiguration, transaction: SDSAnyReadTransaction) {
super.configure(configuration: configuration, transaction: transaction)
let theme: Theme.ActionSheet = (configuration.forceDarkAppearance) ? .translucentDark : .default
backgroundColor = theme.backgroundColor
button.setBackgroundColors(upColor: theme.safetyNumberChangeButtonBackgroundColor)
button.setTitleColor(theme.safetyNumberChangeButtonTextColor)
}
}
private extension NSAttributedString {
static func prefixedWithCheck(
text: String
) -> NSAttributedString {
let string = NSMutableAttributedString()
string.appendTemplatedImage(named: "check-extra-small", font: UIFont.regularFont(ofSize: 11))
string.append(" ")
string.append(text)
return string
}
}
// MARK: -
extension SafetyNumberConfirmationSheet: UIGestureRecognizerDelegate {
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
switch gestureRecognizer {
case is UITapGestureRecognizer:
let point = gestureRecognizer.location(in: view)
guard !contentView.frame.contains(point) else { return false }
return true
default:
return true
}
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
switch gestureRecognizer {
case is UIPanGestureRecognizer:
return tableView.panGestureRecognizer == otherGestureRecognizer
default:
return false
}
}
}
// MARK: -
private class SafetyNumberConfirmationAnimationController: UIPresentationController {
var backdropView: UIView? {
guard let vc = presentedViewController as? SafetyNumberConfirmationSheet else { return nil }
return vc.backdropView
}
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
backdropView?.backgroundColor = Theme.backdropColor
}
override func presentationTransitionWillBegin() {
guard let containerView = containerView, let backdropView = backdropView else { return }
backdropView.alpha = 0
containerView.addSubview(backdropView)
backdropView.autoPinEdgesToSuperviewEdges()
containerView.layoutIfNeeded()
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
self.backdropView?.alpha = 1
}, completion: nil)
}
override func dismissalTransitionWillBegin() {
presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
self.backdropView?.alpha = 0
}, completion: { _ in
self.backdropView?.removeFromSuperview()
})
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
guard let presentedView = presentedView else { return }
coordinator.animate(alongsideTransition: { _ in
presentedView.frame = self.frameOfPresentedViewInContainerView
presentedView.layoutIfNeeded()
}, completion: nil)
}
}
extension SafetyNumberConfirmationSheet: UIViewControllerTransitioningDelegate {
public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return SafetyNumberConfirmationAnimationController(presentedViewController: presented, presenting: presenting)
}
}