Skip to content
This repository has been archived by the owner on Jun 7, 2020. It is now read-only.

[CHORE] Refactor scroll to bottom #2320

Merged
merged 10 commits into from
Nov 16, 2018
192 changes: 147 additions & 45 deletions Rocket.Chat/Controllers/Chat/MessagesViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ final class MessagesViewController: RocketChatViewController {
}

private let buttonScrollToBottomSize = CGFloat(70)
var keyboardHeight: CGFloat = 0
var buttonScrollToBottomConstraint: NSLayoutConstraint!
var buttonScrollToBottomLayerY: CGFloat {
return -composerView.layer.bounds.height - buttonScrollToBottomSize / 2 - collectionView.layoutMargins.top - keyboardHeight

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line Length Violation: Line should be 120 characters or less: currently 131 characters (line_length)

}
var buttonScrollToBottomY: CGFloat {
return -composerView.intrinsicContentSize.height - collectionView.layoutMargins.top - keyboardHeight
}

lazy var buttonScrollToBottom: UIButton = {
let button = UIButton()
button.frame = CGRect(x: .greatestFiniteMagnitude, y: .greatestFiniteMagnitude, width: buttonScrollToBottomSize, height: buttonScrollToBottomSize)
Expand All @@ -62,43 +71,8 @@ final class MessagesViewController: RocketChatViewController {
return button
}()

var scrollToBottomButtonIsVisible: Bool = false {
didSet {
guard oldValue != scrollToBottomButtonIsVisible else {
return
}

func animates(_ animations: @escaping VoidCompletion) {
UIView.animate(withDuration: 0.15, delay: 0, options: UIView.AnimationOptions(rawValue: 7 << 16), animations: {
animations()
}, completion: nil)
}

if self.scrollToBottomButtonIsVisible {
if buttonScrollToBottom.superview == nil {
view.addSubview(buttonScrollToBottom)
}

var frame = buttonScrollToBottom.frame
frame.origin.x = collectionView.frame.width - buttonScrollToBottomSize - view.layoutMargins.right
frame.origin.y = collectionView.frame.origin.y + collectionView.frame.height - buttonScrollToBottomSize - collectionView.layoutMargins.top - composerView.frame.height

animates({
self.buttonScrollToBottom.frame = frame
self.buttonScrollToBottom.alpha = 1
})
} else {
var frame = buttonScrollToBottom.frame
frame.origin.x = collectionView.frame.width - buttonScrollToBottomSize - view.layoutMargins.right
frame.origin.y = collectionView.frame.origin.y + collectionView.frame.height

animates({
self.buttonScrollToBottom.frame = frame
self.buttonScrollToBottom.alpha = 0
})
}
}
}
var isScrollingToBottom: Bool = false
var scrollToBottomButtonIsVisible: Bool = false

lazy var screenSize = view.frame.size
var isInLandscape: Bool {
Expand Down Expand Up @@ -143,7 +117,21 @@ final class MessagesViewController: RocketChatViewController {
collectionView.register(MessageURLCell.nib, forCellWithReuseIdentifier: MessageURLCell.identifier)
collectionView.register(MessageActionsCell.nib, forCellWithReuseIdentifier: MessageActionsCell.identifier)

view.bringSubviewToFront(buttonScrollToBottom)
setupScrollToBottom()

NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillShow(_:)),
name: UIResponder.keyboardWillShowNotification,
object: nil
)

NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillHide(_:)),
name: UIResponder.keyboardWillHideNotification,
object: nil
)

dataUpdateDelegate = self
viewModel.controllerContext = self
Expand Down Expand Up @@ -185,8 +173,7 @@ final class MessagesViewController: RocketChatViewController {

if shouldResetScrollToBottom {
if self.scrollToBottomButtonIsVisible {
self.scrollToBottomButtonIsVisible = false
self.scrollToBottomButtonIsVisible = true
self.showScrollToBottom(forceUpdate: true)
}
}

Expand All @@ -211,6 +198,113 @@ final class MessagesViewController: RocketChatViewController {
}
}

// MARK: Scroll to Bottom

func setupScrollToBottom() {
buttonScrollToBottom.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(buttonScrollToBottom)
view.bringSubviewToFront(buttonScrollToBottom)
buttonScrollToBottomConstraint = buttonScrollToBottom.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 50)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line Length Violation: Line should be 120 characters or less: currently 127 characters (line_length)

NSLayoutConstraint.activate([
buttonScrollToBottom.heightAnchor.constraint(equalToConstant: buttonScrollToBottomSize),
buttonScrollToBottom.widthAnchor.constraint(equalToConstant: buttonScrollToBottomSize),
buttonScrollToBottomConstraint,
buttonScrollToBottom.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -15)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line Length Violation: Line should be 120 characters or less: currently 123 characters (line_length)

])
}

