Skip to content

Commit

Permalink
Merge branch 'master' into feature/fix-some-xcode9-warnings
Browse files Browse the repository at this point in the history
  • Loading branch information
AntonPalich authored Mar 10, 2018
2 parents b5e8332 + 0444057 commit 7591f6c
Show file tree
Hide file tree
Showing 32 changed files with 1,479 additions and 1,196 deletions.
4 changes: 4 additions & 0 deletions Chatto/Chatto.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
3565429D203DB99300B29DA1 /* ChatLayoutConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3565429C203DB99300B29DA1 /* ChatLayoutConfiguration.swift */; };
B3B1B0FF1D6B40DF00D1183D /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B1B0FE1D6B40DF00D1183D /* Utils.swift */; };
C31E919A1BFF4CA300339585 /* BaseChatViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31E91991BFF4CA300339585 /* BaseChatViewControllerTests.swift */; };
C321C3961BE78835009803D1 /* CollectionChangesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C321C3951BE78835009803D1 /* CollectionChangesTests.swift */; };
Expand Down Expand Up @@ -48,6 +49,7 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
3565429C203DB99300B29DA1 /* ChatLayoutConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatLayoutConfiguration.swift; sourceTree = "<group>"; };
55E85D821BE390BE001885AD /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B3B1B0FE1D6B40DF00D1183D /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
C31E91991BFF4CA300339585 /* BaseChatViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseChatViewControllerTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -206,6 +208,7 @@
C36281E41BF0F0F0004D6BCE /* BaseChatViewController+Changes.swift */,
C3E904B11BE0509E00C662A2 /* BaseChatViewController+Presenters.swift */,
C38352D01CC6514B006C359C /* BaseChatViewController+AccessoryViewRevealer.swift */,
3565429C203DB99300B29DA1 /* ChatLayoutConfiguration.swift */,
);
path = ChatController;
sourceTree = "<group>";
Expand Down Expand Up @@ -351,6 +354,7 @@
C38352D11CC6514B006C359C /* BaseChatViewController+AccessoryViewRevealer.swift in Sources */,
C342D0BD1C638681008A4605 /* ChatItemCompanion.swift in Sources */,
C36281E71BF0F196004D6BCE /* BaseChatViewController+Scrolling.swift in Sources */,
3565429D203DB99300B29DA1 /* ChatLayoutConfiguration.swift in Sources */,
C36281EB1BF0F62F004D6BCE /* DummyChatItemPresenter.swift in Sources */,
C3C7C3981CAC4BAC00A49929 /* ChatCollectionViewLayout.swift in Sources */,
C3C7C39B1CAC4BAC00A49929 /* KeyboardTracker.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,13 @@ extension BaseChatViewController {
self.collectionView.contentOffset = CGPoint(x: 0, y: self.collectionView.contentOffset.y + diffY)
}

public func scrollViewDidScroll(_ scrollView: UIScrollView) {
open func scrollViewDidScroll(_ scrollView: UIScrollView) {
if self.collectionView.isDragging {
self.autoLoadMoreContentIfNeeded()
}
}

public func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
open func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
self.autoLoadMoreContentIfNeeded()
}

Expand Down
103 changes: 68 additions & 35 deletions Chatto/Source/ChatController/BaseChatViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@ open class BaseChatViewController: UIViewController, UICollectionViewDataSource,

public typealias ChatItemCompanionCollection = ReadOnlyOrderedDictionary<ChatItemCompanion>

open var layoutConfiguration: ChatLayoutConfigurationProtocol = ChatLayoutConfiguration.defaultConfiguration {
didSet {
self.adjustCollectionViewInsets(shouldUpdateContentOffset: false)
}
}

public struct Constants {
public var updatesAnimationDuration: TimeInterval = 0.33
public var defaultContentInsets = UIEdgeInsets(top: 10, left: 0, bottom: 10, right: 0)
public var defaultScrollIndicatorInsets = UIEdgeInsets.zero
public var preferredMaxMessageCount: Int? = 500 // If not nil, will ask data source to reduce number of messages when limit is reached. @see ChatDataSourceDelegateProtocol
public var preferredMaxMessageCountAdjustment: Int = 400 // When the above happens, will ask to adjust with this value. It may be wise for this to be smaller to reduce number of adjustments
public var autoloadingFractionalThreshold: CGFloat = 0.05 // in [0, 1]
Expand Down Expand Up @@ -90,6 +94,7 @@ open class BaseChatViewController: UIViewController, UICollectionViewDataSource,
super.viewDidLoad()
self.addCollectionView()
self.addInputViews()
self.addBottomSpaceView()
self.setupKeyboardTracker()
self.setupTapGestureRecognizer()
}
Expand Down Expand Up @@ -118,8 +123,8 @@ open class BaseChatViewController: UIViewController, UICollectionViewDataSource,

private func addCollectionView() {
self.collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: self.createCollectionViewLayout())
self.collectionView.contentInset = self.constants.defaultContentInsets
self.collectionView.scrollIndicatorInsets = self.constants.defaultScrollIndicatorInsets
self.collectionView.contentInset = self.layoutConfiguration.contentInsets
self.collectionView.scrollIndicatorInsets = self.layoutConfiguration.scrollIndicatorInsets
self.collectionView.alwaysBounceVertical = true
self.collectionView.backgroundColor = UIColor.clear
self.collectionView.keyboardDismissMode = .interactive
Expand Down Expand Up @@ -165,47 +170,69 @@ open class BaseChatViewController: UIViewController, UICollectionViewDataSource,
self.inputContainer.addConstraint(NSLayoutConstraint(item: self.inputContainer, attribute: .bottom, relatedBy: .equal, toItem: inputView, attribute: .bottom, multiplier: 1, constant: 0))
self.inputContainer.addConstraint(NSLayoutConstraint(item: self.inputContainer, attribute: .trailing, relatedBy: .equal, toItem: inputView, attribute: .trailing, multiplier: 1, constant: 0))
}
private func setupInputContainerBottomConstraint() {
// If we have been pushed on nav controller and hidesBottomBarWhenPushed = true, then ignore bottomLayoutMargin
// because it has incorrect value when we actually have a bottom bar (tabbar)
// Also if instance of BaseChatViewController is added as childViewController to another view controller, we had to check all this stuf on parent instance instead of self
let navigatedController: UIViewController
if let parent = self.parent, !(parent is UINavigationController || parent is UITabBarController) {
navigatedController = parent
} else {
navigatedController = self
}

if navigatedController.hidesBottomBarWhenPushed && (navigationController?.viewControllers.count ?? 0) > 1 && navigationController?.viewControllers.last == navigatedController {
self.inputContainerBottomConstraint.constant = 0
} else {
private func addBottomSpaceView() {
self.bottomSpaceView = UIView(frame: CGRect.zero)
self.bottomSpaceView.autoresizingMask = UIViewAutoresizing()
self.bottomSpaceView.translatesAutoresizingMaskIntoConstraints = false
self.bottomSpaceView.backgroundColor = UIColor.white
self.view.addSubview(self.bottomSpaceView)
self.view.addConstraint(NSLayoutConstraint(item: self.bottomSpaceView, attribute: .top, relatedBy: .greaterThanOrEqual, toItem: self.inputContainer, attribute: .bottom, multiplier: 1, constant: 0))
self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: .leading, relatedBy: .equal, toItem: self.bottomSpaceView, attribute: .leading, multiplier: 1, constant: 0))
self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: .trailing, relatedBy: .equal, toItem: self.bottomSpaceView, attribute: .trailing, multiplier: 1, constant: 0))
self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: .bottom, relatedBy: .equal, toItem: self.bottomSpaceView, attribute: .bottom, multiplier: 1, constant: 0))
}

private func setupInputContainerBottomConstraint() {
if #available(iOS 11.0, *) {
self.inputContainerBottomConstraint.constant = self.bottomLayoutGuide.length
} else {
// If we have been pushed on nav controller and hidesBottomBarWhenPushed = true, then ignore bottomLayoutMargin
// because it has incorrect value when we actually have a bottom bar (tabbar)
// Also if instance of BaseChatViewController is added as childViewController to another view controller, we had to check all this stuf on parent instance instead of self
// UPD: Fixed in iOS 11.0
let navigatedController: UIViewController
if let parent = self.parent, !(parent is UINavigationController || parent is UITabBarController) {
navigatedController = parent
} else {
navigatedController = self
}

if navigatedController.hidesBottomBarWhenPushed && (navigationController?.viewControllers.count ?? 0) > 1 && navigationController?.viewControllers.last == navigatedController {
self.inputContainerBottomConstraint.constant = 0
} else {
self.inputContainerBottomConstraint.constant = self.bottomLayoutGuide.length
}
}
}

var isAdjustingInputContainer: Bool = false
open func setupKeyboardTracker() {
let layoutBlock = { [weak self] (bottomMargin: CGFloat) in
let layoutBlock = { [weak self] (bottomMargin: CGFloat, keyboardStatus: KeyboardStatus) in
guard let sSelf = self else { return }
sSelf.isAdjustingInputContainer = true
sSelf.inputContainerBottomConstraint.constant = max(bottomMargin, sSelf.bottomLayoutGuide.length)
sSelf.view.layoutIfNeeded()
sSelf.isAdjustingInputContainer = false
sSelf.handleKeyboardPositionChange(bottomMargin: bottomMargin, keyboardStatus: keyboardStatus)
}
self.keyboardTracker = KeyboardTracker(viewController: self, inputContainer: self.inputContainer, layoutBlock: layoutBlock, notificationCenter: self.notificationCenter)

(self.view as? BaseChatViewControllerViewProtocol)?.bmaInputAccessoryView = self.keyboardTracker?.trackingView

}

open func handleKeyboardPositionChange(bottomMargin: CGFloat, keyboardStatus: KeyboardStatus) {
self.isAdjustingInputContainer = true
self.inputContainerBottomConstraint.constant = max(bottomMargin, self.bottomLayoutGuide.length)
self.view.layoutIfNeeded()
self.isAdjustingInputContainer = false
}

var notificationCenter = NotificationCenter.default
var keyboardTracker: KeyboardTracker!

public private(set) var isFirstLayout: Bool = true
override open func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

self.adjustCollectionViewInsets()
self.adjustCollectionViewInsets(shouldUpdateContentOffset: true)
self.keyboardTracker.adjustTrackingViewSizeIfNeeded()

if self.isFirstLayout {
Expand All @@ -215,21 +242,25 @@ open class BaseChatViewController: UIViewController, UICollectionViewDataSource,
}
}

private func adjustCollectionViewInsets() {
public var allContentFits: Bool {
let inputHeightWithKeyboard = self.view.bounds.height - self.inputContainer.frame.minY
let insetTop = self.topLayoutGuide.length + self.layoutConfiguration.contentInsets.top
let insetBottom = self.layoutConfiguration.contentInsets.bottom + inputHeightWithKeyboard
let availableHeight = self.collectionView.bounds.height - (insetTop + insetBottom)
let contentSize = self.collectionView.collectionViewLayout.collectionViewContentSize
return availableHeight >= contentSize.height
}

private func adjustCollectionViewInsets(shouldUpdateContentOffset: Bool) {
let isInteracting = self.collectionView.panGestureRecognizer.numberOfTouches > 0
let isBouncingAtTop = isInteracting && self.collectionView.contentOffset.y < -self.collectionView.contentInset.top
if isBouncingAtTop { return }

let inputHeightWithKeyboard = self.view.bounds.height - self.inputContainer.frame.minY
let newInsetBottom = self.constants.defaultContentInsets.bottom + inputHeightWithKeyboard
let newInsetBottom = self.layoutConfiguration.contentInsets.bottom + inputHeightWithKeyboard
let insetBottomDiff = newInsetBottom - self.collectionView.contentInset.bottom
let newInsetTop = self.topLayoutGuide.length + self.constants.defaultContentInsets.top

let newInsetTop = self.topLayoutGuide.length + self.layoutConfiguration.contentInsets.top
let contentSize = self.collectionView.collectionViewLayout.collectionViewContentSize
let allContentFits: Bool = {
let availableHeight = self.collectionView.bounds.height - (newInsetTop + newInsetBottom)
return availableHeight >= contentSize.height
}()

let newContentOffsetY: CGFloat = {
let minOffset = -newInsetTop
Expand All @@ -247,14 +278,15 @@ open class BaseChatViewController: UIViewController, UICollectionViewDataSource,

self.collectionView.scrollIndicatorInsets = {
var currentInsets = self.collectionView.scrollIndicatorInsets
currentInsets.bottom = self.constants.defaultScrollIndicatorInsets.bottom + inputHeightWithKeyboard
currentInsets.top = self.topLayoutGuide.length + self.constants.defaultScrollIndicatorInsets.top
currentInsets.bottom = self.layoutConfiguration.scrollIndicatorInsets.bottom + inputHeightWithKeyboard
currentInsets.top = self.topLayoutGuide.length + self.layoutConfiguration.scrollIndicatorInsets.top
return currentInsets
}()

let inputIsAtBottom = self.view.bounds.maxY - self.inputContainer.frame.maxY <= 0
guard shouldUpdateContentOffset else { return }

if allContentFits {
let inputIsAtBottom = self.view.bounds.maxY - self.inputContainer.frame.maxY <= 0
if self.allContentFits {
self.collectionView.contentOffset.y = -self.collectionView.contentInset.top
} else if !isInteracting || inputIsAtBottom {
self.collectionView.contentOffset.y = newContentOffsetY
Expand All @@ -271,6 +303,7 @@ open class BaseChatViewController: UIViewController, UICollectionViewDataSource,
var autoLoadingEnabled: Bool = false
var accessoryViewRevealer: AccessoryViewRevealer!
public private(set) var inputContainer: UIView!
public private(set) var bottomSpaceView: UIView!
var presenterFactory: ChatItemPresenterFactoryProtocol!
let presentersByCell = NSMapTable<UICollectionViewCell, AnyObject>(keyOptions: .weakMemory, valueOptions: .weakMemory)
var visibleCells: [IndexPath: UICollectionViewCell] = [:] // @see visibleCellsAreValid(changes:)
Expand Down
49 changes: 49 additions & 0 deletions Chatto/Source/ChatController/ChatLayoutConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
The MIT License (MIT)
Copyright (c) 2015-present Badoo Trading Limited.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

import UIKit

public protocol ChatLayoutConfigurationProtocol {
var contentInsets: UIEdgeInsets { get }
var scrollIndicatorInsets: UIEdgeInsets { get }
}

public struct ChatLayoutConfiguration: ChatLayoutConfigurationProtocol {
public let contentInsets: UIEdgeInsets
public let scrollIndicatorInsets: UIEdgeInsets

public init(contentInsets: UIEdgeInsets, scrollIndicatorInsets: UIEdgeInsets) {
self.contentInsets = contentInsets
self.scrollIndicatorInsets = scrollIndicatorInsets
}
}

extension ChatLayoutConfiguration {
static var defaultConfiguration: ChatLayoutConfiguration {
let contentInsets = UIEdgeInsets(top: 10, left: 0, bottom: 10, right: 0)
let scrollIndicatorInsets = UIEdgeInsets.zero
return ChatLayoutConfiguration(contentInsets: contentInsets,
scrollIndicatorInsets: scrollIndicatorInsets)
}
}
28 changes: 18 additions & 10 deletions Chatto/Source/ChatController/Collaborators/KeyboardTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@

import Foundation

class KeyboardTracker {

private enum KeyboardStatus {
case hidden
case showing
case shown
}
public enum KeyboardStatus {
case hiding
case hidden
case showing
case shown
}

class KeyboardTracker {
private var keyboardStatus: KeyboardStatus = .hidden
private let view: UIView
var trackingView: UIView {
Expand All @@ -52,7 +52,7 @@ class KeyboardTracker {
var inputContainer: UIView
private var notificationCenter: NotificationCenter

typealias LayoutBlock = (_ bottomMargin: CGFloat) -> Void
typealias LayoutBlock = (_ bottomMargin: CGFloat, _ status: KeyboardStatus) -> Void
private var layoutBlock: LayoutBlock

init(viewController: UIViewController, inputContainer: UIView, layoutBlock: @escaping LayoutBlock, notificationCenter: NotificationCenter) {
Expand All @@ -63,6 +63,7 @@ class KeyboardTracker {
self.notificationCenter.addObserver(self, selector: #selector(KeyboardTracker.keyboardWillShow(_:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
self.notificationCenter.addObserver(self, selector: #selector(KeyboardTracker.keyboardDidShow(_:)), name: NSNotification.Name.UIKeyboardDidShow, object: nil)
self.notificationCenter.addObserver(self, selector: #selector(KeyboardTracker.keyboardWillHide(_:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
self.notificationCenter.addObserver(self, selector: #selector(KeyboardTracker.keyboardDidHide(_:)), name: NSNotification.Name.UIKeyboardDidHide, object: nil)
self.notificationCenter.addObserver(self, selector: #selector(KeyboardTracker.keyboardWillChangeFrame(_:)), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
}

Expand Down Expand Up @@ -105,13 +106,20 @@ class KeyboardTracker {
guard self.isTracking else { return }
let bottomConstraint = self.bottomConstraintFromNotification(notification)
if bottomConstraint == 0 {
self.keyboardStatus = .hidden
self.keyboardStatus = .hiding
self.layoutInputAtBottom()
}
}

@objc
private func keyboardWillHide(_ notification: Notification) {
guard self.isTracking else { return }
self.keyboardStatus = .hiding
self.layoutInputAtBottom()
}

@objc
private func keyboardDidHide(_ notification: Notification) {
guard self.isTracking else { return }
self.keyboardStatus = .hidden
self.layoutInputAtBottom()
Expand Down Expand Up @@ -159,7 +167,7 @@ class KeyboardTracker {

private func layoutInputContainer(withBottomConstraint constraint: CGFloat) {
self.isPerformingForcedLayout = true
self.layoutBlock(constraint)
self.layoutBlock(constraint, self.keyboardStatus)
self.isPerformingForcedLayout = false
}
}
Expand Down
Loading

0 comments on commit 7591f6c

Please sign in to comment.