Skip to content

Commit

Permalink
Merge pull request #538 from alaija/bugfix/force_unwrap_collectionview
Browse files Browse the repository at this point in the history
Great job, @alaija
  • Loading branch information
AntonPalich authored Dec 10, 2018
2 parents baf2a76 + 40f4ead commit 1016906
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 98 deletions.
41 changes: 26 additions & 15 deletions Chatto/Source/ChatController/BaseChatViewController+Changes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

import Foundation

extension BaseChatViewController {
extension BaseChatViewController {

public func enqueueModelUpdate(updateType: UpdateType) {
let newItems = self.chatDataSource?.chatItems ?? []
Expand Down Expand Up @@ -65,19 +65,21 @@ extension BaseChatViewController {

// Returns scrolling position in interval [0, 1], 0 top, 1 bottom
public var focusPosition: Double {
guard let collectionView = self.collectionView else { return 0 }
if self.isCloseToBottom() {
return 1
} else if self.isCloseToTop() {
return 0
}

let contentHeight = self.collectionView.contentSize.height
let contentHeight = collectionView.contentSize.height
guard contentHeight > 0 else {
return 0.5
}

// Rough estimation
let midContentOffset = self.collectionView.contentOffset.y + self.visibleRect().height / 2
let collectionViewContentYOffset = collectionView.contentOffset.y
let midContentOffset = collectionViewContentYOffset + self.visibleRect().height / 2
return min(max(0, Double(midContentOffset / contentHeight)), 1.0)
}

Expand All @@ -98,8 +100,9 @@ extension BaseChatViewController {

private func visibleCellsFromCollectionViewApi() -> [IndexPath: UICollectionViewCell] {
var visibleCells: [IndexPath: UICollectionViewCell] = [:]
self.collectionView.indexPathsForVisibleItems.forEach({ (indexPath) in
if let cell = self.collectionView.cellForItem(at: indexPath) {
guard let collectionView = self.collectionView else { return visibleCells }
collectionView.indexPathsForVisibleItems.forEach({ (indexPath) in
if let cell = collectionView.cellForItem(at: indexPath) {
visibleCells[indexPath] = cell
}
})
Expand Down Expand Up @@ -127,7 +130,10 @@ extension BaseChatViewController {
changes: CollectionChanges,
updateType: UpdateType,
completion: @escaping () -> Void) {

guard let collectionView = self.collectionView else {
completion()
return
}
let usesBatchUpdates: Bool
do { // Recover from too fast updates...
let visibleCellsAreValid = self.visibleCellsAreValid(changes: changes)
Expand Down Expand Up @@ -180,14 +186,14 @@ extension BaseChatViewController {
if usesBatchUpdates {
UIView.animate(withDuration: self.constants.updatesAnimationDuration, animations: { () -> Void in
self.unfinishedBatchUpdatesCount += 1
self.collectionView.performBatchUpdates({ () -> Void in
collectionView.performBatchUpdates({ () -> Void in
updateModelClosure()
self.updateVisibleCells(changes) // For instance, to support removal of tails

self.collectionView.deleteItems(at: Array(changes.deletedIndexPaths))
self.collectionView.insertItems(at: Array(changes.insertedIndexPaths))
collectionView.deleteItems(at: Array(changes.deletedIndexPaths))
collectionView.insertItems(at: Array(changes.insertedIndexPaths))
for move in changes.movedIndexPaths {
self.collectionView.moveItem(at: move.indexPathOld, to: move.indexPathNew)
collectionView.moveItem(at: move.indexPathOld, to: move.indexPathNew)
}
}, completion: { [weak self] (_) -> Void in
defer { myCompletion() }
Expand All @@ -204,8 +210,8 @@ extension BaseChatViewController {
} else {
self.visibleCells = [:]
updateModelClosure()
self.collectionView.reloadData()
self.collectionView.collectionViewLayout.prepare()
collectionView.reloadData()
collectionView.collectionViewLayout.prepare()
if self.placeMessagesFromBottom {
self.adjustCollectionViewInsets(shouldUpdateContentOffset: false)
}
Expand All @@ -225,7 +231,11 @@ extension BaseChatViewController {
}

private func updateModels(newItems: [ChatItemProtocol], oldItems: ChatItemCompanionCollection, updateType: UpdateType, completion: @escaping () -> Void) {
let collectionViewWidth = self.collectionView.bounds.width
guard let collectionView = self.collectionView else {
completion()
return
}
let collectionViewWidth = collectionView.bounds.width
let updateType = self.isFirstLayout ? .firstLoad : updateType
let performInBackground = updateType != .firstLoad

Expand Down Expand Up @@ -329,8 +339,9 @@ extension BaseChatViewController {
}

public func chatCollectionViewLayoutModel() -> ChatCollectionViewLayoutModel {
if self.layoutModel.calculatedForWidth != self.collectionView.bounds.width {
self.layoutModel = self.createLayoutModel(self.chatItemCompanionCollection, collectionViewWidth: self.collectionView.bounds.width)
guard let collectionView = self.collectionView else { return self.layoutModel }
if self.layoutModel.calculatedForWidth != collectionView.bounds.width {
self.layoutModel = self.createLayoutModel(self.chatItemCompanionCollection, collectionViewWidth: collectionView.bounds.width)
}
return self.layoutModel
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,12 @@ extension BaseChatViewController: ChatCollectionViewLayoutDelegate {

public func confugureCollectionViewWithPresenters() {
assert(self.presenterFactory == nil, "Presenter factory is already initialized")
guard let collectionView = self.collectionView else {
assertionFailure("CollectionView is not initialized")
return
}
self.presenterFactory = self.createPresenterFactory()
self.presenterFactory.configure(withCollectionView: self.collectionView)
self.presenterFactory.configure(withCollectionView: collectionView )
}

public func decorationAttributesForIndexPath(_ indexPath: IndexPath) -> ChatItemDecorationAttributesProtocol? {
Expand Down
61 changes: 34 additions & 27 deletions Chatto/Source/ChatController/BaseChatViewController+Scrolling.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,79 +36,86 @@ extension CGFloat {
extension BaseChatViewController {

public func isScrolledAtBottom() -> Bool {
guard self.collectionView.numberOfSections > 0 && self.collectionView.numberOfItems(inSection: 0) > 0 else { return true }
let sectionIndex = self.collectionView.numberOfSections - 1
let itemIndex = self.collectionView.numberOfItems(inSection: sectionIndex) - 1
guard let collectionView = self.collectionView else { return true }
guard collectionView.numberOfSections > 0 && collectionView.numberOfItems(inSection: 0) > 0 else { return true }
let sectionIndex = collectionView.numberOfSections - 1
let itemIndex = collectionView.numberOfItems(inSection: sectionIndex) - 1
let lastIndexPath = IndexPath(item: itemIndex, section: sectionIndex)
return self.isIndexPathVisible(lastIndexPath, atEdge: .bottom)
}

public func isScrolledAtTop() -> Bool {
guard self.collectionView.numberOfSections > 0 && self.collectionView.numberOfItems(inSection: 0) > 0 else { return true }
guard let collectionView = self.collectionView else { return true }
guard collectionView.numberOfSections > 0 && collectionView.numberOfItems(inSection: 0) > 0 else { return true }
let firstIndexPath = IndexPath(item: 0, section: 0)
return self.isIndexPathVisible(firstIndexPath, atEdge: .top)
}

public func isCloseToBottom() -> Bool {
guard self.collectionView.contentSize.height > 0 else { return true }
return (self.visibleRect().maxY / self.collectionView.contentSize.height) > (1 - self.constants.autoloadingFractionalThreshold)
guard let collectionView = self.collectionView else { return true }
guard collectionView.contentSize.height > 0 else { return true }
return (self.visibleRect().maxY / collectionView.contentSize.height) > (1 - self.constants.autoloadingFractionalThreshold)
}

public func isCloseToTop() -> Bool {
guard self.collectionView.contentSize.height > 0 else { return true }
return (self.visibleRect().minY / self.collectionView.contentSize.height) < self.constants.autoloadingFractionalThreshold
guard let collectionView = self.collectionView else { return true }
guard collectionView.contentSize.height > 0 else { return true }
return (self.visibleRect().minY / collectionView.contentSize.height) < self.constants.autoloadingFractionalThreshold
}

public func isIndexPathVisible(_ indexPath: IndexPath, atEdge edge: CellVerticalEdge) -> Bool {
if let attributes = self.collectionView.collectionViewLayout.layoutAttributesForItem(at: indexPath) {
let visibleRect = self.visibleRect()
let intersection = visibleRect.intersection(attributes.frame)
if edge == .top {
return abs(intersection.minY - attributes.frame.minY) < CGFloat.bma_epsilon
} else {
return abs(intersection.maxY - attributes.frame.maxY) < CGFloat.bma_epsilon
}
guard let collectionView = self.collectionView else { return true }
guard let attributes = collectionView.collectionViewLayout.layoutAttributesForItem(at: indexPath) else { return false }
let visibleRect = self.visibleRect()
let intersection = visibleRect.intersection(attributes.frame)
if edge == .top {
return abs(intersection.minY - attributes.frame.minY) < CGFloat.bma_epsilon
} else {
return abs(intersection.maxY - attributes.frame.maxY) < CGFloat.bma_epsilon
}
return false
}

public func visibleRect() -> CGRect {
let contentInset = self.collectionView.contentInset
let collectionViewBounds = self.collectionView.bounds
let contentSize = self.collectionView.collectionViewLayout.collectionViewContentSize
return CGRect(x: CGFloat(0), y: self.collectionView.contentOffset.y + contentInset.top, width: collectionViewBounds.width, height: min(contentSize.height, collectionViewBounds.height - contentInset.top - contentInset.bottom))
guard let collectionView = self.collectionView else { return CGRect.zero }
let contentInset = collectionView.contentInset
let collectionViewBounds = collectionView.bounds
let contentSize = collectionView.collectionViewLayout.collectionViewContentSize
return CGRect(x: CGFloat(0), y: collectionView.contentOffset.y + contentInset.top, width: collectionViewBounds.width, height: min(contentSize.height, collectionViewBounds.height - contentInset.top - contentInset.bottom))
}

@objc
open func scrollToBottom(animated: Bool) {
guard let collectionView = self.collectionView else { return }
// Cancel current scrolling
self.collectionView.setContentOffset(self.collectionView.contentOffset, animated: false)
collectionView.setContentOffset(collectionView.contentOffset, animated: false)

// Note that we don't rely on collectionView's contentSize. This is because it won't be valid after performBatchUpdates or reloadData
// After reload data, collectionViewLayout.collectionViewContentSize won't be even valid, so you may want to refresh the layout manually
let offsetY = max(-self.collectionView.contentInset.top, self.collectionView.collectionViewLayout.collectionViewContentSize.height - self.collectionView.bounds.height + self.collectionView.contentInset.bottom)
let offsetY = max(-collectionView.contentInset.top, collectionView.collectionViewLayout.collectionViewContentSize.height - collectionView.bounds.height + collectionView.contentInset.bottom)

// Don't use setContentOffset(:animated). If animated, contentOffset property will be updated along with the animation for each frame update
// If a message is inserted while scrolling is happening (as in very fast typing), we want to take the "final" content offset (not the "real time" one) to check if we should scroll to bottom again
if animated {
UIView.animate(withDuration: self.constants.updatesAnimationDuration, animations: { () -> Void in
self.collectionView.contentOffset = CGPoint(x: 0, y: offsetY)
collectionView.contentOffset = CGPoint(x: 0, y: offsetY)
})
} else {
self.collectionView.contentOffset = CGPoint(x: 0, y: offsetY)
collectionView.contentOffset = CGPoint(x: 0, y: offsetY)
}
}

public func scrollToPreservePosition(oldRefRect: CGRect?, newRefRect: CGRect?) {
guard let collectionView = self.collectionView else { return }
guard let oldRefRect = oldRefRect, let newRefRect = newRefRect else {
return
}
let diffY = newRefRect.minY - oldRefRect.minY
self.collectionView.contentOffset = CGPoint(x: 0, y: self.collectionView.contentOffset.y + diffY)
collectionView.contentOffset = CGPoint(x: 0, y: collectionView.contentOffset.y + diffY)
}

open func scrollViewDidScroll(_ scrollView: UIScrollView) {
if self.collectionView.isDragging {
guard let collectionView = self.collectionView else { return }
if collectionView.isDragging {
self.autoLoadMoreContentIfNeeded()
}
}
Expand Down
Loading

0 comments on commit 1016906

Please sign in to comment.