func showScrollToBottom(forceUpdate: Bool = false) {
let isScrollToBottomVisible = forceUpdate ? false : scrollToBottomButtonIsVisible
guard !isScrollToBottomVisible, !isScrollingToBottom else {
return
}

isScrollingToBottom = true
CATransaction.begin()
CATransaction.setAnimationDuration(0.25)
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut))
CATransaction.setCompletionBlock {
self.buttonScrollToBottomConstraint.constant = self.buttonScrollToBottomY
self.scrollToBottomButtonIsVisible = true
self.isScrollingToBottom = false
}

var fromPosition = buttonScrollToBottom.layer.position
fromPosition.y -= keyboardHeight
var position = buttonScrollToBottom.layer.position
position.y = collectionView.bounds.height + buttonScrollToBottomLayerY

let positionAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.position))
positionAnimation.fromValue = fromPosition
positionAnimation.toValue = position

buttonScrollToBottom.layer.position = position
buttonScrollToBottom.layer.add(positionAnimation, forKey: #keyPath(CALayer.position))

let alphaAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
alphaAnimation.fromValue = 0
alphaAnimation.toValue = 1

buttonScrollToBottom.layer.opacity = 1
buttonScrollToBottom.layer.add(alphaAnimation, forKey: #keyPath(CALayer.opacity))

CATransaction.commit()
}

func hideScrollToBottom(forceUpdate: Bool = false) {
let isScrollToBottomVisible = forceUpdate ? true : scrollToBottomButtonIsVisible
guard isScrollToBottomVisible, !isScrollingToBottom else {
return
}

isScrollingToBottom = true
CATransaction.begin()
CATransaction.setAnimationDuration(0.25)
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut))
CATransaction.setCompletionBlock {
self.buttonScrollToBottomConstraint.constant = 200
self.scrollToBottomButtonIsVisible = false
self.isScrollingToBottom = false
}

var position = buttonScrollToBottom.layer.position
position.y = collectionView.bounds.height + 200

let positionAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.position))
positionAnimation.fromValue = buttonScrollToBottom.layer.position
positionAnimation.toValue = position

buttonScrollToBottom.layer.position = position
buttonScrollToBottom.layer.add(positionAnimation, forKey: #keyPath(CALayer.position))

let alphaAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
alphaAnimation.fromValue = 1
alphaAnimation.toValue = 0

buttonScrollToBottom.layer.opacity = 0
buttonScrollToBottom.layer.add(alphaAnimation, forKey: #keyPath(CALayer.opacity))

CATransaction.commit()
}

override func keyboardWillShow(_ notification: Notification) {
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
let keyboardRectangle = keyboardFrame.cgRectValue
keyboardHeight = keyboardRectangle.height - composerView.intrinsicContentSize.height

if scrollToBottomButtonIsVisible {
showScrollToBottom(forceUpdate: true)
}
}
}

override func keyboardWillHide(_ notification: Notification) {
keyboardHeight = 0
if scrollToBottomButtonIsVisible {
showScrollToBottom(forceUpdate: true)
}
}

// MARK: Pagination

func loadNextPageIfNeeded() {
Expand Down Expand Up @@ -240,17 +334,22 @@ final class MessagesViewController: RocketChatViewController {
}

@objc internal func scrollToBottom(_ animated: Bool = false) {
let offset = CGPoint(x: 0, y: -composerView.frame.height)
let offset = CGPoint(x: 0, y: -composerView.frame.height - keyboardHeight)
collectionView.setContentOffset(offset, animated: animated)
scrollToBottomButtonIsVisible = false
hideScrollToBottom()
}

internal func resetScrollToBottomButtonPosition() {
scrollToBottomButtonIsVisible = !chatLogIsAtBottom()
guard !isScrollingToBottom else { return }
if !chatLogIsAtBottom() {
showScrollToBottom()
} else {
hideScrollToBottom()
}
}

private func chatLogIsAtBottom() -> Bool {
return collectionView.contentOffset.y <= -composerView.frame.height
return (collectionView.contentOffset.y + keyboardHeight) <= -composerView.frame.height
}

func openURL(url: URL) {
Expand Down Expand Up @@ -332,7 +431,10 @@ extension MessagesViewController: ChatDataUpdateDelegate {
extension MessagesViewController {

override func scrollViewDidScroll(_ scrollView: UIScrollView) {
resetScrollToBottomButtonPosition()
if scrollView.isDragging {
resetScrollToBottomButtonPosition()
}

loadNextPageIfNeeded()
}

Expand Down