Skip to content

Commit

Permalink
AI Chat transition animation (#3682)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1204167627774280/1208906546735883/f

**Description**:
Add new transition for AI Chat UI
  • Loading branch information
Bunn authored Dec 5, 2024
1 parent 75dff24 commit 684a4d9
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 68 deletions.
16 changes: 16 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@
3170048227A9504F00C03F35 /* DownloadMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3170048127A9504F00C03F35 /* DownloadMocks.swift */; };
317045C02858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317045BF2858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift */; };
317CA3432CFF82E100F88848 /* SettingsAIChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317CA3422CFF82DB00F88848 /* SettingsAIChatView.swift */; };
317DF6082D01E7B900DE0145 /* RoundedPageSheetContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317DF6072D01E7B900DE0145 /* RoundedPageSheetContainerViewController.swift */; };
317DF60B2D01E7D600DE0145 /* RoundedPageSheetPresentationAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317DF60A2D01E7D600DE0145 /* RoundedPageSheetPresentationAnimator.swift */; };
317F5F982C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317F5F972C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift */; };
31860A5B2C57ED2D005561F5 /* DuckPlayerStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31860A5A2C57ED2D005561F5 /* DuckPlayerStorage.swift */; };
31951E8E2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31951E8D2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift */; };
Expand Down Expand Up @@ -1535,6 +1537,8 @@
317045BF2858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillInterfaceEmailTruncatorTests.swift; sourceTree = "<group>"; };
31794BFF2821DFB600F18633 /* DuckUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = DuckUI; sourceTree = "<group>"; };
317CA3422CFF82DB00F88848 /* SettingsAIChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAIChatView.swift; sourceTree = "<group>"; };
317DF6072D01E7B900DE0145 /* RoundedPageSheetContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedPageSheetContainerViewController.swift; sourceTree = "<group>"; };
317DF60A2D01E7D600DE0145 /* RoundedPageSheetPresentationAnimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedPageSheetPresentationAnimator.swift; sourceTree = "<group>"; };
317F5F972C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceAdPostbackStorage.swift; sourceTree = "<group>"; };
31860A5A2C57ED2D005561F5 /* DuckPlayerStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerStorage.swift; sourceTree = "<group>"; };
31951E8D2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillLoginDetailsHeaderView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3710,6 +3714,15 @@
name = Utils;
sourceTree = "<group>";
};
317DF6092D01E7C400DE0145 /* RoundedPageContainer */ = {
isa = PBXGroup;
children = (
317DF60A2D01E7D600DE0145 /* RoundedPageSheetPresentationAnimator.swift */,
317DF6072D01E7B900DE0145 /* RoundedPageSheetContainerViewController.swift */,
);
path = RoundedPageContainer;
sourceTree = "<group>";
};
31951E9328230D8900CAF535 /* Shared */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -6508,6 +6521,7 @@
F1D796ED1E7AE4090019D451 /* UserInterface */ = {
isa = PBXGroup;
children = (
317DF6092D01E7C400DE0145 /* RoundedPageContainer */,
859872221F5743AF00041CB8 /* FireAnimation */,
1E162603296840790004127F /* SwiftUI */,
982686AC2600C0850011A8D6 /* ActionMessageView.swift */,
Expand Down Expand Up @@ -7954,6 +7968,7 @@
8598D2E32CEB98B500C45685 /* FaviconUserScript.swift in Sources */,
8598D2E42CEB98B500C45685 /* FaviconSourcesProvider.swift in Sources */,
BD862E052B30DB250073E2EE /* VPNFeedbackCategory.swift in Sources */,
317DF60B2D01E7D600DE0145 /* RoundedPageSheetPresentationAnimator.swift in Sources */,
85AE6690209724120014CF04 /* NotificationView.swift in Sources */,
BDE91CE02C6515420005CB74 /* UnifiedFeedbackFormViewModel.swift in Sources */,
1EA51376286596A000493C6A /* PrivacyIconLogic.swift in Sources */,
Expand Down Expand Up @@ -8040,6 +8055,7 @@
BD862E072B30F5E30073E2EE /* VPNFeedbackSender.swift in Sources */,
AA4D6A6A23DB87B1007E8790 /* AppIconManager.swift in Sources */,
8563A03C1F9288D600F04442 /* BrowserChromeManager.swift in Sources */,
317DF6082D01E7B900DE0145 /* RoundedPageSheetContainerViewController.swift in Sources */,
980891A32237146B00313A70 /* Feedback.swift in Sources */,
F1D796F01E7B07610019D451 /* BookmarksViewControllerCells.swift in Sources */,
9F9EE4D42C37BB1300D4118E /* OnboardingView+Landing.swift in Sources */,
Expand Down
15 changes: 11 additions & 4 deletions DuckDuckGo/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,14 @@ class MainViewController: UIViewController {

var appDidFinishLaunchingStartTime: CFAbsoluteTime?

private lazy var aiChatNavigationController: UINavigationController = {
private lazy var aiChatViewController: AIChatViewController = {
let settings = AIChatSettings(privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager,
internalUserDecider: AppDependencyProvider.shared.internalUserDecider)
let aiChatViewController = AIChatViewController(settings: settings,
webViewConfiguration: WKWebViewConfiguration.persistent(),
pixelHandler: AIChatPixelHandler())
aiChatViewController.delegate = self
return UINavigationController(rootViewController: aiChatViewController)
return aiChatViewController
}()

init(
Expand Down Expand Up @@ -2359,8 +2359,15 @@ extension MainViewController: TabDelegate {
}

func tabDidRequestAIChat(tab: TabViewController) {
aiChatNavigationController.modalPresentationStyle = .fullScreen
tab.present(aiChatNavigationController, animated: true, completion: nil)
let logoImage = UIImage(named: "Logo")
let title = UserText.aiChatTitle

let roundedPageSheet = RoundedPageSheetContainerViewController(
contentViewController: aiChatViewController,
logoImage: logoImage,
title: title)

present(roundedPageSheet, animated: true, completion: nil)
}

func tabDidRequestBookmarks(tab: TabViewController) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
//
// RoundedPageSheetContainerViewController.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

final class RoundedPageSheetContainerViewController: UIViewController {
let contentViewController: UIViewController
private let logoImage: UIImage?
private let titleText: String

private lazy var titleBarView: TitleBarView = {
let titleBarView = TitleBarView(logoImage: logoImage, title: titleText) { [weak self] in
self?.closeController()
}
return titleBarView
}()

init(contentViewController: UIViewController, logoImage: UIImage?, title: String) {
self.contentViewController = contentViewController
self.logoImage = logoImage
self.titleText = title
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .custom

transitioningDelegate = self
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black

setupTitleBar()
setupContentViewController()
}

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)

coordinator.animate(alongsideTransition: { _ in
// Update layout or constraints here
}, completion: nil)
}


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: 44)
])
}

