From 141103226227c72325636a5603c6de7580ee1009 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 20 Dec 2024 10:43:38 -0300 Subject: [PATCH] iOS AI Chat - Improve show/dismiss animation (#3741) Task/Issue URL: https://app.asana.com/0/1204167627774280/1208991512395325/f **Description**: Allow user to drag the AI Chat view controller down by the title bar --- .../24px/AIChat-24.imageset/AIChat-24.pdf | Bin 1590 -> 4283 bytes DuckDuckGo/MainViewController.swift | 9 +- ...ndedPageSheetContainerViewController.swift | 135 +++++++----------- ...RoundedPageSheetPresentationAnimator.swift | 58 +++++++- DuckDuckGo/UserText.swift | 1 - DuckDuckGo/en.lproj/Localizable.strings | 3 - LocalPackages/AIChat/Package.swift | 6 + .../Public API/AIChatViewController.swift | 32 ++++- .../AIChat/Sources/AIChat/TitleBarView.swift | 90 ++++++++++++ .../AIChat/Sources/AIChat/UserText.swift | 2 +- 10 files changed, 236 insertions(+), 100 deletions(-) create mode 100644 LocalPackages/AIChat/Sources/AIChat/TitleBarView.swift diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/AIChat-24.imageset/AIChat-24.pdf b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/AIChat-24.imageset/AIChat-24.pdf index b806da0f5968053e90bf582df2ee923e9bbd50b3..731eeaddaa204972e4e3d503a098631dd39b7281 100644 GIT binary patch delta 3925 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 literal 1590 zcmY!laBkm(vTRHc6J~)rRJr8Ji%pz;v2WjoRZWc1%0<1ppRTqlk-zjA+Fc= zNzF?y$xtu`Dh~n*0Zk|_DN0Su<*H!ZI`wp(h@pV%_rIc>-MSuEP2X;FMCwP27~>KZ zp{^s}i|@|dXnT@Paj7Zy`ONFD!*168{`h--{O|AU=ii@iS93e>`ToC|zxUp)TXea9 zd9Ro4yx_e$dB?K$JU-pDDJ7_D{tPyBf?^i0aWy8`>+EvpsK3Ys+ zaekrvE#dbyF3&4^ml`4;{JeF)%k2TDd)wvPIw@_kZ=#ATlh5uIh-dpW$HruSNs7M& zf8D&x6SWe%cJ10_kfC?jHo6w5C{0|>$n*Mh!*LxEg&FpHYCb~+xFJ(rV*pa*f57*VR6z(_enVqbtYU9h1 zxY=>aJag7nV$J!wXO2na%02vL?K4-GqweI4RfozJ#%<8w{54LxI7`NCT0yO3=$qMV z;=QC8^^mRjk}LTKrPf@@_jvnReM9p!wuRLa>!U6%tA27}Re_OFj+|K2^ck%OO6E!5 ztrF6&zN+3jVYY_*dEVc#!n!Wy35V`jnXGI0&hArTpAc5`O5o9!Rf#*wF3&djTHM`G zv7jStnag(esHxE!HnSete4U!i#&q`Z?R%U&|B5+l?$e67NV-P<2dd%=DxqmTK@{u-HE zzh+k5J$UcN?(*ZZAH;L~mob2H3pBaI@(m~>KuQ5~69af|GJ%VN$_B^0y!?`4h3Hsl z9tx=}NLA1eNKA)iD&N$U%tWXB3WaE3c>yXdfaL|8VPpamLa<;a1*I0}mlh?b7At^C zBT$9}0)6NFypq&BppBqR4HFDVECPxtm_mh+vOY)%QIL6OrU0F)0P;bQ0?Z!g{8FG^ zLk#y9Ly7=cV3?tp1oB`p+(MAUARc#4EJ@7CPe;{SQIwj-WuRcr1@S&O2+T}PjZGDR z!cbrc3>C0M9!$u{5NIWekfDJIx++5hGhjfVsxkr^j3#7kU 0.3 || velocity.y > 1000 + if shouldDismiss { + interactiveDismissalTransition?.finish() + } else { + interactiveDismissalTransition?.cancel() + UIView.animate(withDuration: 0.2, animations: { + self.view.transform = .identity + }) + } + isInteractiveDismissal = false + interactiveDismissalTransition = nil + default: + break + } + } + + private func setupBackgroundView() { + view.addSubview(backgroundView) + + backgroundView.backgroundColor = .black + backgroundView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - titleBarView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - titleBarView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - titleBarView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - titleBarView.heightAnchor.constraint(equalToConstant: 44) + backgroundView.topAnchor.constraint(equalTo: view.topAnchor), + backgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor) ]) } @@ -84,7 +108,7 @@ final class RoundedPageSheetContainerViewController: UIViewController { contentViewController.view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - contentViewController.view.topAnchor.constraint(equalTo: titleBarView.bottomAnchor), // Below the title bar + contentViewController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), contentViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), contentViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), contentViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor) @@ -95,6 +119,9 @@ final class RoundedPageSheetContainerViewController: UIViewController { contentViewController.view.clipsToBounds = true contentViewController.didMove(toParent: self) + + let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) + contentViewController.view.addGestureRecognizer(panGesture) } @objc func closeController() { @@ -110,70 +137,8 @@ extension RoundedPageSheetContainerViewController: UIViewControllerTransitioning func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { return RoundedPageSheetDismissalAnimator() } -} - -final private class TitleBarView: UIView { - private let imageView: UIImageView - private let titleLabel: UILabel - private let closeButton: UIButton - - init(logoImage: UIImage?, title: String, closeAction: @escaping () -> Void) { - imageView = UIImageView(image: logoImage) - titleLabel = UILabel() - closeButton = UIButton(type: .system) - - super.init(frame: .zero) - - setupView(title: title, closeAction: closeAction) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupView(title: String, closeAction: @escaping () -> Void) { - backgroundColor = .clear - - imageView.contentMode = .scaleAspectFit - imageView.translatesAutoresizingMaskIntoConstraints = false - - let imageSize: CGFloat = 28 - NSLayoutConstraint.activate([ - imageView.widthAnchor.constraint(equalToConstant: imageSize), - imageView.heightAnchor.constraint(equalToConstant: imageSize) - ]) - - titleLabel.text = title - titleLabel.font = UIFont.systemFont(ofSize: 17, weight: .semibold) - titleLabel.textColor = .white - titleLabel.translatesAutoresizingMaskIntoConstraints = false - - closeButton.setImage(UIImage(named: "Close-24"), for: .normal) - closeButton.tintColor = .white - closeButton.translatesAutoresizingMaskIntoConstraints = false - closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) - - addSubview(imageView) - addSubview(titleLabel) - addSubview(closeButton) - - NSLayoutConstraint.activate([ - imageView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 16), - imageView.centerYAnchor.constraint(equalTo: centerYAnchor), - - titleLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 8), - titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor), - - closeButton.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -16), - closeButton.centerYAnchor.constraint(equalTo: centerYAnchor) - ]) - - self.closeAction = closeAction - } - - private var closeAction: (() -> Void)? - @objc private func closeButtonTapped() { - closeAction?() + func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { + return isInteractiveDismissal ? interactiveDismissalTransition : nil } } diff --git a/DuckDuckGo/RoundedPageContainer/RoundedPageSheetPresentationAnimator.swift b/DuckDuckGo/RoundedPageContainer/RoundedPageSheetPresentationAnimator.swift index 8f5f584591..85cd7d8787 100644 --- a/DuckDuckGo/RoundedPageContainer/RoundedPageSheetPresentationAnimator.swift +++ b/DuckDuckGo/RoundedPageContainer/RoundedPageSheetPresentationAnimator.swift @@ -21,9 +21,11 @@ import UIKit enum AnimatorConstants { static let duration: TimeInterval = 0.4 + static let springDamping: CGFloat = 0.9 + static let springVelocity: CGFloat = 0.5 } -class RoundedPageSheetPresentationAnimator: NSObject, UIViewControllerAnimatedTransitioning { +final class RoundedPageSheetPresentationAnimator: NSObject, UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return AnimatorConstants.duration } @@ -39,7 +41,12 @@ class RoundedPageSheetPresentationAnimator: NSObject, UIViewControllerAnimatedTr toView.alpha = 0 contentView.transform = CGAffineTransform(translationX: 0, y: containerView.bounds.height) - UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: { + UIView.animate(withDuration: AnimatorConstants.duration, + delay: 0, + usingSpringWithDamping: AnimatorConstants.springDamping, + initialSpringVelocity: AnimatorConstants.springVelocity, + options: .curveEaseInOut, + animations: { toView.alpha = 1 contentView.transform = .identity }, completion: { finished in @@ -47,8 +54,9 @@ class RoundedPageSheetPresentationAnimator: NSObject, UIViewControllerAnimatedTr }) } } - class RoundedPageSheetDismissalAnimator: NSObject, UIViewControllerAnimatedTransitioning { + private var animator: UIViewPropertyAnimator? + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return AnimatorConstants.duration } @@ -58,14 +66,54 @@ class RoundedPageSheetDismissalAnimator: NSObject, UIViewControllerAnimatedTrans let fromView = fromViewController.view, let contentView = fromViewController.contentViewController.view else { return } + let fromBackgroundView = fromViewController.backgroundView let containerView = transitionContext.containerView - UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: { - fromView.alpha = 0 + UIView.animate(withDuration: AnimatorConstants.duration, + delay: 0, + usingSpringWithDamping: AnimatorConstants.springDamping, + initialSpringVelocity: AnimatorConstants.springVelocity, + options: .curveEaseInOut, + animations: { + fromBackgroundView.alpha = 0 contentView.transform = CGAffineTransform(translationX: 0, y: containerView.bounds.height) }, completion: { finished in fromView.removeFromSuperview() transitionContext.completeTransition(finished) }) } + + func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating { + if let existingAnimator = animator { + return existingAnimator + } + + guard let fromViewController = transitionContext.viewController(forKey: .from) as? RoundedPageSheetContainerViewController, + let fromView = fromViewController.view, + let contentView = fromViewController.contentViewController.view else { + fatalError("Invalid view controller setup") + } + + let containerView = transitionContext.containerView + let fromBackgroundView = fromViewController.backgroundView + + let animator = UIViewPropertyAnimator(duration: AnimatorConstants.duration, + dampingRatio: AnimatorConstants.springDamping) { + fromBackgroundView.alpha = 0 + contentView.transform = CGAffineTransform(translationX: 0, y: containerView.bounds.height) + } + + animator.addCompletion { position in + switch position { + case .end: + fromView.removeFromSuperview() + transitionContext.completeTransition(true) + default: + transitionContext.completeTransition(false) + } + } + + self.animator = animator + return animator + } } diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 86a2337975..c6e7a5722f 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1352,7 +1352,6 @@ But if you *do* want a peek under the hood, you can find more information about static let duckPlayerContingencyMessageCTA = NSLocalizedString("duck-player.video-contingency-cta", value: "Learn More", comment: "Button for the message explaining to the user that Duck Player is not available so the user can learn more") // MARK: - AI Chat - public static let aiChatTitle = NSLocalizedString("aichat.title", value: "DuckDuckGo AI Chat", comment: "Title for DuckDuckGo AI Chat. Should not be translated") public static let aiChatFeatureName = NSLocalizedString("aichat.settings.title", value: "AI Chat", comment: "Settings screen cell text for AI Chat settings") public static let aiChatSettingsEnableFooter = NSLocalizedString("aichat.settings.enable.footer", value: "Turning this off will hide the AI Chat feature in the DuckDuckGo app.", comment: "Footer text for AI Chat settings") diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index d1bf6015ea..022fc631d0 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -158,9 +158,6 @@ /* Settings screen cell text for AI Chat settings */ "aichat.settings.title" = "AI Chat"; -/* Title for DuckDuckGo AI Chat. Should not be translated */ -"aichat.title" = "DuckDuckGo AI Chat"; - /* No comment provided by engineer. */ "alert.message.bookmarkAll" = "Existing bookmarks will not be duplicated."; diff --git a/LocalPackages/AIChat/Package.swift b/LocalPackages/AIChat/Package.swift index 478b4b4705..29f3abc3d5 100644 --- a/LocalPackages/AIChat/Package.swift +++ b/LocalPackages/AIChat/Package.swift @@ -13,9 +13,15 @@ let package = Package( targets: ["AIChat"] ), ], + dependencies: [ + .package(url: "https://github.com/duckduckgo/DesignResourcesKit", exact: "3.3.0") + ], targets: [ .target( name: "AIChat", + dependencies: [ + "DesignResourcesKit", + ], resources: [ .process("Resources/Assets.xcassets") ] diff --git a/LocalPackages/AIChat/Sources/AIChat/Public API/AIChatViewController.swift b/LocalPackages/AIChat/Sources/AIChat/Public API/AIChatViewController.swift index 31d394969c..2d4bddede5 100644 --- a/LocalPackages/AIChat/Sources/AIChat/Public API/AIChatViewController.swift +++ b/LocalPackages/AIChat/Sources/AIChat/Public API/AIChatViewController.swift @@ -16,6 +16,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // + import UIKit import Combine import WebKit @@ -28,6 +29,11 @@ public protocol AIChatViewControllerDelegate: AnyObject { /// - viewController: The `AIChatViewController` instance making the request. /// - url: The `URL` that is requested to be loaded. func aiChatViewController(_ viewController: AIChatViewController, didRequestToLoad url: URL) + + /// Tells the delegate that the `AIChatViewController` has finished its task. + /// + /// - Parameter viewController: The `AIChatViewController` instance that has finished. + func aiChatViewControllerDidFinish(_ viewController: AIChatViewController) } public final class AIChatViewController: UIViewController { @@ -35,6 +41,17 @@ public final class AIChatViewController: UIViewController { private let chatModel: AIChatViewModeling private var webViewController: AIChatWebViewController? + private lazy var titleBarView: TitleBarView = { + let title = UserText.aiChatTitle + + let titleBarView = TitleBarView(title: UserText.aiChatTitle) { [weak self] in + guard let self = self else { return } + self.delegate?.aiChatViewControllerDidFinish(self) + } + return titleBarView + }() + + /// Initializes a new instance of `AIChatViewController` with the specified remote settings and web view configuration. /// /// - Parameters: @@ -62,6 +79,7 @@ extension AIChatViewController { public override func viewDidLoad() { super.viewDidLoad() self.view.backgroundColor = .black + setupTitleBar() } public override func viewWillAppear(_ animated: Bool) { @@ -87,6 +105,18 @@ extension AIChatViewController { // MARK: - Views Setup extension AIChatViewController { + private func setupTitleBar() { + view.addSubview(titleBarView) + titleBarView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + titleBarView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + titleBarView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + titleBarView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + titleBarView.heightAnchor.constraint(equalToConstant: 68) + ]) + } + private func addWebViewController() { guard webViewController == nil else { return } @@ -99,7 +129,7 @@ extension AIChatViewController { viewController.view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - viewController.view.topAnchor.constraint(equalTo: view.topAnchor), + viewController.view.topAnchor.constraint(equalTo: titleBarView.bottomAnchor), viewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), viewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), viewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor) diff --git a/LocalPackages/AIChat/Sources/AIChat/TitleBarView.swift b/LocalPackages/AIChat/Sources/AIChat/TitleBarView.swift new file mode 100644 index 0000000000..16f8ad4b5a --- /dev/null +++ b/LocalPackages/AIChat/Sources/AIChat/TitleBarView.swift @@ -0,0 +1,90 @@ +// +// TitleBarView.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +import UIKit +import DesignResourcesKit + +final class TitleBarView: UIView { + private let titleLabel: UILabel + private let closeButton: UIButton + private let handleBar: UIView + private var closeAction: (() -> Void)? + + init(title: String, closeAction: @escaping () -> Void) { + titleLabel = UILabel() + closeButton = UIButton(type: .system) + handleBar = UIView() + + self.closeAction = closeAction + + super.init(frame: .zero) + + setupView(title: title) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView(title: String) { + backgroundColor = .webViewBackgroundColor + + handleBar.backgroundColor = UIColor(white: 0.5, alpha: 0.5) + handleBar.layer.cornerRadius = Constants.handlebarHeight / 2 + handleBar.translatesAutoresizingMaskIntoConstraints = false + + titleLabel.text = title + titleLabel.font = UIFont.systemFont(ofSize: 17, weight: .semibold) + titleLabel.textColor = UIColor(designSystemColor: .textPrimary) + titleLabel.translatesAutoresizingMaskIntoConstraints = false + + closeButton.setImage(UIImage(named: "Close-24"), for: .normal) + closeButton.tintColor = UIColor(designSystemColor: .icons) + closeButton.translatesAutoresizingMaskIntoConstraints = false + closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) + + addSubview(handleBar) + addSubview(titleLabel) + addSubview(closeButton) + + NSLayoutConstraint.activate([ + handleBar.topAnchor.constraint(equalTo: topAnchor, constant: 8), + handleBar.centerXAnchor.constraint(equalTo: centerXAnchor), + handleBar.widthAnchor.constraint(equalToConstant: Constants.handlebarWidth), + handleBar.heightAnchor.constraint(equalToConstant: Constants.handlebarHeight), + + titleLabel.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor), + titleLabel.centerYAnchor.constraint(equalTo: handleBar.bottomAnchor, constant: 30), + + closeButton.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -8), + closeButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + closeButton.widthAnchor.constraint(equalToConstant: Constants.closeButtonSize), + closeButton.heightAnchor.constraint(equalToConstant: Constants.closeButtonSize) + ]) + } + + @objc private func closeButtonTapped() { + closeAction?() + } +} + +private enum Constants { + static let closeButtonSize: CGFloat = 44 + static let handlebarHeight: CGFloat = 3 + static let handlebarWidth: CGFloat = 42 +} diff --git a/LocalPackages/AIChat/Sources/AIChat/UserText.swift b/LocalPackages/AIChat/Sources/AIChat/UserText.swift index d33e745d2d..e88c31793c 100644 --- a/LocalPackages/AIChat/Sources/AIChat/UserText.swift +++ b/LocalPackages/AIChat/Sources/AIChat/UserText.swift @@ -20,5 +20,5 @@ import Foundation public struct UserText { - public static let aiChatTitle = NSLocalizedString("aichat.title", value: "DuckDuckGo AI Chat", comment: "Title for DuckDuckGo AI Chat. Should not be translated") + public static let aiChatTitle = NSLocalizedString("aichat.title", value: "Duck.ai", comment: "Title for DuckDuckGo AI Chat. Should not be translated") }