private func setupContentViewController() {
addChild(contentViewController)
view.addSubview(contentViewController.view)
contentViewController.view.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
contentViewController.view.topAnchor.constraint(equalTo: titleBarView.bottomAnchor), // Below the title bar
contentViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
contentViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
contentViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])

contentViewController.view.layer.cornerRadius = 20
contentViewController.view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
contentViewController.view.clipsToBounds = true

contentViewController.didMove(toParent: self)
}

@objc func closeController() {
dismiss(animated: true, completion: nil)
}
}

extension RoundedPageSheetContainerViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return RoundedPageSheetPresentationAnimator()
}

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?()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//
// RoundedPageSheetPresentationAnimator.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

enum AnimatorConstants {
static let duration: TimeInterval = 0.4
}

class RoundedPageSheetPresentationAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return AnimatorConstants.duration
}

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toViewController = transitionContext.viewController(forKey: .to) as? RoundedPageSheetContainerViewController,
let toView = toViewController.view,
let contentView = toViewController.contentViewController.view else { return }

let containerView = transitionContext.containerView

containerView.addSubview(toView)
toView.alpha = 0
contentView.transform = CGAffineTransform(translationX: 0, y: containerView.bounds.height)

UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
toView.alpha = 1
contentView.transform = .identity
}, completion: { finished in
transitionContext.completeTransition(finished)
})
}
}

class RoundedPageSheetDismissalAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return AnimatorConstants.duration
}

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromViewController = transitionContext.viewController(forKey: .from) as? RoundedPageSheetContainerViewController,
let fromView = fromViewController.view,
let contentView = fromViewController.contentViewController.view else { return }

let containerView = transitionContext.containerView

UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
fromView.alpha = 0
contentView.transform = CGAffineTransform(translationX: 0, y: containerView.bounds.height)
}, completion: { finished in
fromView.removeFromSuperview()
transitionContext.completeTransition(finished)
})
}
}
Loading

0 comments on commit 684a4d9

Please sign in to comment.