Compare commits
2 Commits
master
...
IOS-10460-
| Author | SHA1 | Date |
|---|---|---|
|
|
bc1b4733d7 | |
|
|
2892305ad5 |
|
|
@ -1,3 +0,0 @@
|
|||
ignore:
|
||||
- "./Chatto/Tests"
|
||||
- "./ChattoAdditions/Tests"
|
||||
|
|
@ -1 +0,0 @@
|
|||
3.0.1
|
||||
|
|
@ -1,9 +1,4 @@
|
|||
opt_in_rules:
|
||||
- closure_spacing
|
||||
- overridden_super_call
|
||||
- redundant_nil_coalesing
|
||||
- explicit_init
|
||||
disabled_rules:
|
||||
disabled_rules: # rule identifiers to exclude from running
|
||||
- file_length
|
||||
- force_cast
|
||||
- function_body_length
|
||||
|
|
|
|||
14
.travis.yml
14
.travis.yml
|
|
@ -1,10 +1,12 @@
|
|||
language: objective-c
|
||||
osx_image: xcode8.1
|
||||
osx_image: xcode7.3
|
||||
|
||||
script:
|
||||
- set -o pipefail
|
||||
- xcodebuild clean build test -workspace ./Chatto.xcworkspace -scheme Chatto -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 7' -configuration Debug | xcpretty
|
||||
- bash <(curl -s https://codecov.io/bash) -J 'Chatto'
|
||||
- xcodebuild clean build test -workspace ./Chatto.xcworkspace -scheme ChattoAdditions -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 7' -configuration Debug | xcpretty
|
||||
- bash <(curl -s https://codecov.io/bash) -J 'ChattoAdditions'
|
||||
- xcodebuild clean build test -workspace ./ChattoApp/ChattoApp.xcworkspace -scheme ChattoApp -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 7' -configuration Debug | xcpretty
|
||||
- xcodebuild clean build test -workspace ./ChattoApp/ChattoApp.xcworkspace -scheme ChattoApp -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 6,OS=9.3' -configuration Debug | xcpretty
|
||||
- rm -rf ~/Library/Developer/Xcode/DerivedData
|
||||
- xcodebuild clean build test -workspace ./Chatto.xcworkspace -scheme Chatto -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 6,OS=9.3' -configuration Debug | xcpretty
|
||||
- (curl -s https://codecov.io/bash) | bash
|
||||
- rm -rf ~/Library/Developer/Xcode/DerivedData
|
||||
- xcodebuild clean build test -workspace ./Chatto.xcworkspace -scheme ChattoAdditions -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 6,OS=9.3' -configuration Debug | xcpretty
|
||||
- (curl -s https://codecov.io/bash) | bash
|
||||
|
|
|
|||
|
|
@ -1,12 +1,3 @@
|
|||
### 3.0.1 (Nov 14, 2016)
|
||||
* Swift 3.0.1 / Xcode 8.1 support [#233](https://github.com/badoo/Chatto/pull/233) - [@diegosanchezr](https://github.com/diegosanchezr)
|
||||
* Fixes weird linker issue with Carthage [#232](https://github.com/badoo/Chatto/pull/232) - [@zwang](https://github.com/zwang)
|
||||
* Avoids using AVCapture in simulator [#235](https://github.com/badoo/Chatto/pull/235) - [@geegaset](https://github.com/geegaset)
|
||||
* Avoids crashing when receiving a nil indexPath (WebDriverAgent) [#248](https://github.com/badoo/Chatto/pull/248) - [@diegosanchezr](https://github.com/diegosanchezr)
|
||||
|
||||
### 3.0 (Sept 21, 2016)
|
||||
* Swift 3 support 🎉 - [#220](https://github.com/badoo/Chatto/pull/220) - [@diegosanchezr](https://github.com/diegosanchezr)
|
||||
|
||||
### 2.1 (Sept 17, 2016)
|
||||
* Enhanced customization for LiveCameraCell [#199](https://github.com/badoo/Chatto/pull/199) - [@TerekhovAnton](https://github.com/TerekhovAnton)
|
||||
* Fixes input not being at the bottom when chat is embedded in a UITabbarController [#202](https://github.com/badoo/Chatto/pull/202) - [@andris-zalitis](https://github.com/andris-zalitis)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = "Chatto"
|
||||
s.version = "3.0.1"
|
||||
s.version = "2.1.0"
|
||||
s.summary = "Chat framework in Swift"
|
||||
s.description = <<-DESC
|
||||
Lightweight chat framework to build Chat apps
|
||||
|
|
|
|||
|
|
@ -426,7 +426,7 @@
|
|||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 3.0;
|
||||
SWIFT_VERSION = 2.3;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
|
|
@ -470,7 +470,7 @@
|
|||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 3.0;
|
||||
SWIFT_VERSION = 2.3;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
|
|
|
|||
|
|
@ -25,62 +25,62 @@
|
|||
import UIKit
|
||||
|
||||
public enum ChatItemVisibility {
|
||||
case hidden
|
||||
case appearing
|
||||
case visible
|
||||
case Hidden
|
||||
case Appearing
|
||||
case Visible
|
||||
}
|
||||
|
||||
open class BaseChatItemPresenter<CellT: UICollectionViewCell>: ChatItemPresenterProtocol {
|
||||
public class BaseChatItemPresenter<CellT: UICollectionViewCell>: ChatItemPresenterProtocol {
|
||||
public final weak var cell: CellT?
|
||||
|
||||
public init() {}
|
||||
public init() { }
|
||||
|
||||
open class func registerCells(_ collectionView: UICollectionView) {
|
||||
public class func registerCells(collectionView: UICollectionView) {
|
||||
assert(false, "Implement in subclass")
|
||||
}
|
||||
|
||||
open var canCalculateHeightInBackground: Bool {
|
||||
public var canCalculateHeightInBackground: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
open func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat {
|
||||
public func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat {
|
||||
assert(false, "Implement in subclass")
|
||||
return 0
|
||||
}
|
||||
|
||||
open func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell {
|
||||
public func dequeueCell(collectionView collectionView: UICollectionView, indexPath: NSIndexPath) -> UICollectionViewCell {
|
||||
assert(false, "Implemenent in subclass")
|
||||
return UICollectionViewCell()
|
||||
}
|
||||
|
||||
open func configureCell(_ cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) {
|
||||
public func configureCell(cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) {
|
||||
assert(false, "Implemenent in subclass")
|
||||
}
|
||||
|
||||
final public private(set) var itemVisibility: ChatItemVisibility = .hidden
|
||||
final public private(set) var itemVisibility: ChatItemVisibility = .Hidden
|
||||
|
||||
// Need to override default implementatios. Otherwise subclasses's code won't be executed
|
||||
// http://stackoverflow.com/questions/31795158/swift-2-protocol-extension-not-calling-overriden-method-correctly
|
||||
public final func cellWillBeShown(_ cell: UICollectionViewCell) {
|
||||
public final func cellWillBeShown(cell: UICollectionViewCell) {
|
||||
if let cell = cell as? CellT {
|
||||
self.cell = cell
|
||||
self.itemVisibility = .appearing
|
||||
self.itemVisibility = .Appearing
|
||||
self.cellWillBeShown()
|
||||
self.itemVisibility = .visible
|
||||
self.itemVisibility = .Visible
|
||||
} else {
|
||||
assert(false, "Invalid cell was given to presenter!")
|
||||
}
|
||||
}
|
||||
|
||||
open func cellWillBeShown() {
|
||||
public func cellWillBeShown() {
|
||||
// Hook for subclasses
|
||||
}
|
||||
|
||||
open func shouldShowMenu() -> Bool {
|
||||
public func shouldShowMenu() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
public final func cellWasHidden(_ cell: UICollectionViewCell) {
|
||||
public final func cellWasHidden(cell: UICollectionViewCell) {
|
||||
// Carefull!! This doesn't mean that this is no longer visible
|
||||
// If cell is replaced (due to a reload for instance) we can have the following sequence:
|
||||
// - New cell is taken from the pool and configured. We'll get cellWillBeShown
|
||||
|
|
@ -89,7 +89,7 @@ open class BaseChatItemPresenter<CellT: UICollectionViewCell>: ChatItemPresenter
|
|||
if let cell = cell as? CellT {
|
||||
if cell === self.cell {
|
||||
self.cell = nil
|
||||
self.itemVisibility = .hidden
|
||||
self.itemVisibility = .Hidden
|
||||
self.cellWasHidden()
|
||||
}
|
||||
} else {
|
||||
|
|
@ -97,15 +97,15 @@ open class BaseChatItemPresenter<CellT: UICollectionViewCell>: ChatItemPresenter
|
|||
}
|
||||
}
|
||||
|
||||
open func cellWasHidden() {
|
||||
public func cellWasHidden() {
|
||||
// Hook for subclasses. Here we are not visible for real.
|
||||
}
|
||||
|
||||
open func canPerformMenuControllerAction(_ action: Selector) -> Bool {
|
||||
public func canPerformMenuControllerAction(action: Selector) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
open func performMenuControllerAction(_ action: Selector) {
|
||||
public func performMenuControllerAction(action: Selector) {
|
||||
assert(self.canPerformMenuControllerAction(action))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ THE SOFTWARE.
|
|||
import Foundation
|
||||
|
||||
public protocol ChatItemsDecoratorProtocol {
|
||||
func decorateItems(_ chatItems: [ChatItemProtocol]) -> [DecoratedChatItem]
|
||||
func decorateItems(chatItems: [ChatItemProtocol]) -> [DecoratedChatItem]
|
||||
}
|
||||
|
||||
public struct DecoratedChatItem: UniqueIdentificable {
|
||||
|
|
|
|||
|
|
@ -35,29 +35,29 @@ public protocol ChatItemDecorationAttributesProtocol {
|
|||
}
|
||||
|
||||
public protocol ChatItemPresenterProtocol: class {
|
||||
static func registerCells(_ collectionView: UICollectionView)
|
||||
static func registerCells(collectionView: UICollectionView)
|
||||
var canCalculateHeightInBackground: Bool { get } // Default is false
|
||||
func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat
|
||||
func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell
|
||||
func configureCell(_ cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?)
|
||||
func cellWillBeShown(_ cell: UICollectionViewCell) // optional
|
||||
func cellWasHidden(_ cell: UICollectionViewCell) // optional
|
||||
func dequeueCell(collectionView collectionView: UICollectionView, indexPath: NSIndexPath) -> UICollectionViewCell
|
||||
func configureCell(cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?)
|
||||
func cellWillBeShown(cell: UICollectionViewCell) // optional
|
||||
func cellWasHidden(cell: UICollectionViewCell) // optional
|
||||
func shouldShowMenu() -> Bool // optional. Default is false
|
||||
func canPerformMenuControllerAction(_ action: Selector) -> Bool // optional. Default is false
|
||||
func performMenuControllerAction(_ action: Selector) // optional
|
||||
func canPerformMenuControllerAction(action: Selector) -> Bool // optional. Default is false
|
||||
func performMenuControllerAction(action: Selector) // optional
|
||||
}
|
||||
|
||||
public extension ChatItemPresenterProtocol { // Optionals
|
||||
var canCalculateHeightInBackground: Bool { return false }
|
||||
func cellWillBeShown(_ cell: UICollectionViewCell) {}
|
||||
func cellWasHidden(_ cell: UICollectionViewCell) {}
|
||||
func cellWillBeShown(cell: UICollectionViewCell) {}
|
||||
func cellWasHidden(cell: UICollectionViewCell) {}
|
||||
func shouldShowMenu() -> Bool { return false }
|
||||
func canPerformMenuControllerAction(_ action: Selector) -> Bool { return false }
|
||||
func performMenuControllerAction(_ action: Selector) {}
|
||||
func canPerformMenuControllerAction(action: Selector) -> Bool { return false }
|
||||
func performMenuControllerAction(action: Selector) {}
|
||||
}
|
||||
|
||||
public protocol ChatItemPresenterBuilderProtocol {
|
||||
func canHandleChatItem(_ chatItem: ChatItemProtocol) -> Bool
|
||||
func createPresenterWithChatItem(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol
|
||||
func canHandleChatItem(chatItem: ChatItemProtocol) -> Bool
|
||||
func createPresenterWithChatItem(chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol
|
||||
var presenterType: ChatItemPresenterProtocol.Type { get }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ import Foundation
|
|||
// Handles messages that aren't supported so they appear as invisible
|
||||
class DummyChatItemPresenter: ChatItemPresenterProtocol {
|
||||
|
||||
class func registerCells(_ collectionView: UICollectionView) {
|
||||
collectionView.register(DummyCollectionViewCell.self, forCellWithReuseIdentifier: "cell-id-unhandled-message")
|
||||
class func registerCells(collectionView: UICollectionView) {
|
||||
collectionView.registerClass(DummyCollectionViewCell.self, forCellWithReuseIdentifier: "cell-id-unhandled-message")
|
||||
}
|
||||
|
||||
var canCalculateHeightInBackground: Bool {
|
||||
|
|
@ -39,13 +39,14 @@ class DummyChatItemPresenter: ChatItemPresenterProtocol {
|
|||
return 0
|
||||
}
|
||||
|
||||
func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell {
|
||||
return collectionView.dequeueReusableCell(withReuseIdentifier: "cell-id-unhandled-message", for: indexPath)
|
||||
func dequeueCell(collectionView collectionView: UICollectionView, indexPath: NSIndexPath) -> UICollectionViewCell {
|
||||
return collectionView.dequeueReusableCellWithReuseIdentifier("cell-id-unhandled-message", forIndexPath: indexPath)
|
||||
}
|
||||
|
||||
func configureCell(_ cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) {
|
||||
cell.isHidden = true
|
||||
func configureCell(cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) {
|
||||
cell.hidden = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class DummyCollectionViewCell: UICollectionViewCell {}
|
||||
|
|
|
|||
|
|
@ -26,15 +26,15 @@ import Foundation
|
|||
|
||||
extension BaseChatViewController: ChatDataSourceDelegateProtocol {
|
||||
|
||||
public func chatDataSourceDidUpdate(_ chatDataSource: ChatDataSourceProtocol, updateType: UpdateType) {
|
||||
public func chatDataSourceDidUpdate(chatDataSource: ChatDataSourceProtocol, updateType: UpdateType) {
|
||||
self.enqueueModelUpdate(updateType: updateType)
|
||||
}
|
||||
|
||||
public func chatDataSourceDidUpdate(_ chatDataSource: ChatDataSourceProtocol) {
|
||||
self.enqueueModelUpdate(updateType: .normal)
|
||||
public func chatDataSourceDidUpdate(chatDataSource: ChatDataSourceProtocol) {
|
||||
self.enqueueModelUpdate(updateType: .Normal)
|
||||
}
|
||||
|
||||
public func enqueueModelUpdate(updateType: UpdateType) {
|
||||
public func enqueueModelUpdate(updateType updateType: UpdateType) {
|
||||
let newItems = self.chatDataSource?.chatItems ?? []
|
||||
|
||||
if self.updatesConfig.coalesceUpdates {
|
||||
|
|
@ -56,7 +56,7 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
|
|||
}
|
||||
|
||||
public func enqueueMessageCountReductionIfNeeded() {
|
||||
guard let preferredMaxMessageCount = self.constants.preferredMaxMessageCount, (self.chatDataSource?.chatItems.count ?? 0) > preferredMaxMessageCount else { return }
|
||||
guard let preferredMaxMessageCount = self.constants.preferredMaxMessageCount where (self.chatDataSource?.chatItems.count ?? 0) > preferredMaxMessageCount else { return }
|
||||
self.updateQueue.addTask { [weak self] (completion) -> () in
|
||||
guard let sSelf = self else { return }
|
||||
sSelf.chatDataSource?.adjustNumberOfMessages(preferredMaxCount: sSelf.constants.preferredMaxMessageCountAdjustment, focusPosition: sSelf.focusPosition, completion: { (didAdjust) -> Void in
|
||||
|
|
@ -66,7 +66,7 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
|
|||
}
|
||||
let newItems = sSelf.chatDataSource?.chatItems ?? []
|
||||
let oldItems = sSelf.chatItemCompanionCollection
|
||||
sSelf.updateModels(newItems: newItems, oldItems: oldItems, updateType: .messageCountReduction, completion: completion )
|
||||
sSelf.updateModels(newItems: newItems, oldItems: oldItems, updateType: .MessageCountReduction, completion: completion )
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -89,7 +89,7 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
|
|||
return min(max(0, Double(midContentOffset / contentHeight)), 1.0)
|
||||
}
|
||||
|
||||
func updateVisibleCells(_ changes: CollectionChanges) {
|
||||
func updateVisibleCells(changes: CollectionChanges) {
|
||||
// Datasource should be already updated!
|
||||
|
||||
assert(self.visibleCellsAreValid(changes: changes), "Invalid visible cells. Don't call me")
|
||||
|
|
@ -104,17 +104,17 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
private func visibleCellsFromCollectionViewApi() -> [IndexPath: UICollectionViewCell] {
|
||||
var visibleCells: [IndexPath: UICollectionViewCell] = [:]
|
||||
self.collectionView.indexPathsForVisibleItems.forEach({ (indexPath) in
|
||||
if let cell = self.collectionView.cellForItem(at: indexPath) {
|
||||
private func visibleCellsFromCollectionViewApi() -> [NSIndexPath: UICollectionViewCell] {
|
||||
var visibleCells: [NSIndexPath: UICollectionViewCell] = [:]
|
||||
self.collectionView.indexPathsForVisibleItems().forEach({ (indexPath) in
|
||||
if let cell = self.collectionView.cellForItemAtIndexPath(indexPath) {
|
||||
visibleCells[indexPath] = cell
|
||||
}
|
||||
})
|
||||
return visibleCells
|
||||
}
|
||||
|
||||
private func visibleCellsAreValid(changes: CollectionChanges) -> Bool {
|
||||
private func visibleCellsAreValid(changes changes: CollectionChanges) -> Bool {
|
||||
// Afer performBatchUpdates, indexPathForCell may return a cell refering to the state before the update
|
||||
// if self.updatesConfig.fastUpdates is enabled, very fast updates could result in `updateVisibleCells` updating wrong cells.
|
||||
// See more: https://github.com/diegosanchezr/UICollectionViewStressing
|
||||
|
|
@ -128,19 +128,18 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
|
|||
|
||||
private enum ScrollAction {
|
||||
case scrollToBottom
|
||||
case preservePosition(rectForReferenceIndexPathBeforeUpdate: CGRect?, referenceIndexPathAfterUpdate: IndexPath?)
|
||||
case preservePosition(rectForReferenceIndexPathBeforeUpdate: CGRect?, referenceIndexPathAfterUpdate: NSIndexPath?)
|
||||
}
|
||||
|
||||
func performBatchUpdates(updateModelClosure: @escaping () -> Void,
|
||||
func performBatchUpdates(updateModelClosure updateModelClosure: () -> Void,
|
||||
changes: CollectionChanges,
|
||||
updateType: UpdateType,
|
||||
completion: @escaping () -> Void) {
|
||||
completion: () -> Void) {
|
||||
|
||||
let usesBatchUpdates: Bool
|
||||
let animateBatchUpdates: Bool
|
||||
do { // Recover from too fast updates...
|
||||
let visibleCellsAreValid = self.visibleCellsAreValid(changes: changes)
|
||||
let wantsReloadData = ![UpdateType.normal, UpdateType.pagination, UpdateType.firstLoad].contains(updateType)
|
||||
let wantsReloadData = updateType != .Normal
|
||||
let hasUnfinishedBatchUpdates = self.unfinishedBatchUpdatesCount > 0 // This can only happen when enabling self.updatesConfig.fastUpdates
|
||||
|
||||
// a) It's unsafe to perform reloadData while there's a performBatchUpdates animating: https://github.com/diegosanchezr/UICollectionViewStressing/tree/master/GhostCells
|
||||
|
|
@ -159,12 +158,11 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
|
|||
// ... if they are still invalid the only thing we can do is a reloadData
|
||||
let mustDoReloadData = !visibleCellsAreValid // Only way to recover from this inconsistent state
|
||||
usesBatchUpdates = !wantsReloadData && !mustDoReloadData
|
||||
animateBatchUpdates = ![UpdateType.pagination, UpdateType.firstLoad].contains(updateType)
|
||||
}
|
||||
|
||||
let scrollAction: ScrollAction
|
||||
do { // Scroll action
|
||||
if updateType != .pagination && self.isScrolledAtBottom() {
|
||||
if updateType != .Pagination && self.isScrolledAtBottom() {
|
||||
scrollAction = .scrollToBottom
|
||||
} else {
|
||||
let (oldReferenceIndexPath, newReferenceIndexPath) = self.referenceIndexPathsToRestoreScrollPositionOnUpdate(itemsBeforeUpdate: self.chatItemCompanionCollection, changes: changes)
|
||||
|
|
@ -180,7 +178,7 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
|
|||
if myCompletionExecuted { return }
|
||||
myCompletionExecuted = true
|
||||
|
||||
DispatchQueue.main.async(execute: { () -> Void in
|
||||
dispatch_async(dispatch_get_main_queue(), { () -> Void in
|
||||
// Reduces inconsistencies before next update: https://github.com/diegosanchezr/UICollectionViewStressing
|
||||
completion()
|
||||
})
|
||||
|
|
@ -188,46 +186,36 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
|
|||
}
|
||||
|
||||
if usesBatchUpdates {
|
||||
let batchUpdates = {
|
||||
UIView.animateWithDuration(self.constants.updatesAnimationDuration, animations: { () -> Void in
|
||||
self.unfinishedBatchUpdatesCount += 1
|
||||
self.collectionView.performBatchUpdates({ () -> Void in
|
||||
updateModelClosure()
|
||||
self.updateVisibleCells(changes) // For instace, to support removal of tails
|
||||
|
||||
self.collectionView.deleteItems(at: Array(changes.deletedIndexPaths))
|
||||
self.collectionView.insertItems(at: Array(changes.insertedIndexPaths))
|
||||
self.collectionView.deleteItemsAtIndexPaths(Array(changes.deletedIndexPaths))
|
||||
self.collectionView.insertItemsAtIndexPaths(Array(changes.insertedIndexPaths))
|
||||
for move in changes.movedIndexPaths {
|
||||
self.collectionView.moveItem(at: move.indexPathOld, to: move.indexPathNew)
|
||||
self.collectionView.moveItemAtIndexPath(move.indexPathOld, toIndexPath: move.indexPathNew)
|
||||
}
|
||||
}) { [weak self] (finished) -> Void in
|
||||
defer { myCompletion() }
|
||||
guard let sSelf = self else { return }
|
||||
sSelf.unfinishedBatchUpdatesCount -= 1
|
||||
if sSelf.unfinishedBatchUpdatesCount == 0, let onAllBatchUpdatesFinished = self?.onAllBatchUpdatesFinished {
|
||||
DispatchQueue.main.async(execute: onAllBatchUpdatesFinished)
|
||||
dispatch_async(dispatch_get_main_queue(), onAllBatchUpdatesFinished)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if animateBatchUpdates {
|
||||
UIView.animate(withDuration: self.constants.updatesAnimationDuration, animations: { () -> Void in
|
||||
batchUpdates()
|
||||
})
|
||||
} else {
|
||||
UIView.performWithoutAnimation {
|
||||
batchUpdates()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.visibleCells = [:]
|
||||
updateModelClosure()
|
||||
self.collectionView.reloadData()
|
||||
self.collectionView.collectionViewLayout.prepare()
|
||||
self.collectionView.collectionViewLayout.prepareLayout()
|
||||
}
|
||||
|
||||
switch scrollAction {
|
||||
case .scrollToBottom:
|
||||
self.scrollToBottom(animated: updateType == .normal)
|
||||
self.scrollToBottom(animated: updateType == .Normal)
|
||||
case .preservePosition(rectForReferenceIndexPathBeforeUpdate: let oldRect, referenceIndexPathAfterUpdate: let indexPath):
|
||||
let newRect = self.rectAtIndexPath(indexPath)
|
||||
self.scrollToPreservePosition(oldRefRect: oldRect, newRefRect: newRect)
|
||||
|
|
@ -238,16 +226,16 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
private func updateModels(newItems: [ChatItemProtocol], oldItems: ChatItemCompanionCollection, updateType: UpdateType, completion: @escaping () -> Void) {
|
||||
private func updateModels(newItems newItems: [ChatItemProtocol], oldItems: ChatItemCompanionCollection, updateType: UpdateType, completion: () -> Void) {
|
||||
let collectionViewWidth = self.collectionView.bounds.width
|
||||
let updateType = self.isFirstLayout ? .firstLoad : updateType
|
||||
let performInBackground = updateType != .firstLoad
|
||||
let updateType = self.isFirstLayout ? .FirstLoad : updateType
|
||||
let performInBackground = updateType != .FirstLoad
|
||||
|
||||
self.autoLoadingEnabled = false
|
||||
let perfomBatchUpdates: (_ changes: CollectionChanges, _ updateModelClosure: @escaping () -> Void) -> () = { [weak self] (changes, updateModelClosure) in
|
||||
let perfomBatchUpdates: (changes: CollectionChanges, updateModelClosure: () -> Void) -> () = { [weak self] modelUpdate in
|
||||
self?.performBatchUpdates(
|
||||
updateModelClosure: updateModelClosure,
|
||||
changes: changes,
|
||||
updateModelClosure: modelUpdate.updateModelClosure,
|
||||
changes: modelUpdate.changes,
|
||||
updateType: updateType,
|
||||
completion: { () -> Void in
|
||||
self?.autoLoadingEnabled = true
|
||||
|
|
@ -263,19 +251,19 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
|
|||
}
|
||||
|
||||
if performInBackground {
|
||||
DispatchQueue.global(qos: .userInitiated).async { () -> Void in
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
|
||||
let modelUpdate = createModelUpdate()
|
||||
DispatchQueue.main.async(execute: { () -> Void in
|
||||
perfomBatchUpdates(modelUpdate.changes, modelUpdate.updateModelClosure)
|
||||
dispatch_async(dispatch_get_main_queue(), { () -> Void in
|
||||
perfomBatchUpdates(changes: modelUpdate.changes, updateModelClosure: modelUpdate.updateModelClosure)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
let modelUpdate = createModelUpdate()
|
||||
perfomBatchUpdates(modelUpdate.changes, modelUpdate.updateModelClosure)
|
||||
perfomBatchUpdates(changes: modelUpdate.changes, updateModelClosure: modelUpdate.updateModelClosure)
|
||||
}
|
||||
}
|
||||
|
||||
private func createModelUpdates(newItems: [ChatItemProtocol], oldItems: ChatItemCompanionCollection, collectionViewWidth: CGFloat) -> (changes: CollectionChanges, updateModelClosure: () -> Void) {
|
||||
private func createModelUpdates(newItems newItems: [ChatItemProtocol], oldItems: ChatItemCompanionCollection, collectionViewWidth: CGFloat) -> (changes: CollectionChanges, updateModelClosure: () -> Void) {
|
||||
let newDecoratedItems = self.chatItemsDecorator?.decorateItems(newItems) ?? newItems.map { DecoratedChatItem(chatItem: $0, decorationAttributes: nil) }
|
||||
let changes = Chatto.generateChanges(oldCollection: oldItems.lazy.map { $0 }, newCollection: newDecoratedItems.lazy.map { $0 })
|
||||
let itemCompanionCollection = self.createCompanionCollection(fromChatItems: newDecoratedItems, previousCompanionCollection: oldItems)
|
||||
|
|
@ -296,7 +284,7 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
|
|||
// Oherwise updateVisibleCells may try to update existing cell with a new presenter which is working with a different type of cell
|
||||
|
||||
// Optimization: reuse presenter if it's the same instance.
|
||||
if let oldChatItemCompanion = oldItems[decoratedChatItem.uid], oldChatItemCompanion.chatItem === chatItem {
|
||||
if let oldChatItemCompanion = oldItems[decoratedChatItem.uid] where oldChatItemCompanion.chatItem === chatItem {
|
||||
presenter = oldChatItemCompanion.presenter
|
||||
} else {
|
||||
presenter = self.createPresenterForChatItem(decoratedChatItem.chatItem)
|
||||
|
|
@ -305,22 +293,22 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
|
|||
})
|
||||
}
|
||||
|
||||
private func createLayoutModel(_ items: ChatItemCompanionCollection, collectionViewWidth: CGFloat) -> ChatCollectionViewLayoutModel {
|
||||
private func createLayoutModel(items: ChatItemCompanionCollection, collectionViewWidth: CGFloat) -> ChatCollectionViewLayoutModel {
|
||||
typealias IntermediateItemLayoutData = (height: CGFloat?, bottomMargin: CGFloat)
|
||||
typealias ItemLayoutData = (height: CGFloat, bottomMargin: CGFloat)
|
||||
|
||||
func createLayoutModel(intermediateLayoutData: [IntermediateItemLayoutData]) -> ChatCollectionViewLayoutModel {
|
||||
func createLayoutModel(intermediateLayoutData intermediateLayoutData: [IntermediateItemLayoutData]) -> ChatCollectionViewLayoutModel {
|
||||
let layoutData = intermediateLayoutData.map { (intermediateLayoutData: IntermediateItemLayoutData) -> ItemLayoutData in
|
||||
return (height: intermediateLayoutData.height!, bottomMargin: intermediateLayoutData.bottomMargin)
|
||||
}
|
||||
return ChatCollectionViewLayoutModel.createModel(self.collectionView.bounds.width, itemsLayoutData: layoutData)
|
||||
}
|
||||
|
||||
let isInbackground = !Thread.isMainThread
|
||||
let isInbackground = !NSThread.isMainThread()
|
||||
var intermediateLayoutData = [IntermediateItemLayoutData]()
|
||||
var itemsForMainThread = [(index: Int, itemCompanion: ChatItemCompanion)]()
|
||||
|
||||
for (index, itemCompanion) in items.enumerated() {
|
||||
for (index, itemCompanion) in items.enumerate() {
|
||||
var height: CGFloat?
|
||||
let bottomMargin: CGFloat = itemCompanion.decorationAttributes?.bottomMargin ?? 0
|
||||
if !isInbackground || itemCompanion.presenter.canCalculateHeightInBackground {
|
||||
|
|
@ -332,7 +320,7 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
|
|||
}
|
||||
|
||||
if itemsForMainThread.count > 0 {
|
||||
DispatchQueue.main.sync(execute: { () -> Void in
|
||||
dispatch_sync(dispatch_get_main_queue(), { () -> Void in
|
||||
for (index, itemCompanion) in itemsForMainThread {
|
||||
let height = itemCompanion.presenter.heightForCell(maximumWidth: collectionViewWidth, decorationAttributes: itemCompanion.decorationAttributes)
|
||||
intermediateLayoutData[index].height = height
|
||||
|
|
|
|||
|
|
@ -26,12 +26,11 @@ import Foundation
|
|||
|
||||
extension BaseChatViewController: ChatCollectionViewLayoutDelegate {
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
public func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return self.chatItemCompanionCollection.count
|
||||
}
|
||||
|
||||
@objc(collectionView:cellForItemAtIndexPath:)
|
||||
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
public func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
|
||||
let presenter = self.presenterForIndexPath(indexPath)
|
||||
let cell = presenter.dequeueCell(collectionView: collectionView, indexPath: indexPath)
|
||||
let decorationAttributes = self.decorationAttributesForIndexPath(indexPath)
|
||||
|
|
@ -39,17 +38,16 @@ extension BaseChatViewController: ChatCollectionViewLayoutDelegate {
|
|||
return cell
|
||||
}
|
||||
|
||||
@objc(collectionView:didEndDisplayingCell:forItemAtIndexPath:)
|
||||
open func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
public func collectionView(collectionView: UICollectionView, didEndDisplayingCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
|
||||
// Carefull: this index path can refer to old data source after an update. Don't use it to grab items from the model
|
||||
// Instead let's use a mapping presenter <--> cell
|
||||
if let oldPresenterForCell = self.presentersByCell.object(forKey: cell) as? ChatItemPresenterProtocol {
|
||||
self.presentersByCell.removeObject(forKey: cell)
|
||||
if let oldPresenterForCell = self.presentersByCell.objectForKey(cell) as? ChatItemPresenterProtocol {
|
||||
self.presentersByCell.removeObjectForKey(cell)
|
||||
oldPresenterForCell.cellWasHidden(cell)
|
||||
}
|
||||
|
||||
if self.updatesConfig.fastUpdates {
|
||||
if let visibleCell = self.visibleCells[indexPath], visibleCell === cell {
|
||||
if let visibleCell = self.visibleCells[indexPath] where visibleCell === cell {
|
||||
self.visibleCells[indexPath] = nil
|
||||
} else {
|
||||
self.visibleCells.forEach({ (indexPath, storedCell) in
|
||||
|
|
@ -62,8 +60,7 @@ extension BaseChatViewController: ChatCollectionViewLayoutDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
@objc(collectionView:willDisplayCell:forItemAtIndexPath:)
|
||||
open func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
public func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
|
||||
// Here indexPath should always referer to updated data source.
|
||||
|
||||
let presenter = self.presenterForIndexPath(indexPath)
|
||||
|
|
@ -83,29 +80,23 @@ extension BaseChatViewController: ChatCollectionViewLayoutDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
@objc(collectionView:shouldShowMenuForItemAtIndexPath:)
|
||||
open func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool {
|
||||
return self.presenterForIndexPath(indexPath).shouldShowMenu()
|
||||
public func collectionView(collectionView: UICollectionView, shouldShowMenuForItemAtIndexPath indexPath: NSIndexPath) -> Bool {
|
||||
return self.presenterForIndexPath(indexPath).shouldShowMenu() ?? false
|
||||
}
|
||||
|
||||
@objc(collectionView:canPerformAction:forItemAtIndexPath:withSender:)
|
||||
open func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath?, withSender sender: Any?) -> Bool {
|
||||
// Note: IndexPath set optional due to https://github.com/badoo/Chatto/issues/247. SR-2417 might be related
|
||||
// Might be related: https://bugs.swift.org/browse/SR-2417
|
||||
guard let indexPath = indexPath else { return false }
|
||||
return self.presenterForIndexPath(indexPath).canPerformMenuControllerAction(action)
|
||||
public func collectionView(collectionView: UICollectionView, canPerformAction action: Selector, forItemAtIndexPath indexPath: NSIndexPath, withSender sender: AnyObject?) -> Bool {
|
||||
return self.presenterForIndexPath(indexPath).canPerformMenuControllerAction(action) ?? false
|
||||
}
|
||||
|
||||
@objc(collectionView:performAction:forItemAtIndexPath:withSender:)
|
||||
open func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) {
|
||||
public func collectionView(collectionView: UICollectionView, performAction action: Selector, forItemAtIndexPath indexPath: NSIndexPath, withSender sender: AnyObject?) {
|
||||
self.presenterForIndexPath(indexPath).performMenuControllerAction(action)
|
||||
}
|
||||
|
||||
func presenterForIndexPath(_ indexPath: IndexPath) -> ChatItemPresenterProtocol {
|
||||
func presenterForIndexPath(indexPath: NSIndexPath) -> ChatItemPresenterProtocol {
|
||||
return self.presenterForIndex(indexPath.item, chatItemCompanionCollection: self.chatItemCompanionCollection)
|
||||
}
|
||||
|
||||
func presenterForIndex(_ index: Int, chatItemCompanionCollection items: ChatItemCompanionCollection) -> ChatItemPresenterProtocol {
|
||||
func presenterForIndex(index: Int, chatItemCompanionCollection items: ChatItemCompanionCollection) -> ChatItemPresenterProtocol {
|
||||
guard index < items.count else {
|
||||
// This can happen from didEndDisplayingCell if we reloaded with less messages
|
||||
return DummyChatItemPresenter()
|
||||
|
|
@ -113,11 +104,11 @@ extension BaseChatViewController: ChatCollectionViewLayoutDelegate {
|
|||
return items[index].presenter
|
||||
}
|
||||
|
||||
public func createPresenterForChatItem(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol {
|
||||
public func createPresenterForChatItem(chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol {
|
||||
return self.presenterFactory.createChatItemPresenter(chatItem)
|
||||
}
|
||||
|
||||
public func decorationAttributesForIndexPath(_ indexPath: IndexPath) -> ChatItemDecorationAttributesProtocol? {
|
||||
public func decorationAttributesForIndexPath(indexPath: NSIndexPath) -> ChatItemDecorationAttributesProtocol? {
|
||||
return self.chatItemCompanionCollection[indexPath.item].decorationAttributes
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@
|
|||
import Foundation
|
||||
|
||||
public enum CellVerticalEdge {
|
||||
case top
|
||||
case bottom
|
||||
case Top
|
||||
case Bottom
|
||||
}
|
||||
|
||||
extension CGFloat {
|
||||
|
|
@ -36,17 +36,17 @@ 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
|
||||
let lastIndexPath = IndexPath(item: itemIndex, section: sectionIndex)
|
||||
return self.isIndexPathVisible(lastIndexPath, atEdge: .bottom)
|
||||
guard self.collectionView.numberOfSections() > 0 && self.collectionView.numberOfItemsInSection(0) > 0 else { return true }
|
||||
let sectionIndex = self.collectionView.numberOfSections() - 1
|
||||
let itemIndex = self.collectionView.numberOfItemsInSection(sectionIndex) - 1
|
||||
let lastIndexPath = NSIndexPath(forItem: itemIndex, inSection: 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 }
|
||||
let firstIndexPath = IndexPath(item: 0, section: 0)
|
||||
return self.isIndexPathVisible(firstIndexPath, atEdge: .top)
|
||||
guard self.collectionView.numberOfSections() > 0 && self.collectionView.numberOfItemsInSection(0) > 0 else { return true }
|
||||
let firstIndexPath = NSIndexPath(forItem: 0, inSection: 0)
|
||||
return self.isIndexPathVisible(firstIndexPath, atEdge: .Top)
|
||||
}
|
||||
|
||||
public func isCloseToBottom() -> Bool {
|
||||
|
|
@ -59,14 +59,14 @@ extension BaseChatViewController {
|
|||
return (self.visibleRect().minY / self.collectionView.contentSize.height) < self.constants.autoloadingFractionalThreshold
|
||||
}
|
||||
|
||||
public func isIndexPathVisible(_ indexPath: IndexPath, atEdge edge: CellVerticalEdge) -> Bool {
|
||||
if let attributes = self.collectionView.collectionViewLayout.layoutAttributesForItem(at: indexPath) {
|
||||
public func isIndexPathVisible(indexPath: NSIndexPath, atEdge edge: CellVerticalEdge) -> Bool {
|
||||
if let attributes = self.collectionView.collectionViewLayout.layoutAttributesForItemAtIndexPath(indexPath) {
|
||||
let visibleRect = self.visibleRect()
|
||||
let intersection = visibleRect.intersection(attributes.frame)
|
||||
if edge == .top {
|
||||
return abs(intersection.minY - attributes.frame.minY) < CGFloat.bma_epsilon
|
||||
let intersection = visibleRect.intersect(attributes.frame)
|
||||
if edge == .Top {
|
||||
return CGFloat.abs(intersection.minY - attributes.frame.minY) < CGFloat.bma_epsilon
|
||||
} else {
|
||||
return abs(intersection.maxY - attributes.frame.maxY) < CGFloat.bma_epsilon
|
||||
return CGFloat.abs(intersection.maxY - attributes.frame.maxY) < CGFloat.bma_epsilon
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
|
@ -75,22 +75,22 @@ extension BaseChatViewController {
|
|||
public func visibleRect() -> CGRect {
|
||||
let contentInset = self.collectionView.contentInset
|
||||
let collectionViewBounds = self.collectionView.bounds
|
||||
let contentSize = self.collectionView.collectionViewLayout.collectionViewContentSize
|
||||
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))
|
||||
}
|
||||
|
||||
public func scrollToBottom(animated: Bool) {
|
||||
public func scrollToBottom(animated animated: Bool) {
|
||||
// Cancel current scrolling
|
||||
self.collectionView.setContentOffset(self.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(-self.collectionView.contentInset.top, self.collectionView.collectionViewLayout.collectionViewContentSize().height - self.collectionView.bounds.height + self.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
|
||||
UIView.animateWithDuration(self.constants.updatesAnimationDuration, animations: { () -> Void in
|
||||
self.collectionView.contentOffset = CGPoint(x: 0, y: offsetY)
|
||||
})
|
||||
} else {
|
||||
|
|
@ -98,21 +98,21 @@ extension BaseChatViewController {
|
|||
}
|
||||
}
|
||||
|
||||
public func scrollToPreservePosition(oldRefRect: CGRect?, newRefRect: CGRect?) {
|
||||
guard let oldRefRect = oldRefRect, let newRefRect = newRefRect else {
|
||||
public func scrollToPreservePosition(oldRefRect oldRefRect: CGRect?, newRefRect: CGRect?) {
|
||||
guard let oldRefRect = oldRefRect, newRefRect = newRefRect else {
|
||||
return
|
||||
}
|
||||
let diffY = newRefRect.minY - oldRefRect.minY
|
||||
self.collectionView.contentOffset = CGPoint(x: 0, y: self.collectionView.contentOffset.y + diffY)
|
||||
}
|
||||
|
||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if self.collectionView.isDragging {
|
||||
public func scrollViewDidScroll(scrollView: UIScrollView) {
|
||||
if self.collectionView.dragging {
|
||||
self.autoLoadMoreContentIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
public func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
|
||||
public func scrollViewDidScrollToTop(scrollView: UIScrollView) {
|
||||
self.autoLoadMoreContentIfNeeded()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,14 +24,14 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
open class BaseChatViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
|
||||
public class BaseChatViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
|
||||
|
||||
public typealias ChatItemCompanionCollection = ReadOnlyOrderedDictionary<ChatItemCompanion>
|
||||
|
||||
public struct Constants {
|
||||
public var updatesAnimationDuration: TimeInterval = 0.33
|
||||
public var updatesAnimationDuration: NSTimeInterval = 0.33
|
||||
public var defaultContentInsets = UIEdgeInsets(top: 10, left: 0, bottom: 10, right: 0)
|
||||
public var defaultScrollIndicatorInsets = UIEdgeInsets.zero
|
||||
public var defaultScrollIndicatorInsets = UIEdgeInsetsZero
|
||||
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]
|
||||
|
|
@ -54,12 +54,12 @@ open class BaseChatViewController: UIViewController, UICollectionViewDataSource,
|
|||
return _chatDataSource
|
||||
}
|
||||
set {
|
||||
self.setChatDataSource(newValue, triggeringUpdateType: .normal)
|
||||
self.setChatDataSource(newValue, triggeringUpdateType: .Normal)
|
||||
}
|
||||
}
|
||||
|
||||
// Custom update on setting the data source. if triggeringUpdateType is nil it won't enqueue any update (you should do it later manually)
|
||||
public final func setChatDataSource(_ dataSource: ChatDataSourceProtocol?, triggeringUpdateType updateType: UpdateType?) {
|
||||
public final func setChatDataSource(dataSource: ChatDataSourceProtocol?, triggeringUpdateType updateType: UpdateType?) {
|
||||
self._chatDataSource = dataSource
|
||||
self._chatDataSource?.delegate = self
|
||||
if let updateType = updateType {
|
||||
|
|
@ -72,12 +72,12 @@ open class BaseChatViewController: UIViewController, UICollectionViewDataSource,
|
|||
self.collectionView?.dataSource = nil
|
||||
}
|
||||
|
||||
open override func loadView() {
|
||||
public override func loadView() {
|
||||
self.view = BaseChatViewControllerView() // http://stackoverflow.com/questions/24596031/uiviewcontroller-with-inputaccessoryview-is-not-deallocated
|
||||
self.view.backgroundColor = UIColor.white
|
||||
self.view.backgroundColor = UIColor.whiteColor()
|
||||
}
|
||||
|
||||
override open func viewDidLoad() {
|
||||
override public func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.addCollectionView()
|
||||
self.addInputViews()
|
||||
|
|
@ -91,39 +91,39 @@ open class BaseChatViewController: UIViewController, UICollectionViewDataSource,
|
|||
|
||||
public var endsEditingWhenTappingOnChatBackground = true
|
||||
@objc
|
||||
open func userDidTapOnCollectionView() {
|
||||
public func userDidTapOnCollectionView() {
|
||||
if self.endsEditingWhenTappingOnChatBackground {
|
||||
self.view.endEditing(true)
|
||||
}
|
||||
}
|
||||
|
||||
open override func viewWillAppear(_ animated: Bool) {
|
||||
public override func viewWillAppear(animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
self.keyboardTracker.startTracking()
|
||||
}
|
||||
|
||||
open override func viewWillDisappear(_ animated: Bool) {
|
||||
public override func viewWillDisappear(animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
self.keyboardTracker.stopTracking()
|
||||
}
|
||||
|
||||
private func addCollectionView() {
|
||||
self.collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: self.createCollectionViewLayout())
|
||||
self.collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: self.createCollectionViewLayout)
|
||||
self.collectionView.contentInset = self.constants.defaultContentInsets
|
||||
self.collectionView.scrollIndicatorInsets = self.constants.defaultScrollIndicatorInsets
|
||||
self.collectionView.alwaysBounceVertical = true
|
||||
self.collectionView.backgroundColor = UIColor.clear
|
||||
self.collectionView.keyboardDismissMode = .interactive
|
||||
self.collectionView.backgroundColor = UIColor.clearColor()
|
||||
self.collectionView.keyboardDismissMode = .Interactive
|
||||
self.collectionView.showsVerticalScrollIndicator = true
|
||||
self.collectionView.showsHorizontalScrollIndicator = false
|
||||
self.collectionView.allowsSelection = false
|
||||
self.collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.collectionView.autoresizingMask = UIViewAutoresizing()
|
||||
self.collectionView.autoresizingMask = .None
|
||||
self.view.addSubview(self.collectionView)
|
||||
self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: .top, relatedBy: .equal, toItem: self.collectionView, attribute: .top, multiplier: 1, constant: 0))
|
||||
self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: .leading, relatedBy: .equal, toItem: self.collectionView, attribute: .leading, multiplier: 1, constant: 0))
|
||||
self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: .bottom, relatedBy: .equal, toItem: self.collectionView, attribute: .bottom, multiplier: 1, constant: 0))
|
||||
self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: .trailing, relatedBy: .equal, toItem: self.collectionView, attribute: .trailing, multiplier: 1, constant: 0))
|
||||
self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: .Top, relatedBy: .Equal, toItem: self.collectionView, attribute: .Top, multiplier: 1, constant: 0))
|
||||
self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: .Leading, relatedBy: .Equal, toItem: self.collectionView, attribute: .Leading, multiplier: 1, constant: 0))
|
||||
self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: .Bottom, relatedBy: .Equal, toItem: self.collectionView, attribute: .Bottom, multiplier: 1, constant: 0))
|
||||
self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: .Trailing, relatedBy: .Equal, toItem: self.collectionView, attribute: .Trailing, multiplier: 1, constant: 0))
|
||||
self.collectionView.dataSource = self
|
||||
self.collectionView.delegate = self
|
||||
self.accessoryViewRevealer = AccessoryViewRevealer(collectionView: self.collectionView)
|
||||
|
|
@ -140,25 +140,25 @@ open class BaseChatViewController: UIViewController, UICollectionViewDataSource,
|
|||
private var inputContainerBottomConstraint: NSLayoutConstraint!
|
||||
private func addInputViews() {
|
||||
self.inputContainer = UIView(frame: CGRect.zero)
|
||||
self.inputContainer.autoresizingMask = UIViewAutoresizing()
|
||||
self.inputContainer.autoresizingMask = .None
|
||||
self.inputContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.view.addSubview(self.inputContainer)
|
||||
self.view.addConstraint(NSLayoutConstraint(item: self.inputContainer, attribute: .top, relatedBy: .greaterThanOrEqual, toItem: self.topLayoutGuide, attribute: .bottom, multiplier: 1, constant: 0))
|
||||
self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: .leading, relatedBy: .equal, toItem: self.inputContainer, attribute: .leading, multiplier: 1, constant: 0))
|
||||
self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: .trailing, relatedBy: .equal, toItem: self.inputContainer, attribute: .trailing, multiplier: 1, constant: 0))
|
||||
self.inputContainerBottomConstraint = NSLayoutConstraint(item: self.view, attribute: .bottom, relatedBy: .equal, toItem: self.inputContainer, attribute: .bottom, multiplier: 1, constant: 0)
|
||||
self.view.addConstraint(NSLayoutConstraint(item: self.inputContainer, attribute: .Top, relatedBy: .GreaterThanOrEqual, toItem: self.topLayoutGuide, attribute: .Bottom, multiplier: 1, constant: 0))
|
||||
self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: .Leading, relatedBy: .Equal, toItem: self.inputContainer, attribute: .Leading, multiplier: 1, constant: 0))
|
||||
self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: .Trailing, relatedBy: .Equal, toItem: self.inputContainer, attribute: .Trailing, multiplier: 1, constant: 0))
|
||||
self.inputContainerBottomConstraint = NSLayoutConstraint(item: self.view, attribute: .Bottom, relatedBy: .Equal, toItem: self.inputContainer, attribute: .Bottom, multiplier: 1, constant: 0)
|
||||
self.view.addConstraint(self.inputContainerBottomConstraint)
|
||||
|
||||
let inputView = self.createChatInputView()
|
||||
self.inputContainer.addSubview(inputView)
|
||||
self.inputContainer.addConstraint(NSLayoutConstraint(item: self.inputContainer, attribute: .top, relatedBy: .equal, toItem: inputView, attribute: .top, multiplier: 1, constant: 0))
|
||||
self.inputContainer.addConstraint(NSLayoutConstraint(item: self.inputContainer, attribute: .leading, relatedBy: .equal, toItem: inputView, attribute: .leading, multiplier: 1, constant: 0))
|
||||
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))
|
||||
self.inputContainer.addConstraint(NSLayoutConstraint(item: self.inputContainer, attribute: .Top, relatedBy: .Equal, toItem: inputView, attribute: .Top, multiplier: 1, constant: 0))
|
||||
self.inputContainer.addConstraint(NSLayoutConstraint(item: self.inputContainer, attribute: .Leading, relatedBy: .Equal, toItem: inputView, attribute: .Leading, multiplier: 1, constant: 0))
|
||||
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))
|
||||
}
|
||||
|
||||
var isAdjustingInputContainer: Bool = false
|
||||
open func setupKeyboardTracker() {
|
||||
public func setupKeyboardTracker() {
|
||||
let layoutBlock = { [weak self] (bottomMargin: CGFloat) in
|
||||
guard let sSelf = self else { return }
|
||||
sSelf.isAdjustingInputContainer = true
|
||||
|
|
@ -170,11 +170,11 @@ open class BaseChatViewController: UIViewController, UICollectionViewDataSource,
|
|||
(self.view as? BaseChatViewControllerView)?.bmaInputAccessoryView = self.keyboardTracker?.trackingView
|
||||
}
|
||||
|
||||
var notificationCenter = NotificationCenter.default
|
||||
var notificationCenter = NSNotificationCenter.defaultCenter()
|
||||
var keyboardTracker: KeyboardTracker!
|
||||
|
||||
public private(set) var isFirstLayout: Bool = true
|
||||
override open func viewDidLayoutSubviews() {
|
||||
public var isFirstLayout: Bool = true
|
||||
override public func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
self.adjustCollectionViewInsets()
|
||||
|
|
@ -185,8 +185,7 @@ open class BaseChatViewController: UIViewController, UICollectionViewDataSource,
|
|||
self.isFirstLayout = false
|
||||
// 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)
|
||||
|
||||
if self.hidesBottomBarWhenPushed && (navigationController?.viewControllers.count ?? 0) > 1 && navigationController?.viewControllers.last == self {
|
||||
if hidesBottomBarWhenPushed && navigationController?.viewControllers.count > 1 && navigationController?.viewControllers.last == self {
|
||||
self.inputContainerBottomConstraint.constant = 0
|
||||
} else {
|
||||
self.inputContainerBottomConstraint.constant = self.bottomLayoutGuide.length
|
||||
|
|
@ -195,7 +194,7 @@ open class BaseChatViewController: UIViewController, UICollectionViewDataSource,
|
|||
}
|
||||
|
||||
private func adjustCollectionViewInsets() {
|
||||
let isInteracting = self.collectionView.panGestureRecognizer.numberOfTouches > 0
|
||||
let isInteracting = self.collectionView.panGestureRecognizer.numberOfTouches() > 0
|
||||
let isBouncingAtTop = isInteracting && self.collectionView.contentOffset.y < -self.collectionView.contentInset.top
|
||||
if isBouncingAtTop { return }
|
||||
|
||||
|
|
@ -204,7 +203,7 @@ open class BaseChatViewController: UIViewController, UICollectionViewDataSource,
|
|||
let insetBottomDiff = newInsetBottom - self.collectionView.contentInset.bottom
|
||||
let newInsetTop = self.topLayoutGuide.length + self.constants.defaultContentInsets.top
|
||||
|
||||
let contentSize = self.collectionView.collectionViewLayout.collectionViewContentSize
|
||||
let contentSize = self.collectionView.collectionViewLayout.collectionViewContentSize()
|
||||
let allContentFits: Bool = {
|
||||
let availableHeight = self.collectionView.bounds.height - (newInsetTop + newInsetBottom)
|
||||
return availableHeight >= contentSize.height
|
||||
|
|
@ -240,9 +239,9 @@ open class BaseChatViewController: UIViewController, UICollectionViewDataSource,
|
|||
}
|
||||
}
|
||||
|
||||
func rectAtIndexPath(_ indexPath: IndexPath?) -> CGRect? {
|
||||
func rectAtIndexPath(indexPath: NSIndexPath?) -> CGRect? {
|
||||
if let indexPath = indexPath {
|
||||
return self.collectionView.collectionViewLayout.layoutAttributesForItem(at: indexPath)?.frame
|
||||
return self.collectionView.collectionViewLayout.layoutAttributesForItemAtIndexPath(indexPath)?.frame
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -251,8 +250,8 @@ open class BaseChatViewController: UIViewController, UICollectionViewDataSource,
|
|||
var accessoryViewRevealer: AccessoryViewRevealer!
|
||||
public private(set) var inputContainer: UIView!
|
||||
var presenterFactory: ChatItemPresenterFactoryProtocol!
|
||||
let presentersByCell = NSMapTable<UICollectionViewCell, AnyObject>(keyOptions: .weakMemory, valueOptions: .weakMemory)
|
||||
var visibleCells: [IndexPath: UICollectionViewCell] = [:] // @see visibleCellsAreValid(changes:)
|
||||
let presentersByCell = NSMapTable(keyOptions: .WeakMemory, valueOptions: .WeakMemory)
|
||||
var visibleCells: [NSIndexPath: UICollectionViewCell] = [:] // @see visibleCellsAreValid(changes:)
|
||||
|
||||
public internal(set) var updateQueue: SerialTaskQueueProtocol = SerialTaskQueue()
|
||||
|
||||
|
|
@ -264,7 +263,7 @@ open class BaseChatViewController: UIViewController, UICollectionViewDataSource,
|
|||
*/
|
||||
public var chatItemsDecorator: ChatItemsDecoratorProtocol?
|
||||
|
||||
open func createCollectionViewLayout() -> UICollectionViewLayout {
|
||||
public var createCollectionViewLayout: UICollectionViewLayout {
|
||||
let layout = ChatCollectionViewLayout()
|
||||
layout.delegate = self
|
||||
return layout
|
||||
|
|
@ -274,17 +273,17 @@ open class BaseChatViewController: UIViewController, UICollectionViewDataSource,
|
|||
|
||||
// MARK: Subclass overrides
|
||||
|
||||
open func createPresenterFactory() -> ChatItemPresenterFactoryProtocol {
|
||||
public func createPresenterFactory() -> ChatItemPresenterFactoryProtocol {
|
||||
// Default implementation
|
||||
return ChatItemPresenterFactory(presenterBuildersByType: self.createPresenterBuilders())
|
||||
}
|
||||
|
||||
open func createPresenterBuilders() -> [ChatItemType: [ChatItemPresenterBuilderProtocol]] {
|
||||
public func createPresenterBuilders() -> [ChatItemType: [ChatItemPresenterBuilderProtocol]] {
|
||||
assert(false, "Override in subclass")
|
||||
return [ChatItemType: [ChatItemPresenterBuilderProtocol]]()
|
||||
}
|
||||
|
||||
open func createChatInputView() -> UIView {
|
||||
public func createChatInputView() -> UIView {
|
||||
assert(false, "Override in subclass")
|
||||
return UIView()
|
||||
}
|
||||
|
|
@ -293,20 +292,20 @@ open class BaseChatViewController: UIViewController, UICollectionViewDataSource,
|
|||
When paginating up we need to change the scroll position as the content is pushed down.
|
||||
We take distance to top from beforeUpdate indexPath and then we make afterUpdate indexPath to appear at the same distance
|
||||
*/
|
||||
open func referenceIndexPathsToRestoreScrollPositionOnUpdate(itemsBeforeUpdate: ChatItemCompanionCollection, changes: CollectionChanges) -> (beforeUpdate: IndexPath?, afterUpdate: IndexPath?) {
|
||||
public func referenceIndexPathsToRestoreScrollPositionOnUpdate(itemsBeforeUpdate itemsBeforeUpdate: ChatItemCompanionCollection, changes: CollectionChanges) -> (beforeUpdate: NSIndexPath?, afterUpdate: NSIndexPath?) {
|
||||
let firstItemMoved = changes.movedIndexPaths.first
|
||||
return (firstItemMoved?.indexPathOld as IndexPath?, firstItemMoved?.indexPathNew as IndexPath?)
|
||||
return (firstItemMoved?.indexPathOld, firstItemMoved?.indexPathNew)
|
||||
}
|
||||
}
|
||||
|
||||
extension BaseChatViewController { // Rotation
|
||||
|
||||
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
public override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
|
||||
let shouldScrollToBottom = self.isScrolledAtBottom()
|
||||
let referenceIndexPath = self.collectionView.indexPathsForVisibleItems.first
|
||||
let referenceIndexPath = self.collectionView.indexPathsForVisibleItems().first
|
||||
let oldRect = self.rectAtIndexPath(referenceIndexPath)
|
||||
coordinator.animate(alongsideTransition: { (context) -> Void in
|
||||
coordinator.animateAlongsideTransition({ (context) -> Void in
|
||||
if shouldScrollToBottom {
|
||||
self.scrollToBottom(animated: false)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ public protocol AccessoryViewRevealable {
|
|||
|
||||
public struct AccessoryViewRevealerConfig {
|
||||
public let angleThresholdInRads: CGFloat
|
||||
public let translationTransform: (_ rawTranslation: CGFloat) -> CGFloat
|
||||
public init(angleThresholdInRads: CGFloat, translationTransform: @escaping (_ rawTranslation: CGFloat) -> CGFloat) {
|
||||
public let translationTransform: (rawTranslation: CGFloat) -> CGFloat
|
||||
public init(angleThresholdInRads: CGFloat, translationTransform: (rawTranslation: CGFloat) -> CGFloat) {
|
||||
self.angleThresholdInRads = angleThresholdInRads
|
||||
self.translationTransform = translationTransform
|
||||
}
|
||||
|
|
@ -68,51 +68,51 @@ class AccessoryViewRevealer: NSObject, UIGestureRecognizerDelegate {
|
|||
|
||||
var isEnabled: Bool = true {
|
||||
didSet {
|
||||
self.panRecognizer.isEnabled = self.isEnabled
|
||||
self.panRecognizer.enabled = self.isEnabled
|
||||
}
|
||||
}
|
||||
|
||||
var config = AccessoryViewRevealerConfig.defaultConfig()
|
||||
|
||||
@objc
|
||||
private func handlePan(_ panRecognizer: UIPanGestureRecognizer) {
|
||||
private func handlePan(panRecognizer: UIPanGestureRecognizer) {
|
||||
switch panRecognizer.state {
|
||||
case .began:
|
||||
case .Began:
|
||||
break
|
||||
case .changed:
|
||||
let translation = panRecognizer.translation(in: self.collectionView)
|
||||
self.revealAccessoryView(atOffset: self.config.translationTransform(-translation.x))
|
||||
case .ended, .cancelled, .failed:
|
||||
case .Changed:
|
||||
let translation = panRecognizer.translationInView(self.collectionView)
|
||||
self.revealAccessoryView(atOffset: self.config.translationTransform(rawTranslation: -translation.x))
|
||||
case .Ended, .Cancelled, .Failed:
|
||||
self.revealAccessoryView(atOffset: 0)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer != self.panRecognizer {
|
||||
return true
|
||||
}
|
||||
|
||||
let translation = self.panRecognizer.translation(in: self.collectionView)
|
||||
let x = abs(translation.x), y = abs(translation.y)
|
||||
let translation = self.panRecognizer.translationInView(self.collectionView)
|
||||
let x = CGFloat.abs(translation.x), y = CGFloat.abs(translation.y)
|
||||
let angleRads = atan2(y, x)
|
||||
return angleRads <= self.config.angleThresholdInRads
|
||||
}
|
||||
|
||||
private func revealAccessoryView(atOffset offset: CGFloat) {
|
||||
// Find max offset (cells can have slighlty different timestamp size ( 3.00 am vs 11.37 pm )
|
||||
let cells: [AccessoryViewRevealable] = self.collectionView.visibleCells.flatMap({ $0 as? AccessoryViewRevealable })
|
||||
let cells: [AccessoryViewRevealable] = self.collectionView.visibleCells().filter({$0 is AccessoryViewRevealable}).map({$0 as! AccessoryViewRevealable})
|
||||
let offset = min(offset, cells.reduce(0) { (current, cell) -> CGFloat in
|
||||
return max(current, cell.preferredOffsetToRevealAccessoryView() ?? 0)
|
||||
})
|
||||
|
||||
for cell in self.collectionView.visibleCells {
|
||||
if let cell = cell as? AccessoryViewRevealable, cell.allowAccessoryViewRevealing {
|
||||
for cell in self.collectionView.visibleCells() {
|
||||
if let cell = cell as? AccessoryViewRevealable where cell.allowAccessoryViewRevealing {
|
||||
cell.revealAccessoryView(withOffset: offset, animated: offset == 0)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,18 +34,18 @@ public struct ChatCollectionViewLayoutModel {
|
|||
let layoutAttributesBySectionAndItem: [[UICollectionViewLayoutAttributes]]
|
||||
let calculatedForWidth: CGFloat
|
||||
|
||||
public static func createModel(_ collectionViewWidth: CGFloat, itemsLayoutData: [(height: CGFloat, bottomMargin: CGFloat)]) -> ChatCollectionViewLayoutModel {
|
||||
public static func createModel(collectionViewWidth: CGFloat, itemsLayoutData: [(height: CGFloat, bottomMargin: CGFloat)]) -> ChatCollectionViewLayoutModel {
|
||||
var layoutAttributes = [UICollectionViewLayoutAttributes]()
|
||||
var layoutAttributesBySectionAndItem = [[UICollectionViewLayoutAttributes]]()
|
||||
layoutAttributesBySectionAndItem.append([UICollectionViewLayoutAttributes]())
|
||||
|
||||
var verticalOffset: CGFloat = 0
|
||||
for (index, layoutData) in itemsLayoutData.enumerated() {
|
||||
let indexPath = IndexPath(item: index, section: 0)
|
||||
for (index, layoutData) in itemsLayoutData.enumerate() {
|
||||
let indexPath = NSIndexPath(forItem: index, inSection: 0)
|
||||
let (height, bottomMargin) = layoutData
|
||||
let itemSize = CGSize(width: collectionViewWidth, height: height)
|
||||
let frame = CGRect(origin: CGPoint(x: 0, y: verticalOffset), size: itemSize)
|
||||
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
|
||||
let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
|
||||
attributes.frame = frame
|
||||
layoutAttributes.append(attributes)
|
||||
layoutAttributesBySectionAndItem[0].append(attributes)
|
||||
|
|
@ -62,26 +62,27 @@ public struct ChatCollectionViewLayoutModel {
|
|||
}
|
||||
}
|
||||
|
||||
open class ChatCollectionViewLayout: UICollectionViewLayout {
|
||||
|
||||
public class ChatCollectionViewLayout: UICollectionViewLayout {
|
||||
var layoutModel: ChatCollectionViewLayoutModel!
|
||||
public weak var delegate: ChatCollectionViewLayoutDelegate?
|
||||
|
||||
// Optimization: after reloadData we'll get invalidateLayout, but prepareLayout will be delayed until next run loop.
|
||||
// Client may need to force prepareLayout after reloadData, but we don't want to compute layout again in the next run loop.
|
||||
private var layoutNeedsUpdate = true
|
||||
open override func invalidateLayout() {
|
||||
public override func invalidateLayout() {
|
||||
super.invalidateLayout()
|
||||
self.layoutNeedsUpdate = true
|
||||
}
|
||||
|
||||
open override func prepare() {
|
||||
super.prepare()
|
||||
public override func prepareLayout() {
|
||||
super.prepareLayout()
|
||||
guard self.layoutNeedsUpdate else { return }
|
||||
guard let delegate = self.delegate else { return }
|
||||
var oldLayoutModel = self.layoutModel
|
||||
self.layoutModel = delegate.chatCollectionViewLayoutModel()
|
||||
self.layoutNeedsUpdate = false
|
||||
DispatchQueue.global(qos: .default).async { () -> Void in
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
|
||||
// Dealloc of layout with 5000 items take 25 ms on tests on iPhone 4s
|
||||
// This moves dealloc out of main thread
|
||||
if oldLayoutModel != nil {
|
||||
|
|
@ -91,15 +92,15 @@ open class ChatCollectionViewLayout: UICollectionViewLayout {
|
|||
}
|
||||
}
|
||||
|
||||
open override var collectionViewContentSize: CGSize {
|
||||
public override func collectionViewContentSize() -> CGSize {
|
||||
return self.layoutModel?.contentSize ?? .zero
|
||||
}
|
||||
|
||||
open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
|
||||
override public func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
|
||||
return self.layoutModel.layoutAttributes.filter { $0.frame.intersects(rect) }
|
||||
}
|
||||
|
||||
open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
|
||||
public override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
|
||||
if indexPath.section < self.layoutModel.layoutAttributesBySectionAndItem.count && indexPath.item < self.layoutModel.layoutAttributesBySectionAndItem[indexPath.section].count {
|
||||
return self.layoutModel.layoutAttributesBySectionAndItem[indexPath.section][indexPath.item]
|
||||
}
|
||||
|
|
@ -107,7 +108,7 @@ open class ChatCollectionViewLayout: UICollectionViewLayout {
|
|||
return nil
|
||||
}
|
||||
|
||||
open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
|
||||
public override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
|
||||
return self.layoutModel.calculatedForWidth != newBounds.width
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,16 +25,16 @@
|
|||
import Foundation
|
||||
|
||||
public enum UpdateType {
|
||||
case normal
|
||||
case firstLoad
|
||||
case pagination
|
||||
case reload
|
||||
case messageCountReduction
|
||||
case Normal
|
||||
case FirstLoad
|
||||
case Pagination
|
||||
case Reload
|
||||
case MessageCountReduction
|
||||
}
|
||||
|
||||
public protocol ChatDataSourceDelegateProtocol: class {
|
||||
func chatDataSourceDidUpdate(_ chatDataSource: ChatDataSourceProtocol)
|
||||
func chatDataSourceDidUpdate(_ chatDataSource: ChatDataSourceProtocol, updateType: UpdateType)
|
||||
func chatDataSourceDidUpdate(chatDataSource: ChatDataSourceProtocol)
|
||||
func chatDataSourceDidUpdate(chatDataSource: ChatDataSourceProtocol, updateType: UpdateType)
|
||||
}
|
||||
|
||||
public protocol ChatDataSourceProtocol: class {
|
||||
|
|
@ -45,5 +45,5 @@ public protocol ChatDataSourceProtocol: class {
|
|||
|
||||
func loadNext() // Should trigger chatDataSourceDidUpdate with UpdateType.Pagination
|
||||
func loadPrevious() // Should trigger chatDataSourceDidUpdate with UpdateType.Pagination
|
||||
func adjustNumberOfMessages(preferredMaxCount: Int?, focusPosition: Double, completion:((didAdjust: Bool)) -> Void) // If you want, implement message count contention for performance, otherwise just call completion(false)
|
||||
func adjustNumberOfMessages(preferredMaxCount preferredMaxCount: Int?, focusPosition: Double, completion:(didAdjust: Bool) -> Void) // If you want, implement message count contention for performance, otherwise just call completion(false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
import Foundation
|
||||
|
||||
public protocol ChatItemPresenterFactoryProtocol {
|
||||
func createChatItemPresenter(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol
|
||||
func createChatItemPresenter(chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol
|
||||
func configure(withCollectionView collectionView: UICollectionView)
|
||||
}
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ final class ChatItemPresenterFactory: ChatItemPresenterFactoryProtocol {
|
|||
self.presenterBuildersByType = presenterBuildersByType
|
||||
}
|
||||
|
||||
func createChatItemPresenter(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol {
|
||||
func createChatItemPresenter(chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol {
|
||||
for builder in self.presenterBuildersByType[chatItem.type] ?? [] {
|
||||
if builder.canHandleChatItem(chatItem) {
|
||||
return builder.createPresenterWithChatItem(chatItem)
|
||||
|
|
|
|||
|
|
@ -29,14 +29,14 @@ public protocol UniqueIdentificable {
|
|||
}
|
||||
|
||||
public struct CollectionChangeMove: Equatable, Hashable {
|
||||
public let indexPathOld: IndexPath
|
||||
public let indexPathNew: IndexPath
|
||||
public init(indexPathOld: IndexPath, indexPathNew: IndexPath) {
|
||||
public let indexPathOld: NSIndexPath
|
||||
public let indexPathNew: NSIndexPath
|
||||
public init(indexPathOld: NSIndexPath, indexPathNew: NSIndexPath) {
|
||||
self.indexPathOld = indexPathOld
|
||||
self.indexPathNew = indexPathNew
|
||||
}
|
||||
|
||||
public var hashValue: Int { return indexPathOld.hashValue ^ indexPathNew.hashValue }
|
||||
public var hashValue: Int { return indexPathOld.hash ^ indexPathNew.hash }
|
||||
}
|
||||
|
||||
public func == (lhs: CollectionChangeMove, rhs: CollectionChangeMove) -> Bool {
|
||||
|
|
@ -44,21 +44,21 @@ public func == (lhs: CollectionChangeMove, rhs: CollectionChangeMove) -> Bool {
|
|||
}
|
||||
|
||||
public struct CollectionChanges {
|
||||
public let insertedIndexPaths: Set<IndexPath>
|
||||
public let deletedIndexPaths: Set<IndexPath>
|
||||
public let insertedIndexPaths: Set<NSIndexPath>
|
||||
public let deletedIndexPaths: Set<NSIndexPath>
|
||||
public let movedIndexPaths: [CollectionChangeMove]
|
||||
|
||||
init(insertedIndexPaths: Set<IndexPath>, deletedIndexPaths: Set<IndexPath>, movedIndexPaths: [CollectionChangeMove]) {
|
||||
init(insertedIndexPaths: Set<NSIndexPath>, deletedIndexPaths: Set<NSIndexPath>, movedIndexPaths: [CollectionChangeMove]) {
|
||||
self.insertedIndexPaths = insertedIndexPaths
|
||||
self.deletedIndexPaths = deletedIndexPaths
|
||||
self.movedIndexPaths = movedIndexPaths
|
||||
}
|
||||
}
|
||||
|
||||
func generateChanges(oldCollection: [UniqueIdentificable], newCollection: [UniqueIdentificable]) -> CollectionChanges {
|
||||
func generateIndexesById(_ uids: [String]) -> [String: Int] {
|
||||
func generateChanges(oldCollection oldCollection: [UniqueIdentificable], newCollection: [UniqueIdentificable]) -> CollectionChanges {
|
||||
func generateIndexesById(uids: [String]) -> [String: Int] {
|
||||
var map = [String: Int](minimumCapacity: uids.count)
|
||||
for (index, uid) in uids.enumerated() {
|
||||
for (index, uid) in uids.enumerate() {
|
||||
map[uid] = index
|
||||
}
|
||||
return map
|
||||
|
|
@ -68,25 +68,25 @@ func generateChanges(oldCollection: [UniqueIdentificable], newCollection: [Uniqu
|
|||
let newIds = newCollection.map { $0.uid }
|
||||
let oldIndexsById = generateIndexesById(oldIds)
|
||||
let newIndexsById = generateIndexesById(newIds)
|
||||
var deletedIndexPaths = Set<IndexPath>()
|
||||
var insertedIndexPaths = Set<IndexPath>()
|
||||
var deletedIndexPaths = Set<NSIndexPath>()
|
||||
var insertedIndexPaths = Set<NSIndexPath>()
|
||||
var movedIndexPaths = [CollectionChangeMove]()
|
||||
|
||||
// Deletetions
|
||||
for oldId in oldIds {
|
||||
let isDeleted = newIndexsById[oldId] == nil
|
||||
if isDeleted {
|
||||
deletedIndexPaths.insert(IndexPath(item: oldIndexsById[oldId]!, section: 0))
|
||||
deletedIndexPaths.insert(NSIndexPath(forItem: oldIndexsById[oldId]!, inSection: 0))
|
||||
}
|
||||
}
|
||||
|
||||
// Insertions and movements
|
||||
for newId in newIds {
|
||||
let newIndex = newIndexsById[newId]!
|
||||
let newIndexPath = IndexPath(item: newIndex, section: 0)
|
||||
let newIndexPath = NSIndexPath(forItem: newIndex, inSection: 0)
|
||||
if let oldIndex = oldIndexsById[newId] {
|
||||
if oldIndex != newIndex {
|
||||
movedIndexPaths.append(CollectionChangeMove(indexPathOld: IndexPath(item: oldIndex, section: 0), indexPathNew: newIndexPath))
|
||||
movedIndexPaths.append(CollectionChangeMove(indexPathOld: NSIndexPath(forItem: oldIndex, inSection: 0), indexPathNew: newIndexPath))
|
||||
}
|
||||
} else {
|
||||
// It's new
|
||||
|
|
@ -97,14 +97,14 @@ func generateChanges(oldCollection: [UniqueIdentificable], newCollection: [Uniqu
|
|||
return CollectionChanges(insertedIndexPaths: insertedIndexPaths, deletedIndexPaths: deletedIndexPaths, movedIndexPaths: movedIndexPaths)
|
||||
}
|
||||
|
||||
func updated<T: Any>(collection: [IndexPath: T], withChanges changes: CollectionChanges) -> [IndexPath: T] {
|
||||
func updated<T: Any>(collection collection: [NSIndexPath: T], withChanges changes: CollectionChanges) -> [NSIndexPath: T] {
|
||||
var result = collection
|
||||
|
||||
changes.deletedIndexPaths.forEach { (indexPath) in
|
||||
result[indexPath] = nil
|
||||
}
|
||||
|
||||
var movedDestinations = Set<IndexPath>()
|
||||
var movedDestinations = Set<NSIndexPath>()
|
||||
changes.movedIndexPaths.forEach { (move) in
|
||||
result[move.indexPathNew] = collection[move.indexPathOld]
|
||||
movedDestinations.insert(move.indexPathNew)
|
||||
|
|
|
|||
|
|
@ -27,12 +27,12 @@ import Foundation
|
|||
class KeyboardTracker {
|
||||
|
||||
private enum KeyboardStatus {
|
||||
case hidden
|
||||
case showing
|
||||
case shown
|
||||
case Hidden
|
||||
case Showing
|
||||
case Shown
|
||||
}
|
||||
|
||||
private var keyboardStatus: KeyboardStatus = .hidden
|
||||
private var keyboardStatus: KeyboardStatus = .Hidden
|
||||
private let view: UIView
|
||||
var trackingView: UIView {
|
||||
return self.keyboardTrackerView
|
||||
|
|
@ -50,20 +50,20 @@ class KeyboardTracker {
|
|||
|
||||
var isTracking = false
|
||||
var inputContainer: UIView
|
||||
private var notificationCenter: NotificationCenter
|
||||
private var notificationCenter: NSNotificationCenter
|
||||
|
||||
typealias LayoutBlock = (_ bottomMargin: CGFloat) -> Void
|
||||
typealias LayoutBlock = (bottomMargin: CGFloat) -> Void
|
||||
private var layoutBlock: LayoutBlock
|
||||
|
||||
init(viewController: UIViewController, inputContainer: UIView, layoutBlock: @escaping LayoutBlock, notificationCenter: NotificationCenter) {
|
||||
init(viewController: UIViewController, inputContainer: UIView, layoutBlock: LayoutBlock, notificationCenter: NSNotificationCenter) {
|
||||
self.view = viewController.view
|
||||
self.layoutBlock = layoutBlock
|
||||
self.inputContainer = inputContainer
|
||||
self.notificationCenter = notificationCenter
|
||||
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.keyboardWillChangeFrame(_:)), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
|
||||
self.notificationCenter.addObserver(self, selector: #selector(KeyboardTracker.keyboardWillShow(_:)), name: UIKeyboardWillShowNotification, object: nil)
|
||||
self.notificationCenter.addObserver(self, selector: #selector(KeyboardTracker.keyboardDidShow(_:)), name: UIKeyboardDidShowNotification, object: nil)
|
||||
self.notificationCenter.addObserver(self, selector: #selector(KeyboardTracker.keyboardWillHide(_:)), name: UIKeyboardWillHideNotification, object: nil)
|
||||
self.notificationCenter.addObserver(self, selector: #selector(KeyboardTracker.keyboardWillChangeFrame(_:)), name: UIKeyboardWillChangeFrameNotification, object: nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
|
@ -79,60 +79,60 @@ class KeyboardTracker {
|
|||
}
|
||||
|
||||
@objc
|
||||
private func keyboardWillShow(_ notification: Notification) {
|
||||
private func keyboardWillShow(notification: NSNotification) {
|
||||
guard self.isTracking else { return }
|
||||
guard !self.isPerformingForcedLayout else { return }
|
||||
guard !self.isPerformingForcedLayout else { return}
|
||||
let bottomConstraint = self.bottomConstraintFromNotification(notification)
|
||||
guard bottomConstraint > 0 else { return } // Some keyboards may report initial willShow/DidShow notifications with invalid positions
|
||||
self.keyboardStatus = .showing
|
||||
self.keyboardStatus = .Showing
|
||||
self.layoutInputContainer(withBottomConstraint: bottomConstraint)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func keyboardDidShow(_ notification: Notification) {
|
||||
private func keyboardDidShow(notification: NSNotification) {
|
||||
guard self.isTracking else { return }
|
||||
guard !self.isPerformingForcedLayout else { return }
|
||||
guard !self.isPerformingForcedLayout else { return}
|
||||
|
||||
let bottomConstraint = self.bottomConstraintFromNotification(notification)
|
||||
guard bottomConstraint > 0 else { return } // Some keyboards may report initial willShow/DidShow notifications with invalid positions
|
||||
self.keyboardStatus = .shown
|
||||
self.keyboardStatus = .Shown
|
||||
self.layoutInputContainer(withBottomConstraint: bottomConstraint)
|
||||
self.adjustTrackingViewSizeIfNeeded()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func keyboardWillChangeFrame(_ notification: Notification) {
|
||||
private func keyboardWillChangeFrame(notification: NSNotification) {
|
||||
guard self.isTracking else { return }
|
||||
let bottomConstraint = self.bottomConstraintFromNotification(notification)
|
||||
if bottomConstraint == 0 {
|
||||
self.keyboardStatus = .hidden
|
||||
self.keyboardStatus = .Hidden
|
||||
self.layoutInputAtBottom()
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func keyboardWillHide(_ notification: Notification) {
|
||||
private func keyboardWillHide(notification: NSNotification) {
|
||||
guard self.isTracking else { return }
|
||||
self.keyboardStatus = .hidden
|
||||
self.keyboardStatus = .Hidden
|
||||
self.layoutInputAtBottom()
|
||||
}
|
||||
|
||||
private func bottomConstraintFromNotification(_ notification: Notification) -> CGFloat {
|
||||
guard let rect = ((notification as NSNotification).userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { return 0 }
|
||||
private func bottomConstraintFromNotification(notification: NSNotification) -> CGFloat {
|
||||
guard let rect = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.CGRectValue() else { return 0 }
|
||||
guard rect.height > 0 else { return 0 }
|
||||
let rectInView = self.view.convert(rect, from: nil)
|
||||
let rectInView = self.view.convertRect(rect, fromView: nil)
|
||||
guard rectInView.maxY >=~ self.view.bounds.height else { return 0 } // Undocked keyboard
|
||||
return max(0, self.view.bounds.height - rectInView.minY - self.keyboardTrackerView.intrinsicContentSize.height)
|
||||
return max(0, self.view.bounds.height - rectInView.minY - self.keyboardTrackerView.intrinsicContentSize().height)
|
||||
}
|
||||
|
||||
private func bottomConstraintFromTrackingView() -> CGFloat {
|
||||
guard self.keyboardTrackerView.superview != nil else { return 0 }
|
||||
let trackingViewRect = self.view.convert(self.keyboardTrackerView.bounds, from: self.keyboardTrackerView)
|
||||
let trackingViewRect = self.view.convertRect(self.keyboardTrackerView.bounds, fromView: self.keyboardTrackerView)
|
||||
return max(0, self.view.bounds.height - trackingViewRect.maxY)
|
||||
}
|
||||
|
||||
func adjustTrackingViewSizeIfNeeded() {
|
||||
guard self.isTracking && self.keyboardStatus == .shown else { return }
|
||||
guard self.isTracking && self.keyboardStatus == .Shown else { return }
|
||||
self.adjustTrackingViewSize()
|
||||
}
|
||||
|
||||
|
|
@ -153,13 +153,13 @@ class KeyboardTracker {
|
|||
|
||||
var isPerformingForcedLayout: Bool = false
|
||||
func layoutInputAtTrackingViewIfNeeded() {
|
||||
guard self.isTracking && self.keyboardStatus == .shown else { return }
|
||||
guard self.isTracking && self.keyboardStatus == .Shown else { return }
|
||||
self.layoutInputContainer(withBottomConstraint: self.bottomConstraintFromTrackingView())
|
||||
}
|
||||
|
||||
private func layoutInputContainer(withBottomConstraint constraint: CGFloat) {
|
||||
self.isPerformingForcedLayout = true
|
||||
self.layoutBlock(constraint)
|
||||
self.layoutBlock(bottomMargin: constraint)
|
||||
self.isPerformingForcedLayout = false
|
||||
}
|
||||
}
|
||||
|
|
@ -185,14 +185,14 @@ private class KeyboardTrackingView: UIView {
|
|||
self.commonInit()
|
||||
}
|
||||
|
||||
func commonInit() {
|
||||
self.autoresizingMask = .flexibleHeight
|
||||
self.isUserInteractionEnabled = false
|
||||
self.backgroundColor = UIColor.clear
|
||||
self.isHidden = true
|
||||
private func commonInit() {
|
||||
self.autoresizingMask = .FlexibleHeight
|
||||
self.userInteractionEnabled = false
|
||||
self.backgroundColor = UIColor.clearColor()
|
||||
self.hidden = true
|
||||
}
|
||||
|
||||
var preferredSize: CGSize = .zero {
|
||||
private var preferredSize: CGSize = .zero {
|
||||
didSet {
|
||||
if oldValue != self.preferredSize {
|
||||
self.invalidateIntrinsicContentSize()
|
||||
|
|
@ -201,30 +201,30 @@ private class KeyboardTrackingView: UIView {
|
|||
}
|
||||
}
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
private override func intrinsicContentSize() -> CGSize {
|
||||
return self.preferredSize
|
||||
}
|
||||
|
||||
override func willMove(toSuperview newSuperview: UIView?) {
|
||||
override func willMoveToSuperview(newSuperview: UIView?) {
|
||||
if let observedView = self.observedView {
|
||||
observedView.removeObserver(self, forKeyPath: "center")
|
||||
self.observedView = nil
|
||||
}
|
||||
|
||||
if let newSuperview = newSuperview {
|
||||
newSuperview.addObserver(self, forKeyPath: "center", options: [.new, .old], context: nil)
|
||||
newSuperview.addObserver(self, forKeyPath: "center", options: [.New, .Old], context: nil)
|
||||
self.observedView = newSuperview
|
||||
}
|
||||
|
||||
super.willMove(toSuperview: newSuperview)
|
||||
super.willMoveToSuperview(newSuperview)
|
||||
}
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||||
guard let object = object as? UIView, let superview = self.superview else { return }
|
||||
private override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
|
||||
guard let object = object, superview = self.superview else { return }
|
||||
if object === superview {
|
||||
guard let sChange = change else { return }
|
||||
let oldCenter = (sChange[NSKeyValueChangeKey.oldKey] as! NSValue).cgPointValue
|
||||
let newCenter = (sChange[NSKeyValueChangeKey.newKey] as! NSValue).cgPointValue
|
||||
let oldCenter = (sChange[NSKeyValueChangeOldKey] as! NSValue).CGPointValue()
|
||||
let newCenter = (sChange[NSKeyValueChangeNewKey] as! NSValue).CGPointValue()
|
||||
if oldCenter != newCenter {
|
||||
self.positionChangedCallback?()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>3.0.1</string>
|
||||
<string>2.1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
|
|
|||
|
|
@ -1,43 +1,43 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Badoo Trading Limited.
|
||||
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:
|
||||
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 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.
|
||||
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 Foundation
|
||||
|
||||
public struct ReadOnlyOrderedDictionary<T>: Collection where T: UniqueIdentificable {
|
||||
public struct ReadOnlyOrderedDictionary<T where T: UniqueIdentificable>: CollectionType {
|
||||
|
||||
private let items: [T]
|
||||
private let itemIndexesById: [String: Int] // Maping to the position in the array instead the item itself for better performance
|
||||
|
||||
public init(items: [T]) {
|
||||
var dictionary = [String: Int](minimumCapacity: items.count)
|
||||
for (index, item) in items.enumerated() {
|
||||
for (index, item) in items.enumerate() {
|
||||
dictionary[item.uid] = index
|
||||
}
|
||||
self.items = items
|
||||
self.itemIndexesById = dictionary
|
||||
}
|
||||
|
||||
public func indexOf(_ uid: String) -> Int? {
|
||||
public func indexOf(uid: String) -> Int? {
|
||||
return self.itemIndexesById[uid]
|
||||
}
|
||||
|
||||
|
|
@ -52,20 +52,8 @@ public struct ReadOnlyOrderedDictionary<T>: Collection where T: UniqueIdentifica
|
|||
return nil
|
||||
}
|
||||
|
||||
public func makeIterator() -> IndexingIterator<[T]> {
|
||||
return self.items.makeIterator()
|
||||
}
|
||||
|
||||
public func index(_ i: Int, offsetBy n: Int) -> Int {
|
||||
return self.items.index(i, offsetBy: n)
|
||||
}
|
||||
|
||||
public func index(_ i: Int, offsetBy n: Int, limitedBy limit: Int) -> Int? {
|
||||
return self.items.index(i, offsetBy: n, limitedBy: limit)
|
||||
}
|
||||
|
||||
public func index(after i: Int) -> Int {
|
||||
return self.items.index(after: i)
|
||||
public func generate() -> IndexingGenerator<[T]> {
|
||||
return self.items.generate()
|
||||
}
|
||||
|
||||
public var startIndex: Int {
|
||||
|
|
|
|||
|
|
@ -24,10 +24,10 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public typealias TaskClosure = (_ completion: @escaping () -> Void) -> Void
|
||||
public typealias TaskClosure = (completion: () -> Void) -> Void
|
||||
|
||||
public protocol SerialTaskQueueProtocol {
|
||||
func addTask(_ task: @escaping TaskClosure)
|
||||
func addTask(task: TaskClosure)
|
||||
func start()
|
||||
func stop()
|
||||
func flushQueue()
|
||||
|
|
@ -43,7 +43,7 @@ public final class SerialTaskQueue: SerialTaskQueueProtocol {
|
|||
|
||||
public init() {}
|
||||
|
||||
public func addTask(_ task: @escaping TaskClosure) {
|
||||
public func addTask(task: TaskClosure) {
|
||||
self.tasksQueue.append(task)
|
||||
self.maybeExecuteNextTask()
|
||||
}
|
||||
|
|
@ -70,7 +70,7 @@ public final class SerialTaskQueue: SerialTaskQueueProtocol {
|
|||
if !self.isEmpty {
|
||||
let firstTask = self.tasksQueue.removeFirst()
|
||||
self.isBusy = true
|
||||
firstTask({ [weak self] () -> Void in
|
||||
firstTask(completion: { [weak self] () -> Void in
|
||||
self?.isBusy = false
|
||||
self?.maybeExecuteNextTask()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
private let scale = UIScreen.main.scale
|
||||
private let scale = UIScreen.mainScreen().scale
|
||||
|
||||
infix operator >=~
|
||||
infix operator >=~ { }
|
||||
func >=~ (lhs: CGFloat, rhs: CGFloat) -> Bool {
|
||||
return round(lhs * scale) >= round(rhs * scale)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ class BaseChatItemPresenterTests: XCTestCase {
|
|||
var presenter: BaseChatItemPresenter<UICollectionViewCell>!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
self.presenter = BaseChatItemPresenter()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ THE SOFTWARE.
|
|||
|
||||
@testable import Chatto
|
||||
|
||||
func createFakeChatItems(count: Int) -> [ChatItemProtocol] {
|
||||
func createFakeChatItems(count count: Int) -> [ChatItemProtocol] {
|
||||
var items = [ChatItemProtocol]()
|
||||
for i in 0..<count {
|
||||
items.append(FakeChatItem(uid: "\(i)", type: "fake-type"))
|
||||
|
|
@ -66,29 +66,29 @@ class FakeDataSource: ChatDataSourceProtocol {
|
|||
if let chatItemsForLoadNext = self.chatItemsForLoadNext {
|
||||
self.chatItems = chatItemsForLoadNext
|
||||
}
|
||||
self.delegate?.chatDataSourceDidUpdate(self, updateType: .pagination)
|
||||
self.delegate?.chatDataSourceDidUpdate(self, updateType: .Pagination)
|
||||
}
|
||||
|
||||
func loadPrevious() {
|
||||
self.wasRequestedForPrevious = true
|
||||
self.delegate?.chatDataSourceDidUpdate(self, updateType: .pagination)
|
||||
self.delegate?.chatDataSourceDidUpdate(self, updateType: .Pagination)
|
||||
}
|
||||
|
||||
func adjustNumberOfMessages(preferredMaxCount: Int?, focusPosition: Double, completion:((didAdjust: Bool)) -> Void) {
|
||||
func adjustNumberOfMessages(preferredMaxCount preferredMaxCount: Int?, focusPosition: Double, completion:(didAdjust: Bool) -> Void) {
|
||||
self.wasRequestedForMessageCountContention = true
|
||||
completion((didAdjust: false))
|
||||
completion(didAdjust: false)
|
||||
}
|
||||
}
|
||||
|
||||
class FakeCell: UICollectionViewCell {}
|
||||
class FakeCell: UICollectionViewCell { }
|
||||
|
||||
class FakePresenterBuilder: ChatItemPresenterBuilderProtocol {
|
||||
var presentersCreatedCount: Int = 0
|
||||
func canHandleChatItem(_ chatItem: ChatItemProtocol) -> Bool {
|
||||
func canHandleChatItem(chatItem: ChatItemProtocol) -> Bool {
|
||||
return chatItem.type == "fake-type"
|
||||
}
|
||||
|
||||
func createPresenterWithChatItem(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol {
|
||||
func createPresenterWithChatItem(chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol {
|
||||
self.presentersCreatedCount += 1
|
||||
return FakePresenter()
|
||||
}
|
||||
|
|
@ -99,21 +99,21 @@ class FakePresenterBuilder: ChatItemPresenterBuilderProtocol {
|
|||
}
|
||||
|
||||
class FakePresenter: BaseChatItemPresenter<FakeCell> {
|
||||
override class func registerCells(_ collectionView: UICollectionView) {
|
||||
collectionView.register(FakeCell.self, forCellWithReuseIdentifier: "fake-cell")
|
||||
override class func registerCells(collectionView: UICollectionView) {
|
||||
collectionView.registerClass(FakeCell.self, forCellWithReuseIdentifier: "fake-cell")
|
||||
}
|
||||
|
||||
override func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat {
|
||||
return 10
|
||||
}
|
||||
|
||||
override func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell {
|
||||
return collectionView.dequeueReusableCell(withReuseIdentifier: "fake-cell", for: indexPath as IndexPath)
|
||||
override func dequeueCell(collectionView collectionView: UICollectionView, indexPath: NSIndexPath) -> UICollectionViewCell {
|
||||
return collectionView.dequeueReusableCellWithReuseIdentifier("fake-cell", forIndexPath: indexPath)
|
||||
}
|
||||
|
||||
override func configureCell(_ cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) {
|
||||
override func configureCell(cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) {
|
||||
let fakeCell = cell as! FakeCell
|
||||
fakeCell.backgroundColor = UIColor.red
|
||||
fakeCell.backgroundColor = UIColor.redColor()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -127,14 +127,13 @@ class FakeChatItem: ChatItemProtocol {
|
|||
}
|
||||
|
||||
final class SerialTaskQueueTestHelper: SerialTaskQueueProtocol {
|
||||
|
||||
var onAllTasksFinished: (() -> Void)?
|
||||
|
||||
var isBusy = false
|
||||
var isStopped = true
|
||||
var tasksQueue = [TaskClosure]()
|
||||
|
||||
func addTask(_ task: @escaping TaskClosure) {
|
||||
func addTask(task: TaskClosure) {
|
||||
self.tasksQueue.append(task)
|
||||
self.maybeExecuteNextTask()
|
||||
}
|
||||
|
|
@ -161,7 +160,7 @@ final class SerialTaskQueueTestHelper: SerialTaskQueueProtocol {
|
|||
if !self.isEmpty {
|
||||
let firstTask = self.tasksQueue.removeFirst()
|
||||
self.isBusy = true
|
||||
firstTask({ [weak self] () -> Void in
|
||||
firstTask(completion: { [weak self] () -> Void in
|
||||
self?.isBusy = false
|
||||
self?.maybeExecuteNextTask()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class ChatViewControllerTests: XCTestCase {
|
|||
}
|
||||
|
||||
func testThat_WhenDataSourceChanges_ThenCollectionViewUpdatesAsynchronously() {
|
||||
let asyncExpectation = expectation(description: "update")
|
||||
let asyncExpectation = expectationWithDescription("update")
|
||||
let presenterBuilder = FakePresenterBuilder()
|
||||
let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]])
|
||||
let fakeDataSource = FakeDataSource()
|
||||
|
|
@ -74,7 +74,7 @@ class ChatViewControllerTests: XCTestCase {
|
|||
asyncExpectation.fulfill()
|
||||
completion()
|
||||
}
|
||||
self.waitForExpectations(timeout: 1) { (error) -> Void in
|
||||
self.waitForExpectationsWithTimeout(1) { (error) -> Void in
|
||||
XCTAssertEqual(3, controller.collectionView(controller.collectionView, numberOfItemsInSection: 0))
|
||||
}
|
||||
}
|
||||
|
|
@ -91,7 +91,7 @@ class ChatViewControllerTests: XCTestCase {
|
|||
}
|
||||
|
||||
func testThat_GivenManyItems_WhenScrollToTop_ThenLoadsPreviousPage() {
|
||||
let asyncExpectation = expectation(description: "update")
|
||||
let asyncExpectation = expectationWithDescription("update")
|
||||
let presenterBuilder = FakePresenterBuilder()
|
||||
let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]])
|
||||
let fakeDataSource = FakeDataSource()
|
||||
|
|
@ -105,13 +105,13 @@ class ChatViewControllerTests: XCTestCase {
|
|||
completion()
|
||||
asyncExpectation.fulfill()
|
||||
}
|
||||
self.waitForExpectations(timeout: 1) { (error) -> Void in
|
||||
self.waitForExpectationsWithTimeout(1) { (error) -> Void in
|
||||
XCTAssertTrue(fakeDataSource.wasRequestedForPrevious)
|
||||
}
|
||||
}
|
||||
|
||||
func testThat_WhenLoadsNextPage_ThenPreservesScrollPosition() {
|
||||
let asyncExpectation = expectation(description: "update")
|
||||
let asyncExpectation = expectationWithDescription("update")
|
||||
let presenterBuilder = FakePresenterBuilder()
|
||||
let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]])
|
||||
let fakeDataSource = FakeDataSource()
|
||||
|
|
@ -129,14 +129,14 @@ class ChatViewControllerTests: XCTestCase {
|
|||
completion()
|
||||
}
|
||||
|
||||
self.waitForExpectations(timeout: 1) { (error) -> Void in
|
||||
self.waitForExpectationsWithTimeout(1) { (error) -> Void in
|
||||
XCTAssertEqual(3000, controller.collectionView(controller.collectionView, numberOfItemsInSection: 0))
|
||||
XCTAssertEqual(contentOffset, controller.collectionView.contentOffset)
|
||||
}
|
||||
}
|
||||
|
||||
func testThat_WhenManyMessagesAreLoaded_ThenRequestForMessageCountContention() {
|
||||
let asyncExpectation = expectation(description: "update")
|
||||
let asyncExpectation = expectationWithDescription("update")
|
||||
let updateQueue = SerialTaskQueueTestHelper()
|
||||
let presenterBuilder = FakePresenterBuilder()
|
||||
let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]])
|
||||
|
|
@ -148,13 +148,13 @@ class ChatViewControllerTests: XCTestCase {
|
|||
updateQueue.onAllTasksFinished = {
|
||||
asyncExpectation.fulfill()
|
||||
}
|
||||
self.waitForExpectations(timeout: 1) { (error) -> Void in
|
||||
self.waitForExpectationsWithTimeout(1) { (error) -> Void in
|
||||
XCTAssertTrue(fakeDataSource.wasRequestedForMessageCountContention)
|
||||
}
|
||||
}
|
||||
|
||||
func testThat_WhenUpdatesFinish_ControllerIsNotRetained() {
|
||||
let asyncExpectation = expectation(description: "update")
|
||||
let asyncExpectation = expectationWithDescription("update")
|
||||
let updateQueue = SerialTaskQueueTestHelper()
|
||||
var controller: TesteableChatViewController! = TesteableChatViewController(presenterBuilders: ["fake-type": [FakePresenterBuilder()]])
|
||||
weak var weakController = controller
|
||||
|
|
@ -171,7 +171,7 @@ class ChatViewControllerTests: XCTestCase {
|
|||
updateQueue.onAllTasksFinished = {
|
||||
asyncExpectation.fulfill()
|
||||
}
|
||||
self.waitForExpectations(timeout: 1) { (error) -> Void in
|
||||
self.waitForExpectationsWithTimeout(1) { (error) -> Void in
|
||||
controller = nil
|
||||
XCTAssertNil(weakController)
|
||||
}
|
||||
|
|
@ -193,28 +193,28 @@ class ChatViewControllerTests: XCTestCase {
|
|||
|
||||
func testThat_LayoutAdaptsWhenKeyboardIsShown() {
|
||||
let controller = TesteableChatViewController()
|
||||
let notificationCenter = NotificationCenter()
|
||||
let notificationCenter = NSNotificationCenter()
|
||||
controller.notificationCenter = notificationCenter
|
||||
let fakeDataSource = FakeDataSource()
|
||||
fakeDataSource.chatItems = createFakeChatItems(count: 2)
|
||||
controller.chatDataSource = fakeDataSource
|
||||
self.fakeDidAppearAndLayout(controller: controller)
|
||||
notificationCenter.post(name: NSNotification.Name.UIKeyboardWillShow, object: self, userInfo: [UIKeyboardFrameEndUserInfoKey: NSValue(cgRect: CGRect(x: 0, y: 400, width: 400, height: 500))])
|
||||
XCTAssertEqual(400, controller.view.convert(controller.chatInputView.bounds, from: controller.chatInputView).maxY)
|
||||
notificationCenter.postNotificationName(UIKeyboardWillShowNotification, object: self, userInfo: [UIKeyboardFrameEndUserInfoKey: NSValue(CGRect: CGRect(x: 0, y: 400, width: 400, height: 500))])
|
||||
XCTAssertEqual(400, controller.view.convertRect(controller.chatInputView.bounds, fromView: controller.chatInputView).maxY)
|
||||
}
|
||||
|
||||
func testThat_LayoutAdaptsWhenKeyboardIsHidden() {
|
||||
let controller = TesteableChatViewController()
|
||||
let notificationCenter = NotificationCenter()
|
||||
let notificationCenter = NSNotificationCenter()
|
||||
controller.notificationCenter = notificationCenter
|
||||
let fakeDataSource = FakeDataSource()
|
||||
fakeDataSource.chatItems = createFakeChatItems(count: 2)
|
||||
controller.chatDataSource = fakeDataSource
|
||||
self.fakeDidAppearAndLayout(controller: controller)
|
||||
notificationCenter.post(name: NSNotification.Name.UIKeyboardWillShow, object: self, userInfo: [UIKeyboardFrameEndUserInfoKey: NSValue(cgRect: CGRect(x: 0, y: 400, width: 400, height: 500))])
|
||||
notificationCenter.post(name: NSNotification.Name.UIKeyboardDidShow, object: self, userInfo: [UIKeyboardFrameEndUserInfoKey: NSValue(cgRect: CGRect(x: 0, y: 400, width: 400, height: 500))])
|
||||
notificationCenter.post(name: NSNotification.Name.UIKeyboardWillHide, object: self, userInfo: [UIKeyboardFrameEndUserInfoKey: NSValue(cgRect: CGRect(x: 0, y: 400, width: 400, height: 500))])
|
||||
XCTAssertEqual(900, controller.view.convert(controller.chatInputView.bounds, from: controller.chatInputView).maxY)
|
||||
notificationCenter.postNotificationName(UIKeyboardWillShowNotification, object: self, userInfo: [UIKeyboardFrameEndUserInfoKey: NSValue(CGRect: CGRect(x: 0, y: 400, width: 400, height: 500))])
|
||||
notificationCenter.postNotificationName(UIKeyboardDidShowNotification, object: self, userInfo: [UIKeyboardFrameEndUserInfoKey: NSValue(CGRect: CGRect(x: 0, y: 400, width: 400, height: 500))])
|
||||
notificationCenter.postNotificationName(UIKeyboardWillHideNotification, object: self, userInfo: [UIKeyboardFrameEndUserInfoKey: NSValue(CGRect: CGRect(x: 0, y: 400, width: 400, height: 500))])
|
||||
XCTAssertEqual(900, controller.view.convertRect(controller.chatInputView.bounds, fromView: controller.chatInputView).maxY)
|
||||
}
|
||||
|
||||
func testThat_GivenCoalescingIsEnabled_WhenMultipleUpdatesAreRequested_ThenUpdatesAreCoalesced() {
|
||||
|
|
@ -225,7 +225,7 @@ class ChatViewControllerTests: XCTestCase {
|
|||
let updateQueue = SerialTaskQueueTestHelper()
|
||||
controller.updateQueue = updateQueue
|
||||
|
||||
controller.setChatDataSource(fakeDataSource, triggeringUpdateType: .none)
|
||||
controller.setChatDataSource(fakeDataSource, triggeringUpdateType: .None)
|
||||
controller.chatDataSourceDidUpdate(fakeDataSource) // running
|
||||
controller.chatDataSourceDidUpdate(fakeDataSource) // discarded
|
||||
controller.chatDataSourceDidUpdate(fakeDataSource) // discarded
|
||||
|
|
@ -242,7 +242,7 @@ class ChatViewControllerTests: XCTestCase {
|
|||
controller.updateQueue = updateQueue
|
||||
|
||||
updateQueue.start()
|
||||
controller.setChatDataSource(fakeDataSource, triggeringUpdateType: .none)
|
||||
controller.setChatDataSource(fakeDataSource, triggeringUpdateType: .None)
|
||||
controller.chatDataSourceDidUpdate(fakeDataSource) // running
|
||||
controller.chatDataSourceDidUpdate(fakeDataSource) // queued
|
||||
controller.chatDataSourceDidUpdate(fakeDataSource) // queued
|
||||
|
|
@ -253,7 +253,7 @@ class ChatViewControllerTests: XCTestCase {
|
|||
|
||||
// MARK: helpers
|
||||
|
||||
private func fakeDidAppearAndLayout(controller: TesteableChatViewController) {
|
||||
private func fakeDidAppearAndLayout(controller controller: TesteableChatViewController) {
|
||||
controller.view.frame = CGRect(x: 0, y: 0, width: 400, height: 900)
|
||||
controller.viewWillAppear(true)
|
||||
controller.viewDidAppear(true)
|
||||
|
|
|
|||
|
|
@ -33,8 +33,7 @@ class ChatCollectionViewLayoutModelTests: XCTestCase {
|
|||
XCTAssertEqual(width, layoutModel.calculatedForWidth)
|
||||
XCTAssertEqual(CGSize(width: 320, height: 0), layoutModel.contentSize)
|
||||
XCTAssertEqual([], layoutModel.layoutAttributes)
|
||||
XCTAssertEqual(1, layoutModel.layoutAttributesBySectionAndItem.count)
|
||||
XCTAssertEqual([], layoutModel.layoutAttributesBySectionAndItem.first!)
|
||||
XCTAssertEqual([[]], layoutModel.layoutAttributesBySectionAndItem)
|
||||
}
|
||||
|
||||
func testThatLayoutIsCorrectlyCreated() {
|
||||
|
|
@ -50,14 +49,14 @@ class ChatCollectionViewLayoutModelTests: XCTestCase {
|
|||
XCTAssertEqual(width, layoutModel.calculatedForWidth)
|
||||
XCTAssertEqual(CGSize(width: 320, height: 28), layoutModel.contentSize)
|
||||
XCTAssertEqual(expectedLayoutAttributes, layoutModel.layoutAttributes)
|
||||
XCTAssertEqual(1, layoutModel.layoutAttributesBySectionAndItem.count)
|
||||
XCTAssertEqual(expectedLayoutAttributes, layoutModel.layoutAttributesBySectionAndItem.first!)
|
||||
XCTAssertEqual([expectedLayoutAttributes], layoutModel.layoutAttributesBySectionAndItem)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func Atttributes(item: Int, frame: CGRect) -> UICollectionViewLayoutAttributes {
|
||||
let indexPath = IndexPath(item: item, section: 0)
|
||||
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
|
||||
private func Atttributes(item item: Int, frame: CGRect) -> UICollectionViewLayoutAttributes {
|
||||
let indexPath = NSIndexPath(forItem: item, inSection: 0)
|
||||
let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
|
||||
attributes.frame = frame
|
||||
return attributes
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ class CollectionChangesTests: XCTestCase {
|
|||
|
||||
func testThatDoesNotGenerateChangesForEqualCollections() {
|
||||
let changes = generateChanges(
|
||||
oldCollection: [Item(uid: "a"), Item(uid: "b")],
|
||||
newCollection: [Item(uid: "a"), Item(uid: "b")]
|
||||
oldCollection: [Item("a"), Item("b")],
|
||||
newCollection: [Item("a"), Item("b")]
|
||||
)
|
||||
XCTAssertEqual(changes.insertedIndexPaths, [])
|
||||
XCTAssertEqual(changes.deletedIndexPaths, [])
|
||||
|
|
@ -47,27 +47,27 @@ class CollectionChangesTests: XCTestCase {
|
|||
func testThatGeneratesInsertions() {
|
||||
let changes = generateChanges(
|
||||
oldCollection: [],
|
||||
newCollection: [Item(uid: "a"), Item(uid: "b")]
|
||||
newCollection: [Item("a"), Item("b")]
|
||||
)
|
||||
XCTAssertEqual(changes.deletedIndexPaths, [])
|
||||
XCTAssertEqual(changes.movedIndexPaths, [])
|
||||
XCTAssertEqual(Set(changes.insertedIndexPaths), Set([IndexPath(item: 0, section: 0), IndexPath(item: 1, section: 0)]))
|
||||
XCTAssertEqual(Set(changes.insertedIndexPaths), Set([NSIndexPath(forItem: 0, inSection: 0), NSIndexPath(forItem: 1, inSection: 0)]))
|
||||
}
|
||||
|
||||
func testThatGeneratesDeletions() {
|
||||
let changes = generateChanges(
|
||||
oldCollection: [Item(uid: "a"), Item(uid: "b")],
|
||||
oldCollection: [Item("a"), Item("b")],
|
||||
newCollection: []
|
||||
)
|
||||
XCTAssertEqual(changes.deletedIndexPaths, Set([IndexPath(item: 0, section: 0), IndexPath(item: 1, section: 0)]))
|
||||
XCTAssertEqual(changes.deletedIndexPaths, Set([NSIndexPath(forItem: 0, inSection: 0), NSIndexPath(forItem: 1, inSection: 0)]))
|
||||
XCTAssertEqual(changes.movedIndexPaths.count, 0)
|
||||
XCTAssertEqual(changes.insertedIndexPaths.count, 0)
|
||||
}
|
||||
|
||||
func testThatGeneratesMovements() {
|
||||
let changes = generateChanges(
|
||||
oldCollection: [Item(uid: "a"), Item(uid: "b"), Item(uid: "c")],
|
||||
newCollection: [Item(uid: "a"), Item(uid: "c"), Item(uid: "b")]
|
||||
oldCollection: [Item("a"), Item("b"), Item("c")],
|
||||
newCollection: [Item("a"), Item("c"), Item("b")]
|
||||
)
|
||||
XCTAssertEqual(changes.deletedIndexPaths, [])
|
||||
XCTAssertEqual(Set(changes.movedIndexPaths), Set([Move(1, to: 2), Move(2, to:1)]))
|
||||
|
|
@ -76,22 +76,22 @@ class CollectionChangesTests: XCTestCase {
|
|||
|
||||
func testThatGeneratesInsertionsDeletionsAndMovements() {
|
||||
let changes = generateChanges(
|
||||
oldCollection: [Item(uid: "a"), Item(uid: "b"), Item(uid: "c")],
|
||||
newCollection: [Item(uid: "d"), Item(uid: "c"), Item(uid: "a")]
|
||||
oldCollection: [Item("a"), Item("b"), Item("c")],
|
||||
newCollection: [Item("d"), Item("c"), Item("a")]
|
||||
)
|
||||
XCTAssertEqual(changes.deletedIndexPaths, [IndexPath(item: 1, section: 0)])
|
||||
XCTAssertEqual(changes.insertedIndexPaths, [IndexPath(item: 0, section: 0)])
|
||||
XCTAssertEqual(changes.deletedIndexPaths, [NSIndexPath(forItem: 1, inSection: 0)])
|
||||
XCTAssertEqual(changes.insertedIndexPaths, [NSIndexPath(forItem: 0, inSection: 0)])
|
||||
XCTAssertEqual(Set(changes.movedIndexPaths), [Move(0, to: 2), Move(2, to: 1)])
|
||||
}
|
||||
|
||||
func testThatAppliesChangesToCollection() {
|
||||
// (0, 1, 2, 3, 4) -> (2, 3, new, 4)
|
||||
|
||||
let indexPath0 = IndexPath(item: 0, section: 0)
|
||||
let indexPath1 = IndexPath(item: 1, section: 0)
|
||||
let indexPath2 = IndexPath(item: 2, section: 0)
|
||||
let indexPath3 = IndexPath(item: 3, section: 0)
|
||||
let indexPath4 = IndexPath(item: 4, section: 0)
|
||||
let indexPath0 = NSIndexPath(forItem: 0, inSection: 0)
|
||||
let indexPath1 = NSIndexPath(forItem: 1, inSection: 0)
|
||||
let indexPath2 = NSIndexPath(forItem: 2, inSection: 0)
|
||||
let indexPath3 = NSIndexPath(forItem: 3, inSection: 0)
|
||||
let indexPath4 = NSIndexPath(forItem: 4, inSection: 0)
|
||||
|
||||
let collection = [
|
||||
indexPath0: 0,
|
||||
|
|
@ -127,8 +127,8 @@ func Item(uid: String) -> UniqueIdentificable {
|
|||
return UniqueIdentificableItem(uid: uid)
|
||||
}
|
||||
|
||||
func Move(_ from: Int, to: Int) -> CollectionChangeMove {
|
||||
return CollectionChangeMove(indexPathOld: IndexPath(item: from, section: 0), indexPathNew: IndexPath(item: to, section: 0))
|
||||
func Move(from: Int, to: Int) -> CollectionChangeMove {
|
||||
return CollectionChangeMove(indexPathOld: NSIndexPath(forItem: from, inSection: 0), indexPathNew: NSIndexPath(forItem: to, inSection: 0))
|
||||
}
|
||||
|
||||
struct UniqueIdentificableItem: UniqueIdentificable {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>3.0.1</string>
|
||||
<string>2.1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ class ReadOnlyOrderedDictionaryTests: XCTestCase {
|
|||
|
||||
var orderedDictionary: ReadOnlyOrderedDictionary<FakeChatItem>!
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
let items = [
|
||||
FakeChatItem(uid: "3", type: "type3"),
|
||||
FakeChatItem(uid: "1", type: "type1"),
|
||||
|
|
@ -39,7 +38,7 @@ class ReadOnlyOrderedDictionaryTests: XCTestCase {
|
|||
}
|
||||
|
||||
func testThat_MapsCorrectly() {
|
||||
XCTAssertEqual(self.orderedDictionary.map { $0.uid }, ["3", "1", "2"])
|
||||
XCTAssertEqual(self.orderedDictionary.map { $0.uid}, ["3", "1", "2"])
|
||||
}
|
||||
|
||||
func testThat_NumberOfItemsIsCorrect() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = "ChattoAdditions"
|
||||
s.version = "3.0.1"
|
||||
s.version = "2.1.0"
|
||||
s.summary = "UI componentes for Chatto"
|
||||
s.description = <<-DESC
|
||||
Text and photo bubbles
|
||||
|
|
|
|||
|
|
@ -701,7 +701,7 @@
|
|||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 3.0;
|
||||
SWIFT_VERSION = 2.3;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
|
|
@ -745,7 +745,7 @@
|
|||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 3.0;
|
||||
SWIFT_VERSION = 2.3;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
|
|
|
|||
|
|
@ -23,13 +23,13 @@
|
|||
*/
|
||||
|
||||
public extension CABasicAnimation {
|
||||
class func bma_fadeInAnimationWithDuration(_ duration: CFTimeInterval) -> CABasicAnimation {
|
||||
let animation = CABasicAnimation(keyPath: "opacity")
|
||||
class func bma_fadeInAnimationWithDuration(duration: CFTimeInterval) -> CABasicAnimation {
|
||||
let animation = CABasicAnimation.init(keyPath: "opacity")
|
||||
animation.duration = duration
|
||||
animation.fromValue = 0
|
||||
animation.toValue = 1
|
||||
animation.fillMode = kCAFillModeForwards
|
||||
animation.isAdditive = false
|
||||
animation.additive = false
|
||||
return animation
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,15 +26,15 @@ import Foundation
|
|||
import Chatto
|
||||
|
||||
public enum MessageStatus {
|
||||
case failed
|
||||
case sending
|
||||
case success
|
||||
case Failed
|
||||
case Sending
|
||||
case Success
|
||||
}
|
||||
|
||||
public protocol MessageModelProtocol: ChatItemProtocol {
|
||||
var senderId: String { get }
|
||||
var isIncoming: Bool { get }
|
||||
var date: Date { get }
|
||||
var date: NSDate { get }
|
||||
var status: MessageStatus { get }
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ public extension DecoratedMessageModelProtocol {
|
|||
return self.messageModel.isIncoming
|
||||
}
|
||||
|
||||
var date: Date {
|
||||
var date: NSDate {
|
||||
return self.messageModel.date
|
||||
}
|
||||
|
||||
|
|
@ -68,15 +68,15 @@ public extension DecoratedMessageModelProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
open class MessageModel: MessageModelProtocol {
|
||||
open var uid: String
|
||||
open var senderId: String
|
||||
open var type: String
|
||||
open var isIncoming: Bool
|
||||
open var date: Date
|
||||
open var status: MessageStatus
|
||||
public class MessageModel: MessageModelProtocol {
|
||||
public var uid: String
|
||||
public var senderId: String
|
||||
public var type: String
|
||||
public var isIncoming: Bool
|
||||
public var date: NSDate
|
||||
public var status: MessageStatus
|
||||
|
||||
public init(uid: String, senderId: String, type: String, isIncoming: Bool, date: Date, status: MessageStatus) {
|
||||
public init(uid: String, senderId: String, type: String, isIncoming: Bool, date: NSDate, status: MessageStatus) {
|
||||
self.uid = uid
|
||||
self.senderId = senderId
|
||||
self.type = type
|
||||
|
|
|
|||
|
|
@ -29,24 +29,24 @@ public protocol ViewModelBuilderProtocol {
|
|||
associatedtype ModelT: MessageModelProtocol
|
||||
associatedtype ViewModelT: MessageViewModelProtocol
|
||||
func canCreateViewModel(fromModel model: Any) -> Bool
|
||||
func createViewModel(_ model: ModelT) -> ViewModelT
|
||||
func createViewModel(model: ModelT) -> ViewModelT
|
||||
}
|
||||
|
||||
public protocol BaseMessageInteractionHandlerProtocol {
|
||||
associatedtype ViewModelT
|
||||
func userDidTapOnFailIcon(viewModel: ViewModelT, failIconView: UIView)
|
||||
func userDidTapOnAvatar(viewModel: ViewModelT)
|
||||
func userDidTapOnBubble(viewModel: ViewModelT)
|
||||
func userDidBeginLongPressOnBubble(viewModel: ViewModelT)
|
||||
func userDidEndLongPressOnBubble(viewModel: ViewModelT)
|
||||
func userDidTapOnFailIcon(viewModel viewModel: ViewModelT, failIconView: UIView)
|
||||
func userDidTapOnAvatar(viewModel viewModel: ViewModelT)
|
||||
func userDidTapOnBubble(viewModel viewModel: ViewModelT)
|
||||
func userDidBeginLongPressOnBubble(viewModel viewModel: ViewModelT)
|
||||
func userDidEndLongPressOnBubble(viewModel viewModel: ViewModelT)
|
||||
}
|
||||
|
||||
open class BaseMessagePresenter<BubbleViewT, ViewModelBuilderT, InteractionHandlerT>: BaseChatItemPresenter<BaseMessageCollectionViewCell<BubbleViewT>> where
|
||||
public class BaseMessagePresenter<BubbleViewT, ViewModelBuilderT, InteractionHandlerT where
|
||||
ViewModelBuilderT: ViewModelBuilderProtocol,
|
||||
ViewModelBuilderT.ViewModelT: MessageViewModelProtocol,
|
||||
InteractionHandlerT: BaseMessageInteractionHandlerProtocol,
|
||||
InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT,
|
||||
BubbleViewT: UIView, BubbleViewT:MaximumLayoutWidthSpecificable, BubbleViewT: BackgroundSizingQueryable {
|
||||
BubbleViewT: UIView, BubbleViewT:MaximumLayoutWidthSpecificable, BubbleViewT: BackgroundSizingQueryable>: BaseChatItemPresenter<BaseMessageCollectionViewCell<BubbleViewT>> {
|
||||
public typealias CellT = BaseMessageCollectionViewCell<BubbleViewT>
|
||||
public typealias ModelT = ViewModelBuilderT.ModelT
|
||||
public typealias ViewModelT = ViewModelBuilderT.ViewModelT
|
||||
|
|
@ -74,12 +74,12 @@ open class BaseMessagePresenter<BubbleViewT, ViewModelBuilderT, InteractionHandl
|
|||
return self.createViewModel()
|
||||
}()
|
||||
|
||||
open func createViewModel() -> ViewModelT {
|
||||
public func createViewModel() -> ViewModelT {
|
||||
let viewModel = self.viewModelBuilder.createViewModel(self.messageModel)
|
||||
return viewModel
|
||||
}
|
||||
|
||||
public final override func configureCell(_ cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) {
|
||||
public final override func configureCell(cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) {
|
||||
guard let cell = cell as? CellT else {
|
||||
assert(false, "Invalid cell given to presenter")
|
||||
return
|
||||
|
|
@ -94,11 +94,11 @@ open class BaseMessagePresenter<BubbleViewT, ViewModelBuilderT, InteractionHandl
|
|||
}
|
||||
|
||||
public var decorationAttributes: ChatItemDecorationAttributes!
|
||||
open func configureCell(_ cell: CellT, decorationAttributes: ChatItemDecorationAttributes, animated: Bool, additionalConfiguration: (() -> Void)?) {
|
||||
public func configureCell(cell: CellT, decorationAttributes: ChatItemDecorationAttributes, animated: Bool, additionalConfiguration: (() -> Void)?) {
|
||||
cell.performBatchUpdates({ () -> Void in
|
||||
self.messageViewModel.showsTail = decorationAttributes.showsTail
|
||||
cell.avatarView.isHidden = !decorationAttributes.canShowAvatar
|
||||
cell.bubbleView.isUserInteractionEnabled = true // just in case something went wrong while showing UIMenuController
|
||||
cell.avatarView.hidden = !decorationAttributes.canShowAvatar
|
||||
cell.bubbleView.userInteractionEnabled = true // just in case something went wrong while showing UIMenuController
|
||||
cell.baseStyle = self.cellStyle
|
||||
cell.messageViewModel = self.messageViewModel
|
||||
cell.onBubbleTapped = { [weak self] (cell) in
|
||||
|
|
@ -125,73 +125,74 @@ open class BaseMessagePresenter<BubbleViewT, ViewModelBuilderT, InteractionHandl
|
|||
}, animated: animated, completion: nil)
|
||||
}
|
||||
|
||||
open override func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat {
|
||||
public override func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat {
|
||||
guard let decorationAttributes = decorationAttributes as? ChatItemDecorationAttributes else {
|
||||
assert(false, "Expecting decoration attributes")
|
||||
return 0
|
||||
}
|
||||
self.configureCell(self.sizingCell, decorationAttributes: decorationAttributes, animated: false, additionalConfiguration: nil)
|
||||
return self.sizingCell.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude)).height
|
||||
return self.sizingCell.sizeThatFits(CGSize(width: width, height: CGFloat.max)).height
|
||||
}
|
||||
|
||||
open override var canCalculateHeightInBackground: Bool {
|
||||
public override var canCalculateHeightInBackground: Bool {
|
||||
return self.sizingCell.canCalculateSizeInBackground
|
||||
}
|
||||
|
||||
open override func cellWillBeShown() {
|
||||
|
||||
public override func cellWillBeShown() {
|
||||
self.messageViewModel.willBeShown()
|
||||
}
|
||||
|
||||
open override func cellWasHidden() {
|
||||
public override func cellWasHidden() {
|
||||
self.messageViewModel.wasHidden()
|
||||
}
|
||||
|
||||
open override func shouldShowMenu() -> Bool {
|
||||
public override func shouldShowMenu() -> Bool {
|
||||
guard self.canShowMenu() else { return false }
|
||||
guard let cell = self.cell else {
|
||||
assert(false, "Investigate -> Fix or remove assert")
|
||||
return false
|
||||
}
|
||||
cell.bubbleView.isUserInteractionEnabled = false // This is a hack for UITextView, shouldn't harm to all bubbles
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(BaseMessagePresenter.willShowMenu(_:)), name: NSNotification.Name.UIMenuControllerWillShowMenu, object: nil)
|
||||
cell.bubbleView.userInteractionEnabled = false // This is a hack for UITextView, shouldn't harm to all bubbles
|
||||
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(BaseMessagePresenter.willShowMenu(_:)), name: UIMenuControllerWillShowMenuNotification, object: nil)
|
||||
return true
|
||||
}
|
||||
|
||||
@objc
|
||||
func willShowMenu(_ notification: Notification) {
|
||||
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIMenuControllerWillShowMenu, object: nil)
|
||||
guard let cell = self.cell, let menuController = notification.object as? UIMenuController else {
|
||||
func willShowMenu(notification: NSNotification) {
|
||||
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIMenuControllerWillShowMenuNotification, object: nil)
|
||||
guard let cell = self.cell, menuController = notification.object as? UIMenuController else {
|
||||
assert(false, "Investigate -> Fix or remove assert")
|
||||
return
|
||||
}
|
||||
cell.bubbleView.isUserInteractionEnabled = true
|
||||
cell.bubbleView.userInteractionEnabled = true
|
||||
menuController.setMenuVisible(false, animated: false)
|
||||
menuController.setTargetRect(cell.bubbleView.bounds, in: cell.bubbleView)
|
||||
menuController.setTargetRect(cell.bubbleView.bounds, inView: cell.bubbleView)
|
||||
menuController.setMenuVisible(true, animated: true)
|
||||
}
|
||||
|
||||
open func canShowMenu() -> Bool {
|
||||
public func canShowMenu() -> Bool {
|
||||
// Override in subclass
|
||||
return false
|
||||
}
|
||||
|
||||
open func onCellBubbleTapped() {
|
||||
public func onCellBubbleTapped() {
|
||||
self.interactionHandler?.userDidTapOnBubble(viewModel: self.messageViewModel)
|
||||
}
|
||||
|
||||
open func onCellBubbleLongPressBegan() {
|
||||
public func onCellBubbleLongPressBegan() {
|
||||
self.interactionHandler?.userDidBeginLongPressOnBubble(viewModel: self.messageViewModel)
|
||||
}
|
||||
|
||||
open func onCellBubbleLongPressEnded() {
|
||||
public func onCellBubbleLongPressEnded() {
|
||||
self.interactionHandler?.userDidEndLongPressOnBubble(viewModel: self.messageViewModel)
|
||||
}
|
||||
|
||||
open func onCellAvatarTapped() {
|
||||
public func onCellAvatarTapped() {
|
||||
self.interactionHandler?.userDidTapOnAvatar(viewModel: self.messageViewModel)
|
||||
}
|
||||
|
||||
open func onCellFailedButtonTapped(_ failedButtonView: UIView) {
|
||||
public func onCellFailedButtonTapped(failedButtonView: UIView) {
|
||||
self.interactionHandler?.userDidTapOnFailIcon(viewModel: self.messageViewModel, failIconView: failedButtonView)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,20 +25,20 @@
|
|||
import Foundation
|
||||
|
||||
public enum MessageViewModelStatus {
|
||||
case success
|
||||
case sending
|
||||
case failed
|
||||
case Success
|
||||
case Sending
|
||||
case Failed
|
||||
}
|
||||
|
||||
public extension MessageStatus {
|
||||
public func viewModelStatus() -> MessageViewModelStatus {
|
||||
switch self {
|
||||
case .success:
|
||||
return MessageViewModelStatus.success
|
||||
case .failed:
|
||||
return MessageViewModelStatus.failed
|
||||
case .sending:
|
||||
return MessageViewModelStatus.sending
|
||||
case .Success:
|
||||
return MessageViewModelStatus.Success
|
||||
case .Failed:
|
||||
return MessageViewModelStatus.Failed
|
||||
case .Sending:
|
||||
return MessageViewModelStatus.Sending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -97,32 +97,32 @@ extension DecoratedMessageViewModelProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
open class MessageViewModel: MessageViewModelProtocol {
|
||||
open var isIncoming: Bool {
|
||||
public class MessageViewModel: MessageViewModelProtocol {
|
||||
public var isIncoming: Bool {
|
||||
return self.messageModel.isIncoming
|
||||
}
|
||||
|
||||
open var status: MessageViewModelStatus {
|
||||
public var status: MessageViewModelStatus {
|
||||
return self.messageModel.status.viewModelStatus()
|
||||
}
|
||||
|
||||
open var showsTail: Bool
|
||||
open lazy var date: String = {
|
||||
return self.dateFormatter.string(from: self.messageModel.date as Date)
|
||||
public var showsTail: Bool
|
||||
public lazy var date: String = {
|
||||
return self.dateFormatter.stringFromDate(self.messageModel.date)
|
||||
}()
|
||||
|
||||
public let dateFormatter: DateFormatter
|
||||
public let dateFormatter: NSDateFormatter
|
||||
public private(set) var messageModel: MessageModelProtocol
|
||||
|
||||
public init(dateFormatter: DateFormatter, showsTail: Bool, messageModel: MessageModelProtocol, avatarImage: UIImage?) {
|
||||
public init(dateFormatter: NSDateFormatter, showsTail: Bool, messageModel: MessageModelProtocol, avatarImage: UIImage?) {
|
||||
self.dateFormatter = dateFormatter
|
||||
self.showsTail = showsTail
|
||||
self.messageModel = messageModel
|
||||
self.avatarImage = Observable<UIImage?>(avatarImage)
|
||||
}
|
||||
|
||||
open var showsFailedIcon: Bool {
|
||||
return self.status == .failed
|
||||
public var showsFailedIcon: Bool {
|
||||
return self.status == .Failed
|
||||
}
|
||||
|
||||
public var avatarImage: Observable<UIImage?>
|
||||
|
|
@ -132,15 +132,15 @@ public class MessageViewModelDefaultBuilder {
|
|||
|
||||
public init() {}
|
||||
|
||||
static let dateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale.current
|
||||
formatter.dateStyle = .none
|
||||
formatter.timeStyle = .short
|
||||
static let dateFormatter: NSDateFormatter = {
|
||||
let formatter = NSDateFormatter()
|
||||
formatter.locale = NSLocale.currentLocale()
|
||||
formatter.dateStyle = .NoStyle
|
||||
formatter.timeStyle = .ShortStyle
|
||||
return formatter
|
||||
}()
|
||||
|
||||
public func createMessageViewModel(_ message: MessageModelProtocol) -> MessageViewModelProtocol {
|
||||
public func createMessageViewModel(message: MessageModelProtocol) -> MessageViewModelProtocol {
|
||||
// Override to use default avatarImage
|
||||
return MessageViewModel(dateFormatter: MessageViewModelDefaultBuilder.dateFormatter, showsTail: false, messageModel: message, avatarImage: nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,12 +26,12 @@ import UIKit
|
|||
import Chatto
|
||||
|
||||
public protocol BaseMessageCollectionViewCellStyleProtocol {
|
||||
func avatarSize(viewModel: MessageViewModelProtocol) -> CGSize // .zero => no avatar
|
||||
func avatarVerticalAlignment(viewModel: MessageViewModelProtocol) -> VerticalAlignment
|
||||
func avatarSize(viewModel viewModel: MessageViewModelProtocol) -> CGSize // .zero => no avatar
|
||||
func avatarVerticalAlignment(viewModel viewModel: MessageViewModelProtocol) -> VerticalAlignment
|
||||
var failedIcon: UIImage { get }
|
||||
var failedIconHighlighted: UIImage { get }
|
||||
func attributedStringForDate(_ date: String) -> NSAttributedString
|
||||
func layoutConstants(viewModel: MessageViewModelProtocol) -> BaseMessageCollectionViewCellLayoutConstants
|
||||
func attributedStringForDate(date: String) -> NSAttributedString
|
||||
func layoutConstants(viewModel viewModel: MessageViewModelProtocol) -> BaseMessageCollectionViewCellLayoutConstants
|
||||
}
|
||||
|
||||
public struct BaseMessageCollectionViewCellLayoutConstants {
|
||||
|
|
@ -65,13 +65,13 @@ public struct BaseMessageCollectionViewCellLayoutConstants {
|
|||
- Have a BubbleViewType that responds properly to sizeThatFits:
|
||||
*/
|
||||
|
||||
open class BaseMessageCollectionViewCell<BubbleViewType>: UICollectionViewCell, BackgroundSizingQueryable, AccessoryViewRevealable, UIGestureRecognizerDelegate where BubbleViewType:UIView, BubbleViewType:MaximumLayoutWidthSpecificable, BubbleViewType: BackgroundSizingQueryable {
|
||||
public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:UIView, BubbleViewType:MaximumLayoutWidthSpecificable, BubbleViewType: BackgroundSizingQueryable>: UICollectionViewCell, BackgroundSizingQueryable, AccessoryViewRevealable, UIGestureRecognizerDelegate {
|
||||
|
||||
public var animationDuration: CFTimeInterval = 0.33
|
||||
open var viewContext: ViewContext = .normal
|
||||
public var viewContext: ViewContext = .Normal
|
||||
|
||||
public private(set) var isUpdating: Bool = false
|
||||
open func performBatchUpdates(_ updateClosure: @escaping () -> Void, animated: Bool, completion: (() ->())?) {
|
||||
public func performBatchUpdates(updateClosure: () -> Void, animated: Bool, completion: (() ->())?) {
|
||||
self.isUpdating = true
|
||||
let updateAndRefreshViews = {
|
||||
updateClosure()
|
||||
|
|
@ -82,7 +82,7 @@ open class BaseMessageCollectionViewCell<BubbleViewType>: UICollectionViewCell,
|
|||
}
|
||||
}
|
||||
if animated {
|
||||
UIView.animate(withDuration: self.animationDuration, animations: updateAndRefreshViews, completion: { (finished) -> Void in
|
||||
UIView.animateWithDuration(self.animationDuration, animations: updateAndRefreshViews, completion: { (finished) -> Void in
|
||||
completion?()
|
||||
})
|
||||
} else {
|
||||
|
|
@ -90,7 +90,7 @@ open class BaseMessageCollectionViewCell<BubbleViewType>: UICollectionViewCell,
|
|||
}
|
||||
}
|
||||
|
||||
open var messageViewModel: MessageViewModelProtocol! {
|
||||
public var messageViewModel: MessageViewModelProtocol! {
|
||||
didSet {
|
||||
updateViews()
|
||||
}
|
||||
|
|
@ -106,20 +106,20 @@ open class BaseMessageCollectionViewCell<BubbleViewType>: UICollectionViewCell,
|
|||
}
|
||||
}
|
||||
|
||||
override open var isSelected: Bool {
|
||||
override public var selected: Bool {
|
||||
didSet {
|
||||
if oldValue != self.isSelected {
|
||||
if oldValue != self.selected {
|
||||
self.updateViews()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open var canCalculateSizeInBackground: Bool {
|
||||
public var canCalculateSizeInBackground: Bool {
|
||||
return self.bubbleView.canCalculateSizeInBackground
|
||||
}
|
||||
|
||||
public private(set) var bubbleView: BubbleViewType!
|
||||
open func createBubbleView() -> BubbleViewType! {
|
||||
public func createBubbleView() -> BubbleViewType! {
|
||||
assert(false, "Override in subclass")
|
||||
return nil
|
||||
}
|
||||
|
|
@ -127,7 +127,7 @@ open class BaseMessageCollectionViewCell<BubbleViewType>: UICollectionViewCell,
|
|||
public private(set) var avatarView: UIImageView!
|
||||
func createAvatarView() -> UIImageView! {
|
||||
let avatarImageView = UIImageView(frame: CGRect.zero)
|
||||
avatarImageView.isUserInteractionEnabled = true
|
||||
avatarImageView.userInteractionEnabled = true
|
||||
return avatarImageView
|
||||
}
|
||||
|
||||
|
|
@ -161,44 +161,44 @@ open class BaseMessageCollectionViewCell<BubbleViewType>: UICollectionViewCell,
|
|||
self.avatarView = self.createAvatarView()
|
||||
self.avatarView.addGestureRecognizer(self.avatarTapGestureRecognizer)
|
||||
self.bubbleView = self.createBubbleView()
|
||||
self.bubbleView.isExclusiveTouch = true
|
||||
self.bubbleView.exclusiveTouch = true
|
||||
self.bubbleView.addGestureRecognizer(self.tapGestureRecognizer)
|
||||
self.bubbleView.addGestureRecognizer(self.longPressGestureRecognizer)
|
||||
self.contentView.addSubview(self.avatarView)
|
||||
self.contentView.addSubview(self.bubbleView)
|
||||
self.contentView.addSubview(self.failedButton)
|
||||
self.contentView.isExclusiveTouch = true
|
||||
self.isExclusiveTouch = true
|
||||
self.contentView.exclusiveTouch = true
|
||||
self.exclusiveTouch = true
|
||||
}
|
||||
|
||||
open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||||
return self.bubbleView.bounds.contains(touch.location(in: self.bubbleView))
|
||||
public func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
|
||||
return self.bubbleView.bounds.contains(touch.locationInView(self.bubbleView))
|
||||
}
|
||||
|
||||
open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
public func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return gestureRecognizer === self.longPressGestureRecognizer
|
||||
}
|
||||
|
||||
open override func prepareForReuse() {
|
||||
public override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
self.removeAccessoryView()
|
||||
}
|
||||
|
||||
public private(set) lazy var failedButton: UIButton = {
|
||||
let button = UIButton(type: .custom)
|
||||
button.addTarget(self, action: #selector(BaseMessageCollectionViewCell.failedButtonTapped), for: .touchUpInside)
|
||||
public lazy var failedButton: UIButton = {
|
||||
let button = UIButton(type: .Custom)
|
||||
button.addTarget(self, action: #selector(BaseMessageCollectionViewCell.failedButtonTapped), forControlEvents: .TouchUpInside)
|
||||
return button
|
||||
}()
|
||||
|
||||
// MARK: View model binding
|
||||
|
||||
final private func updateViews() {
|
||||
if self.viewContext == .sizing { return }
|
||||
if self.viewContext == .Sizing { return }
|
||||
if self.isUpdating { return }
|
||||
guard let viewModel = self.messageViewModel, let style = self.baseStyle else { return }
|
||||
guard let viewModel = self.messageViewModel, style = self.baseStyle else { return }
|
||||
if viewModel.showsFailedIcon {
|
||||
self.failedButton.setImage(self.failedIcon, for: .normal)
|
||||
self.failedButton.setImage(self.failedIconHighlighted, for: .highlighted)
|
||||
self.failedButton.setImage(self.failedIcon, forState: .Normal)
|
||||
self.failedButton.setImage(self.failedIconHighlighted, forState: .Highlighted)
|
||||
self.failedButton.alpha = 1
|
||||
} else {
|
||||
self.failedButton.alpha = 0
|
||||
|
|
@ -212,7 +212,7 @@ open class BaseMessageCollectionViewCell<BubbleViewType>: UICollectionViewCell,
|
|||
}
|
||||
|
||||
// MARK: layout
|
||||
open override func layoutSubviews() {
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
let layoutModel = self.calculateLayout(availableWidth: self.contentView.bounds.width)
|
||||
|
|
@ -225,7 +225,7 @@ open class BaseMessageCollectionViewCell<BubbleViewType>: UICollectionViewCell,
|
|||
|
||||
if self.accessoryTimestampView.superview != nil {
|
||||
let layoutConstants = baseStyle.layoutConstants(viewModel: messageViewModel)
|
||||
self.accessoryTimestampView.bounds = CGRect(origin: CGPoint.zero, size: self.accessoryTimestampView.intrinsicContentSize)
|
||||
self.accessoryTimestampView.bounds = CGRect(origin: CGPoint.zero, size: self.accessoryTimestampView.intrinsicContentSize())
|
||||
let accessoryViewWidth = self.accessoryTimestampView.bounds.width
|
||||
let leftOffsetForContentView = max(0, offsetToRevealAccessoryView)
|
||||
let leftOffsetForAccessoryView = min(leftOffsetForContentView, accessoryViewWidth + layoutConstants.horizontalTimestampMargin)
|
||||
|
|
@ -240,11 +240,11 @@ open class BaseMessageCollectionViewCell<BubbleViewType>: UICollectionViewCell,
|
|||
}
|
||||
}
|
||||
|
||||
open override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
public override func sizeThatFits(size: CGSize) -> CGSize {
|
||||
return self.calculateLayout(availableWidth: size.width).size
|
||||
}
|
||||
|
||||
private func calculateLayout(availableWidth: CGFloat) -> BaseMessageLayoutModel {
|
||||
private func calculateLayout(availableWidth availableWidth: CGFloat) -> BaseMessageLayoutModel {
|
||||
let layoutConstants = baseStyle.layoutConstants(viewModel: messageViewModel)
|
||||
let parameters = BaseMessageLayoutModelParameters(
|
||||
containerWidth: availableWidth,
|
||||
|
|
@ -263,6 +263,7 @@ open class BaseMessageCollectionViewCell<BubbleViewType>: UICollectionViewCell,
|
|||
return layoutModel
|
||||
}
|
||||
|
||||
|
||||
// MARK: timestamp revealing
|
||||
|
||||
lazy var accessoryTimestampView = UILabel()
|
||||
|
|
@ -275,12 +276,13 @@ open class BaseMessageCollectionViewCell<BubbleViewType>: UICollectionViewCell,
|
|||
|
||||
public var allowAccessoryViewRevealing: Bool = true
|
||||
|
||||
open func preferredOffsetToRevealAccessoryView() -> CGFloat? {
|
||||
public func preferredOffsetToRevealAccessoryView() -> CGFloat? {
|
||||
let layoutConstants = baseStyle.layoutConstants(viewModel: messageViewModel)
|
||||
return self.accessoryTimestampView.intrinsicContentSize.width + layoutConstants.horizontalTimestampMargin
|
||||
return self.accessoryTimestampView.intrinsicContentSize().width + layoutConstants.horizontalTimestampMargin
|
||||
}
|
||||
|
||||
open func revealAccessoryView(withOffset offset: CGFloat, animated: Bool) {
|
||||
|
||||
public func revealAccessoryView(withOffset offset: CGFloat, animated: Bool) {
|
||||
self.offsetToRevealAccessoryView = offset
|
||||
if self.accessoryTimestampView.superview == nil {
|
||||
if offset > 0 {
|
||||
|
|
@ -289,13 +291,13 @@ open class BaseMessageCollectionViewCell<BubbleViewType>: UICollectionViewCell,
|
|||
}
|
||||
|
||||
if animated {
|
||||
UIView.animate(withDuration: self.animationDuration, animations: { () -> Void in
|
||||
UIView.animateWithDuration(self.animationDuration, animations: { () -> Void in
|
||||
self.layoutIfNeeded()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if animated {
|
||||
UIView.animate(withDuration: self.animationDuration, animations: { () -> Void in
|
||||
UIView.animateWithDuration(self.animationDuration, animations: { () -> Void in
|
||||
self.layoutIfNeeded()
|
||||
}, completion: { (finished) -> Void in
|
||||
if offset == 0 {
|
||||
|
|
@ -311,33 +313,33 @@ open class BaseMessageCollectionViewCell<BubbleViewType>: UICollectionViewCell,
|
|||
}
|
||||
|
||||
// MARK: User interaction
|
||||
public var onFailedButtonTapped: ((_ cell: BaseMessageCollectionViewCell) -> Void)?
|
||||
public var onFailedButtonTapped: ((cell: BaseMessageCollectionViewCell) -> Void)?
|
||||
@objc
|
||||
func failedButtonTapped() {
|
||||
self.onFailedButtonTapped?(self)
|
||||
self.onFailedButtonTapped?(cell: self)
|
||||
}
|
||||
|
||||
public var onAvatarTapped: ((_ cell: BaseMessageCollectionViewCell) -> Void)?
|
||||
public var onAvatarTapped: ((cell: BaseMessageCollectionViewCell) -> Void)?
|
||||
@objc
|
||||
func avatarTapped(_ tapGestureRecognizer: UITapGestureRecognizer) {
|
||||
self.onAvatarTapped?(self)
|
||||
func avatarTapped(tapGestureRecognizer: UITapGestureRecognizer) {
|
||||
self.onAvatarTapped?(cell: self)
|
||||
}
|
||||
|
||||
public var onBubbleTapped: ((_ cell: BaseMessageCollectionViewCell) -> Void)?
|
||||
public var onBubbleTapped: ((cell: BaseMessageCollectionViewCell) -> Void)?
|
||||
@objc
|
||||
func bubbleTapped(_ tapGestureRecognizer: UITapGestureRecognizer) {
|
||||
self.onBubbleTapped?(self)
|
||||
func bubbleTapped(tapGestureRecognizer: UITapGestureRecognizer) {
|
||||
self.onBubbleTapped?(cell: self)
|
||||
}
|
||||
|
||||
public var onBubbleLongPressBegan: ((_ cell: BaseMessageCollectionViewCell) -> Void)?
|
||||
public var onBubbleLongPressEnded: ((_ cell: BaseMessageCollectionViewCell) -> Void)?
|
||||
public var onBubbleLongPressBegan: ((cell: BaseMessageCollectionViewCell) -> Void)?
|
||||
public var onBubbleLongPressEnded: ((cell: BaseMessageCollectionViewCell) -> Void)?
|
||||
@objc
|
||||
private func bubbleLongPressed(_ longPressGestureRecognizer: UILongPressGestureRecognizer) {
|
||||
private func bubbleLongPressed(longPressGestureRecognizer: UILongPressGestureRecognizer) {
|
||||
switch longPressGestureRecognizer.state {
|
||||
case .began:
|
||||
self.onBubbleLongPressBegan?(self)
|
||||
case .ended, .cancelled:
|
||||
self.onBubbleLongPressEnded?(self)
|
||||
case .Began:
|
||||
self.onBubbleLongPressBegan?(cell: self)
|
||||
case .Ended, .Cancelled:
|
||||
self.onBubbleLongPressEnded?(cell: self)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
@ -351,7 +353,8 @@ struct BaseMessageLayoutModel {
|
|||
private (set) var avatarViewFrame = CGRect.zero
|
||||
private (set) var preferredMaxWidthForBubble: CGFloat = 0
|
||||
|
||||
mutating func calculateLayout(parameters: BaseMessageLayoutModelParameters) {
|
||||
|
||||
mutating func calculateLayout(parameters parameters: BaseMessageLayoutModelParameters) {
|
||||
let containerWidth = parameters.containerWidth
|
||||
let isIncoming = parameters.isIncoming
|
||||
let isFailed = parameters.isFailed
|
||||
|
|
@ -362,12 +365,13 @@ struct BaseMessageLayoutModel {
|
|||
let avatarSize = parameters.avatarSize
|
||||
|
||||
let preferredWidthForBubble = (containerWidth * parameters.maxContainerWidthPercentageForBubbleView).bma_round()
|
||||
let bubbleSize = bubbleView.sizeThatFits(CGSize(width: preferredWidthForBubble, height: .greatestFiniteMagnitude))
|
||||
let bubbleSize = bubbleView.sizeThatFits(CGSize(width: preferredWidthForBubble, height: .max))
|
||||
let containerRect = CGRect(origin: CGPoint.zero, size: CGSize(width: containerWidth, height: bubbleSize.height))
|
||||
|
||||
self.bubbleViewFrame = bubbleSize.bma_rect(inContainer: containerRect, xAlignament: .center, yAlignment: .center, dx: 0, dy: 0)
|
||||
self.failedViewFrame = failedButtonSize.bma_rect(inContainer: containerRect, xAlignament: .center, yAlignment: .center, dx: 0, dy: 0)
|
||||
self.avatarViewFrame = avatarSize.bma_rect(inContainer: containerRect, xAlignament: .center, yAlignment: parameters.avatarVerticalAlignment, dx: 0, dy: 0)
|
||||
|
||||
self.bubbleViewFrame = bubbleSize.bma_rect(inContainer: containerRect, xAlignament: .Center, yAlignment: .Center, dx: 0, dy: 0)
|
||||
self.failedViewFrame = failedButtonSize.bma_rect(inContainer: containerRect, xAlignament: .Center, yAlignment: .Center, dx: 0, dy: 0)
|
||||
self.avatarViewFrame = avatarSize.bma_rect(inContainer: containerRect, xAlignament: .Center, yAlignment: parameters.avatarVerticalAlignment, dx: 0, dy: 0)
|
||||
|
||||
// Adjust horizontal positions
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
open class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionViewCellStyleProtocol {
|
||||
public class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionViewCellStyleProtocol {
|
||||
|
||||
typealias Class = BaseMessageCollectionViewCellDefaultStyle
|
||||
|
||||
|
|
@ -32,8 +32,8 @@ open class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionViewC
|
|||
let incoming: () -> UIColor
|
||||
let outgoing: () -> UIColor
|
||||
public init(
|
||||
incoming: @autoclosure @escaping () -> UIColor,
|
||||
outgoing: @autoclosure @escaping () -> UIColor) {
|
||||
@autoclosure(escaping) incoming: () -> UIColor,
|
||||
@autoclosure(escaping) outgoing: () -> UIColor) {
|
||||
self.incoming = incoming
|
||||
self.outgoing = outgoing
|
||||
}
|
||||
|
|
@ -45,10 +45,10 @@ open class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionViewC
|
|||
public let borderOutgoingTail: () -> UIImage
|
||||
public let borderOutgoingNoTail: () -> UIImage
|
||||
public init(
|
||||
borderIncomingTail: @autoclosure @escaping () -> UIImage,
|
||||
borderIncomingNoTail: @autoclosure @escaping () -> UIImage,
|
||||
borderOutgoingTail: @autoclosure @escaping () -> UIImage,
|
||||
borderOutgoingNoTail: @autoclosure @escaping () -> UIImage) {
|
||||
@autoclosure(escaping) borderIncomingTail: () -> UIImage,
|
||||
@autoclosure(escaping) borderIncomingNoTail: () -> UIImage,
|
||||
@autoclosure(escaping) borderOutgoingTail: () -> UIImage,
|
||||
@autoclosure(escaping) borderOutgoingNoTail: () -> UIImage) {
|
||||
self.borderIncomingTail = borderIncomingTail
|
||||
self.borderIncomingNoTail = borderIncomingNoTail
|
||||
self.borderOutgoingTail = borderOutgoingTail
|
||||
|
|
@ -60,8 +60,8 @@ open class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionViewC
|
|||
let normal: () -> UIImage
|
||||
let highlighted: () -> UIImage
|
||||
public init(
|
||||
normal: @autoclosure @escaping () -> UIImage,
|
||||
highlighted: @autoclosure @escaping () -> UIImage) {
|
||||
@autoclosure(escaping) normal: () -> UIImage,
|
||||
@autoclosure(escaping) highlighted: () -> UIImage) {
|
||||
self.normal = normal
|
||||
self.highlighted = highlighted
|
||||
}
|
||||
|
|
@ -71,8 +71,8 @@ open class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionViewC
|
|||
let font: () -> UIFont
|
||||
let color: () -> UIColor
|
||||
public init(
|
||||
font: @autoclosure @escaping () -> UIFont,
|
||||
color: @autoclosure @escaping () -> UIColor) {
|
||||
@autoclosure(escaping) font: () -> UIFont,
|
||||
@autoclosure(escaping) color: () -> UIColor) {
|
||||
self.font = font
|
||||
self.color = color
|
||||
}
|
||||
|
|
@ -81,7 +81,7 @@ open class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionViewC
|
|||
public struct AvatarStyle {
|
||||
let size: CGSize
|
||||
let alignment: VerticalAlignment
|
||||
public init(size: CGSize = .zero, alignment: VerticalAlignment = .bottom) {
|
||||
public init(size: CGSize = .zero, alignment: VerticalAlignment = .Bottom) {
|
||||
self.size = size
|
||||
self.alignment = alignment
|
||||
}
|
||||
|
|
@ -128,11 +128,11 @@ open class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionViewC
|
|||
]
|
||||
}()
|
||||
|
||||
open func attributedStringForDate(_ date: String) -> NSAttributedString {
|
||||
public func attributedStringForDate(date: String) -> NSAttributedString {
|
||||
return NSAttributedString(string: date, attributes: self.dateStringAttributes)
|
||||
}
|
||||
|
||||
open func borderImage(viewModel: MessageViewModelProtocol) -> UIImage? {
|
||||
public func borderImage(viewModel viewModel: MessageViewModelProtocol) -> UIImage? {
|
||||
switch (viewModel.isIncoming, viewModel.showsTail) {
|
||||
case (true, true):
|
||||
return self.borderIncomingTail
|
||||
|
|
@ -145,15 +145,15 @@ open class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionViewC
|
|||
}
|
||||
}
|
||||
|
||||
open func avatarSize(viewModel: MessageViewModelProtocol) -> CGSize {
|
||||
public func avatarSize(viewModel viewModel: MessageViewModelProtocol) -> CGSize {
|
||||
return self.avatarStyle.size
|
||||
}
|
||||
|
||||
open func avatarVerticalAlignment(viewModel: MessageViewModelProtocol) -> VerticalAlignment {
|
||||
public func avatarVerticalAlignment(viewModel viewModel: MessageViewModelProtocol) -> VerticalAlignment {
|
||||
return self.avatarStyle.alignment
|
||||
}
|
||||
|
||||
open func layoutConstants(viewModel: MessageViewModelProtocol) -> BaseMessageCollectionViewCellLayoutConstants {
|
||||
public func layoutConstants(viewModel viewModel: MessageViewModelProtocol) -> BaseMessageCollectionViewCellLayoutConstants {
|
||||
return self.layoutConstants
|
||||
}
|
||||
}
|
||||
|
|
@ -165,25 +165,25 @@ public extension BaseMessageCollectionViewCellDefaultStyle { // Default values
|
|||
|
||||
static public func createDefaultBubbleBorderImages() -> BubbleBorderImages {
|
||||
return BubbleBorderImages(
|
||||
borderIncomingTail: UIImage(named: "bubble-incoming-border-tail", in: Bundle(for: Class.self), compatibleWith: nil)!,
|
||||
borderIncomingNoTail: UIImage(named: "bubble-incoming-border", in: Bundle(for: Class.self), compatibleWith: nil)!,
|
||||
borderOutgoingTail: UIImage(named: "bubble-outgoing-border-tail", in: Bundle(for: Class.self), compatibleWith: nil)!,
|
||||
borderOutgoingNoTail: UIImage(named: "bubble-outgoing-border", in: Bundle(for: Class.self), compatibleWith: nil)!
|
||||
borderIncomingTail: UIImage(named: "bubble-incoming-border-tail", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!,
|
||||
borderIncomingNoTail: UIImage(named: "bubble-incoming-border", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!,
|
||||
borderOutgoingTail: UIImage(named: "bubble-outgoing-border-tail", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!,
|
||||
borderOutgoingNoTail: UIImage(named: "bubble-outgoing-border", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!
|
||||
)
|
||||
}
|
||||
|
||||
static public func createDefaultFailedIconImages() -> FailedIconImages {
|
||||
let normal = {
|
||||
return UIImage(named: "base-message-failed-icon", in: Bundle(for: Class.self), compatibleWith: nil)!
|
||||
return UIImage(named: "base-message-failed-icon", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!
|
||||
}
|
||||
return FailedIconImages(
|
||||
normal: normal(),
|
||||
highlighted: normal().bma_blendWithColor(UIColor.black.withAlphaComponent(0.10))
|
||||
highlighted: normal().bma_blendWithColor(UIColor.blackColor().colorWithAlphaComponent(0.10))
|
||||
)
|
||||
}
|
||||
|
||||
static public func createDefaultDateTextStyle() -> DateTextStyle {
|
||||
return DateTextStyle(font: UIFont.systemFont(ofSize: 12), color: UIColor.bma_color(rgb: 0x9aa3ab))
|
||||
return DateTextStyle(font: UIFont.systemFontOfSize(12), color: UIColor.bma_color(rgb: 0x9aa3ab))
|
||||
}
|
||||
|
||||
static public func createDefaultLayoutConstants() -> BaseMessageCollectionViewCellLayoutConstants {
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@
|
|||
import Foundation
|
||||
|
||||
public enum ViewContext {
|
||||
case normal
|
||||
case sizing // You may skip some cell updates for faster sizing
|
||||
case Normal
|
||||
case Sizing // You may skip some cell updates for faster sizing
|
||||
}
|
||||
|
||||
public protocol MaximumLayoutWidthSpecificable {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ public protocol PhotoMessageModelProtocol: DecoratedMessageModelProtocol {
|
|||
var imageSize: CGSize { get }
|
||||
}
|
||||
|
||||
open class PhotoMessageModel<MessageModelT: MessageModelProtocol>: PhotoMessageModelProtocol {
|
||||
public class PhotoMessageModel<MessageModelT: MessageModelProtocol>: PhotoMessageModelProtocol {
|
||||
public var messageModel: MessageModelProtocol {
|
||||
return self._messageModel
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,12 +24,12 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
open class PhotoMessagePresenter<ViewModelBuilderT, InteractionHandlerT>
|
||||
: BaseMessagePresenter<PhotoBubbleView, ViewModelBuilderT, InteractionHandlerT> where
|
||||
public class PhotoMessagePresenter<ViewModelBuilderT, InteractionHandlerT where
|
||||
ViewModelBuilderT: ViewModelBuilderProtocol,
|
||||
ViewModelBuilderT.ViewModelT: PhotoMessageViewModelProtocol,
|
||||
InteractionHandlerT: BaseMessageInteractionHandlerProtocol,
|
||||
InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT {
|
||||
InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT>
|
||||
: BaseMessagePresenter<PhotoBubbleView, ViewModelBuilderT, InteractionHandlerT> {
|
||||
public typealias ModelT = ViewModelBuilderT.ModelT
|
||||
public typealias ViewModelT = ViewModelBuilderT.ViewModelT
|
||||
|
||||
|
|
@ -52,15 +52,15 @@ open class PhotoMessagePresenter<ViewModelBuilderT, InteractionHandlerT>
|
|||
)
|
||||
}
|
||||
|
||||
public final override class func registerCells(_ collectionView: UICollectionView) {
|
||||
collectionView.register(PhotoMessageCollectionViewCell.self, forCellWithReuseIdentifier: "photo-message")
|
||||
public override class func registerCells(collectionView: UICollectionView) {
|
||||
collectionView.registerClass(PhotoMessageCollectionViewCell.self, forCellWithReuseIdentifier: "photo-message")
|
||||
}
|
||||
|
||||
public final override func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell {
|
||||
return collectionView.dequeueReusableCell(withReuseIdentifier: "photo-message", for: indexPath)
|
||||
public override func dequeueCell(collectionView collectionView: UICollectionView, indexPath: NSIndexPath) -> UICollectionViewCell {
|
||||
return collectionView.dequeueReusableCellWithReuseIdentifier("photo-message", forIndexPath: indexPath)
|
||||
}
|
||||
|
||||
open override func createViewModel() -> ViewModelBuilderT.ViewModelT {
|
||||
public override func createViewModel() -> ViewModelBuilderT.ViewModelT {
|
||||
let viewModel = self.viewModelBuilder.createViewModel(self.messageModel)
|
||||
let updateClosure = { [weak self] (old: Any, new: Any) -> () in
|
||||
self?.updateCurrentCell()
|
||||
|
|
@ -84,7 +84,7 @@ open class PhotoMessagePresenter<ViewModelBuilderT, InteractionHandlerT>
|
|||
return nil
|
||||
}
|
||||
|
||||
open override func configureCell(_ cell: BaseMessageCollectionViewCell<PhotoBubbleView>, decorationAttributes: ChatItemDecorationAttributes, animated: Bool, additionalConfiguration: (() -> Void)?) {
|
||||
public override func configureCell(cell: BaseMessageCollectionViewCell<PhotoBubbleView>, decorationAttributes: ChatItemDecorationAttributes, animated: Bool, additionalConfiguration: (() -> Void)?) {
|
||||
guard let cell = cell as? PhotoMessageCollectionViewCell else {
|
||||
assert(false, "Invalid cell received")
|
||||
return
|
||||
|
|
@ -98,8 +98,8 @@ open class PhotoMessagePresenter<ViewModelBuilderT, InteractionHandlerT>
|
|||
}
|
||||
|
||||
public func updateCurrentCell() {
|
||||
if let cell = self.photoCell, let decorationAttributes = self.decorationAttributes {
|
||||
self.configureCell(cell, decorationAttributes: decorationAttributes, animated: self.itemVisibility != .appearing, additionalConfiguration: nil)
|
||||
if let cell = self.photoCell, decorationAttributes = self.decorationAttributes {
|
||||
self.configureCell(cell, decorationAttributes: decorationAttributes, animated: self.itemVisibility != .Appearing, additionalConfiguration: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,11 +25,12 @@
|
|||
import Foundation
|
||||
import Chatto
|
||||
|
||||
open class PhotoMessagePresenterBuilder<ViewModelBuilderT, InteractionHandlerT>: ChatItemPresenterBuilderProtocol where
|
||||
public class PhotoMessagePresenterBuilder<ViewModelBuilderT, InteractionHandlerT where
|
||||
ViewModelBuilderT: ViewModelBuilderProtocol,
|
||||
ViewModelBuilderT.ViewModelT: PhotoMessageViewModelProtocol,
|
||||
InteractionHandlerT: BaseMessageInteractionHandlerProtocol,
|
||||
InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT {
|
||||
InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT
|
||||
>: ChatItemPresenterBuilderProtocol {
|
||||
public typealias ModelT = ViewModelBuilderT.ModelT
|
||||
public typealias ViewModelT = ViewModelBuilderT.ViewModelT
|
||||
|
||||
|
|
@ -46,11 +47,11 @@ open class PhotoMessagePresenterBuilder<ViewModelBuilderT, InteractionHandlerT>:
|
|||
public lazy var photoCellStyle: PhotoMessageCollectionViewCellStyleProtocol = PhotoMessageCollectionViewCellDefaultStyle()
|
||||
public lazy var baseCellStyle: BaseMessageCollectionViewCellStyleProtocol = BaseMessageCollectionViewCellDefaultStyle()
|
||||
|
||||
open func canHandleChatItem(_ chatItem: ChatItemProtocol) -> Bool {
|
||||
public func canHandleChatItem(chatItem: ChatItemProtocol) -> Bool {
|
||||
return self.viewModelBuilder.canCreateViewModel(fromModel: chatItem)
|
||||
}
|
||||
|
||||
open func createPresenterWithChatItem(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol {
|
||||
public func createPresenterWithChatItem(chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol {
|
||||
assert(self.canHandleChatItem(chatItem))
|
||||
return PhotoMessagePresenter<ViewModelBuilderT, InteractionHandlerT>(
|
||||
messageModel: chatItem as! ModelT,
|
||||
|
|
@ -62,7 +63,7 @@ open class PhotoMessagePresenterBuilder<ViewModelBuilderT, InteractionHandlerT>:
|
|||
)
|
||||
}
|
||||
|
||||
open var presenterType: ChatItemPresenterProtocol.Type {
|
||||
public var presenterType: ChatItemPresenterProtocol.Type {
|
||||
return PhotoMessagePresenter<ViewModelBuilderT, InteractionHandlerT>.self
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,40 +25,40 @@
|
|||
import UIKit
|
||||
|
||||
public enum TransferDirection {
|
||||
case upload
|
||||
case download
|
||||
case Upload
|
||||
case Download
|
||||
}
|
||||
|
||||
public enum TransferStatus {
|
||||
case idle
|
||||
case transfering
|
||||
case failed
|
||||
case success
|
||||
case Idle
|
||||
case Transfering
|
||||
case Failed
|
||||
case Success
|
||||
}
|
||||
|
||||
public protocol PhotoMessageViewModelProtocol: DecoratedMessageViewModelProtocol {
|
||||
var transferDirection: Observable<TransferDirection> { get set }
|
||||
var transferProgress: Observable<Double> { get set } // in [0,1]
|
||||
var transferProgress: Observable<Double> { get set} // in [0,1]
|
||||
var transferStatus: Observable<TransferStatus> { get set }
|
||||
var image: Observable<UIImage?> { get set }
|
||||
var imageSize: CGSize { get }
|
||||
}
|
||||
|
||||
open class PhotoMessageViewModel<PhotoMessageModelT: PhotoMessageModelProtocol>: PhotoMessageViewModelProtocol {
|
||||
public class PhotoMessageViewModel<PhotoMessageModelT: PhotoMessageModelProtocol>: PhotoMessageViewModelProtocol {
|
||||
public var photoMessage: PhotoMessageModelProtocol {
|
||||
return self._photoMessage
|
||||
}
|
||||
public let _photoMessage: PhotoMessageModelT // Can't make photoMessage: PhotoMessageModelT: https://gist.github.com/diegosanchezr/5a66c7af862e1117b556
|
||||
public var transferStatus: Observable<TransferStatus> = Observable(.idle)
|
||||
public var transferStatus: Observable<TransferStatus> = Observable(.Idle)
|
||||
public var transferProgress: Observable<Double> = Observable(0)
|
||||
public var transferDirection: Observable<TransferDirection> = Observable(.download)
|
||||
public var transferDirection: Observable<TransferDirection> = Observable(.Download)
|
||||
public var image: Observable<UIImage?>
|
||||
open var imageSize: CGSize {
|
||||
public var imageSize: CGSize {
|
||||
return self.photoMessage.imageSize
|
||||
}
|
||||
public let messageViewModel: MessageViewModelProtocol
|
||||
open var showsFailedIcon: Bool {
|
||||
return self.messageViewModel.showsFailedIcon || self.transferStatus.value == .failed
|
||||
public var showsFailedIcon: Bool {
|
||||
return self.messageViewModel.showsFailedIcon || self.transferStatus.value == .Failed
|
||||
}
|
||||
|
||||
public init(photoMessage: PhotoMessageModelT, messageViewModel: MessageViewModelProtocol) {
|
||||
|
|
@ -67,27 +67,27 @@ open class PhotoMessageViewModel<PhotoMessageModelT: PhotoMessageModelProtocol>:
|
|||
self.messageViewModel = messageViewModel
|
||||
}
|
||||
|
||||
open func willBeShown() {
|
||||
public func willBeShown() {
|
||||
// Need to declare empty. Otherwise subclass code won't execute (as of Xcode 7.2)
|
||||
}
|
||||
|
||||
open func wasHidden() {
|
||||
public func wasHidden() {
|
||||
// Need to declare empty. Otherwise subclass code won't execute (as of Xcode 7.2)
|
||||
}
|
||||
}
|
||||
|
||||
open class PhotoMessageViewModelDefaultBuilder<PhotoMessageModelT: PhotoMessageModelProtocol>: ViewModelBuilderProtocol {
|
||||
public init() {}
|
||||
public class PhotoMessageViewModelDefaultBuilder<PhotoMessageModelT: PhotoMessageModelProtocol>: ViewModelBuilderProtocol {
|
||||
public init() { }
|
||||
|
||||
let messageViewModelBuilder = MessageViewModelDefaultBuilder()
|
||||
|
||||
open func createViewModel(_ model: PhotoMessageModelT) -> PhotoMessageViewModel<PhotoMessageModelT> {
|
||||
public func createViewModel(model: PhotoMessageModelT) -> PhotoMessageViewModel<PhotoMessageModelT> {
|
||||
let messageViewModel = self.messageViewModelBuilder.createMessageViewModel(model)
|
||||
let photoMessageViewModel = PhotoMessageViewModel(photoMessage: model, messageViewModel: messageViewModel)
|
||||
return photoMessageViewModel
|
||||
}
|
||||
|
||||
open func canCreateViewModel(fromModel model: Any) -> Bool {
|
||||
public func canCreateViewModel(fromModel model: Any) -> Bool {
|
||||
return model is PhotoMessageModelT
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,19 +25,19 @@
|
|||
import UIKit
|
||||
|
||||
public protocol PhotoBubbleViewStyleProtocol {
|
||||
func maskingImage(viewModel: PhotoMessageViewModelProtocol) -> UIImage
|
||||
func borderImage(viewModel: PhotoMessageViewModelProtocol) -> UIImage?
|
||||
func placeholderBackgroundImage(viewModel: PhotoMessageViewModelProtocol) -> UIImage
|
||||
func placeholderIconImage(viewModel: PhotoMessageViewModelProtocol) -> (icon: UIImage?, tintColor: UIColor?)
|
||||
func tailWidth(viewModel: PhotoMessageViewModelProtocol) -> CGFloat
|
||||
func bubbleSize(viewModel: PhotoMessageViewModelProtocol) -> CGSize
|
||||
func progressIndicatorColor(viewModel: PhotoMessageViewModelProtocol) -> UIColor
|
||||
func overlayColor(viewModel: PhotoMessageViewModelProtocol) -> UIColor?
|
||||
func maskingImage(viewModel viewModel: PhotoMessageViewModelProtocol) -> UIImage
|
||||
func borderImage(viewModel viewModel: PhotoMessageViewModelProtocol) -> UIImage?
|
||||
func placeholderBackgroundImage(viewModel viewModel: PhotoMessageViewModelProtocol) -> UIImage
|
||||
func placeholderIconImage(viewModel viewModel: PhotoMessageViewModelProtocol) -> (icon: UIImage?, tintColor: UIColor?)
|
||||
func tailWidth(viewModel viewModel: PhotoMessageViewModelProtocol) -> CGFloat
|
||||
func bubbleSize(viewModel viewModel: PhotoMessageViewModelProtocol) -> CGSize
|
||||
func progressIndicatorColor(viewModel viewModel: PhotoMessageViewModelProtocol) -> UIColor
|
||||
func overlayColor(viewModel viewModel: PhotoMessageViewModelProtocol) -> UIColor?
|
||||
}
|
||||
|
||||
open class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, BackgroundSizingQueryable {
|
||||
public class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, BackgroundSizingQueryable {
|
||||
|
||||
public var viewContext: ViewContext = .normal
|
||||
public var viewContext: ViewContext = .Normal
|
||||
public var animationDuration: CFTimeInterval = 0.33
|
||||
public var preferredMaxLayoutWidth: CGFloat = 0
|
||||
|
||||
|
|
@ -60,11 +60,11 @@ open class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, BackgroundSi
|
|||
|
||||
public private(set) lazy var imageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.autoresizingMask = UIViewAutoresizing()
|
||||
imageView.autoresizingMask = .None
|
||||
imageView.clipsToBounds = true
|
||||
imageView.autoresizesSubviews = false
|
||||
imageView.autoresizingMask = UIViewAutoresizing()
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.autoresizingMask = .None
|
||||
imageView.contentMode = .ScaleAspectFill
|
||||
imageView.addSubview(self.borderView)
|
||||
return imageView
|
||||
}()
|
||||
|
|
@ -78,12 +78,12 @@ open class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, BackgroundSi
|
|||
|
||||
public private(set) var progressIndicatorView: CircleProgressIndicatorView = {
|
||||
let progressView = CircleProgressIndicatorView(size: CGSize(width: 33, height: 33))
|
||||
return progressView!
|
||||
return progressView
|
||||
}()
|
||||
|
||||
private var placeholderIconView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.autoresizingMask = UIViewAutoresizing()
|
||||
imageView.autoresizingMask = .None
|
||||
return imageView
|
||||
}()
|
||||
|
||||
|
|
@ -100,7 +100,7 @@ open class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, BackgroundSi
|
|||
}
|
||||
|
||||
public private(set) var isUpdating: Bool = false
|
||||
public func performBatchUpdates(_ updateClosure: @escaping () -> Void, animated: Bool, completion: (() ->())?) {
|
||||
public func performBatchUpdates(updateClosure: () -> Void, animated: Bool, completion: (() ->())?) {
|
||||
self.isUpdating = true
|
||||
let updateAndRefreshViews = {
|
||||
updateClosure()
|
||||
|
|
@ -111,7 +111,7 @@ open class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, BackgroundSi
|
|||
}
|
||||
}
|
||||
if animated {
|
||||
UIView.animate(withDuration: self.animationDuration, animations: updateAndRefreshViews, completion: { (finished) -> Void in
|
||||
UIView.animateWithDuration(self.animationDuration, animations: updateAndRefreshViews, completion: { (finished) -> Void in
|
||||
completion?()
|
||||
})
|
||||
} else {
|
||||
|
|
@ -119,10 +119,10 @@ open class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, BackgroundSi
|
|||
}
|
||||
}
|
||||
|
||||
open func updateViews() {
|
||||
if self.viewContext == .sizing { return }
|
||||
public func updateViews() {
|
||||
if self.viewContext == .Sizing { return }
|
||||
if isUpdating { return }
|
||||
guard let _ = self.photoMessageViewModel, let _ = self.photoMessageStyle else { return }
|
||||
guard let _ = self.photoMessageViewModel, _ = self.photoMessageStyle else { return }
|
||||
|
||||
self.updateProgressIndicator()
|
||||
self.updateImages()
|
||||
|
|
@ -132,23 +132,23 @@ open class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, BackgroundSi
|
|||
private func updateProgressIndicator() {
|
||||
let transferStatus = self.photoMessageViewModel.transferStatus.value
|
||||
let transferProgress = self.photoMessageViewModel.transferProgress.value
|
||||
self.progressIndicatorView.isHidden = [TransferStatus.idle, TransferStatus.success, TransferStatus.failed].contains(self.photoMessageViewModel.transferStatus.value)
|
||||
self.progressIndicatorView.hidden = [TransferStatus.Idle, TransferStatus.Success, TransferStatus.Failed].contains(self.photoMessageViewModel.transferStatus.value)
|
||||
self.progressIndicatorView.progressLineColor = self.photoMessageStyle.progressIndicatorColor(viewModel: self.photoMessageViewModel)
|
||||
self.progressIndicatorView.progressLineWidth = 1
|
||||
self.progressIndicatorView.setProgress(CGFloat(transferProgress))
|
||||
|
||||
switch transferStatus {
|
||||
case .idle, .success, .failed:
|
||||
case .Idle, .Success, .Failed:
|
||||
|
||||
break
|
||||
case .transfering:
|
||||
case .Transfering:
|
||||
switch transferProgress {
|
||||
case 0:
|
||||
if self.progressIndicatorView.progressStatus != .starting { self.progressIndicatorView.progressStatus = .starting }
|
||||
if self.progressIndicatorView.progressStatus != .Starting { self.progressIndicatorView.progressStatus = .Starting }
|
||||
case 1:
|
||||
if self.progressIndicatorView.progressStatus != .completed { self.progressIndicatorView.progressStatus = .completed }
|
||||
if self.progressIndicatorView.progressStatus != .Completed { self.progressIndicatorView.progressStatus = .Completed }
|
||||
default:
|
||||
if self.progressIndicatorView.progressStatus != .inProgress { self.progressIndicatorView.progressStatus = .inProgress }
|
||||
if self.progressIndicatorView.progressStatus != .InProgress { self.progressIndicatorView.progressStatus = .InProgress }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -156,13 +156,13 @@ open class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, BackgroundSi
|
|||
private func updateImages() {
|
||||
if let image = self.photoMessageViewModel.image.value {
|
||||
self.imageView.image = image
|
||||
self.placeholderIconView.isHidden = true
|
||||
self.placeholderIconView.hidden = true
|
||||
} else {
|
||||
self.imageView.image = self.photoMessageStyle.placeholderBackgroundImage(viewModel: self.photoMessageViewModel)
|
||||
let (icon, tintColor) = photoMessageStyle.placeholderIconImage(viewModel: self.photoMessageViewModel)
|
||||
self.placeholderIconView.image = icon
|
||||
self.placeholderIconView.tintColor = tintColor
|
||||
self.placeholderIconView.isHidden = false
|
||||
self.placeholderIconView.hidden = false
|
||||
}
|
||||
|
||||
if let overlayColor = self.photoMessageStyle.overlayColor(viewModel: self.photoMessageViewModel) {
|
||||
|
|
@ -178,13 +178,14 @@ open class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, BackgroundSi
|
|||
self.imageView.layer.mask = UIImageView(image: self.photoMessageStyle.maskingImage(viewModel: self.photoMessageViewModel)).layer
|
||||
}
|
||||
|
||||
|
||||
// MARK: Layout
|
||||
|
||||
open override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
public override func sizeThatFits(size: CGSize) -> CGSize {
|
||||
return self.calculateTextBubbleLayout(maximumWidth: size.width).size
|
||||
}
|
||||
|
||||
open override func layoutSubviews() {
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
let layout = self.calculateTextBubbleLayout(maximumWidth: self.preferredMaxLayoutWidth)
|
||||
self.progressIndicatorView.center = layout.visualCenter
|
||||
|
|
@ -196,19 +197,20 @@ open class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, BackgroundSi
|
|||
self.borderView.bma_rect = self.imageView.bounds
|
||||
}
|
||||
|
||||
private func calculateTextBubbleLayout(maximumWidth: CGFloat) -> PhotoBubbleLayoutModel {
|
||||
private func calculateTextBubbleLayout(maximumWidth maximumWidth: CGFloat) -> PhotoBubbleLayoutModel {
|
||||
let layoutContext = PhotoBubbleLayoutModel.LayoutContext(photoMessageViewModel: self.photoMessageViewModel, style: self.photoMessageStyle, containerWidth: maximumWidth)
|
||||
let layoutModel = PhotoBubbleLayoutModel(layoutContext: layoutContext)
|
||||
layoutModel.calculateLayout()
|
||||
return layoutModel
|
||||
}
|
||||
|
||||
open var canCalculateSizeInBackground: Bool {
|
||||
public var canCalculateSizeInBackground: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private class PhotoBubbleLayoutModel {
|
||||
var photoFrame: CGRect = CGRect.zero
|
||||
var visualCenter: CGPoint = CGPoint.zero // Because image is cropped a few points on the side of the tail, the apparent center will be a bit shifted
|
||||
|
|
|
|||
|
|
@ -30,10 +30,14 @@ public final class PhotoMessageCollectionViewCell: BaseMessageCollectionViewCell
|
|||
|
||||
static func sizingCell() -> PhotoMessageCollectionViewCell {
|
||||
let cell = PhotoMessageCollectionViewCell(frame: CGRect.zero)
|
||||
cell.viewContext = .sizing
|
||||
cell.viewContext = .Sizing
|
||||
return cell
|
||||
}
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
public override func createBubbleView() -> PhotoBubbleView {
|
||||
return PhotoBubbleView()
|
||||
}
|
||||
|
|
@ -57,7 +61,7 @@ public final class PhotoMessageCollectionViewCell: BaseMessageCollectionViewCell
|
|||
}
|
||||
}
|
||||
|
||||
public override func performBatchUpdates(_ updateClosure: @escaping () -> Void, animated: Bool, completion: (() -> Void)?) {
|
||||
public override func performBatchUpdates(updateClosure: () -> Void, animated: Bool, completion: (() -> Void)?) {
|
||||
super.performBatchUpdates({ () -> Void in
|
||||
self.bubbleView.performBatchUpdates(updateClosure, animated: false, completion: nil)
|
||||
}, animated: animated, completion: completion)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
open class PhotoMessageCollectionViewCellDefaultStyle: PhotoMessageCollectionViewCellStyleProtocol {
|
||||
public class PhotoMessageCollectionViewCellDefaultStyle: PhotoMessageCollectionViewCellStyleProtocol {
|
||||
typealias Class = PhotoMessageCollectionViewCellDefaultStyle
|
||||
|
||||
public struct BubbleMasks {
|
||||
|
|
@ -34,10 +34,10 @@ open class PhotoMessageCollectionViewCellDefaultStyle: PhotoMessageCollectionVie
|
|||
public let outgoingNoTail: () -> UIImage
|
||||
public let tailWidth: CGFloat
|
||||
public init(
|
||||
incomingTail: @autoclosure @escaping () -> UIImage,
|
||||
incomingNoTail: @autoclosure @escaping () -> UIImage,
|
||||
outgoingTail: @autoclosure @escaping () -> UIImage,
|
||||
outgoingNoTail: @autoclosure @escaping () -> UIImage,
|
||||
@autoclosure(escaping) incomingTail: () -> UIImage,
|
||||
@autoclosure(escaping) incomingNoTail: () -> UIImage,
|
||||
@autoclosure(escaping) outgoingTail: () -> UIImage,
|
||||
@autoclosure(escaping) outgoingNoTail: () -> UIImage,
|
||||
tailWidth: CGFloat) {
|
||||
self.incomingTail = incomingTail
|
||||
self.incomingNoTail = incomingNoTail
|
||||
|
|
@ -48,12 +48,12 @@ open class PhotoMessageCollectionViewCellDefaultStyle: PhotoMessageCollectionVie
|
|||
}
|
||||
|
||||
public struct Sizes {
|
||||
public let aspectRatioIntervalForSquaredSize: ClosedRange<CGFloat>
|
||||
public let aspectRatioIntervalForSquaredSize: ClosedInterval<CGFloat>
|
||||
public let photoSizeLandscape: CGSize
|
||||
public let photoSizePortrait: CGSize
|
||||
public let photoSizeSquare: CGSize
|
||||
public init(
|
||||
aspectRatioIntervalForSquaredSize: ClosedRange<CGFloat>,
|
||||
aspectRatioIntervalForSquaredSize: ClosedInterval<CGFloat>,
|
||||
photoSizeLandscape: CGSize,
|
||||
photoSizePortrait: CGSize,
|
||||
photoSizeSquare: CGSize) {
|
||||
|
|
@ -113,10 +113,10 @@ open class PhotoMessageCollectionViewCellDefaultStyle: PhotoMessageCollectionVie
|
|||
}()
|
||||
|
||||
lazy private var placeholderIcon: UIImage = {
|
||||
return UIImage(named: "photo-bubble-placeholder-icon", in: Bundle(for: Class.self), compatibleWith: nil)!
|
||||
return UIImage(named: "photo-bubble-placeholder-icon", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!
|
||||
}()
|
||||
|
||||
open func maskingImage(viewModel: PhotoMessageViewModelProtocol) -> UIImage {
|
||||
public func maskingImage(viewModel viewModel: PhotoMessageViewModelProtocol) -> UIImage {
|
||||
switch (viewModel.isIncoming, viewModel.showsTail) {
|
||||
case (true, true):
|
||||
return self.maskImageIncomingTail
|
||||
|
|
@ -129,44 +129,44 @@ open class PhotoMessageCollectionViewCellDefaultStyle: PhotoMessageCollectionVie
|
|||
}
|
||||
}
|
||||
|
||||
open func borderImage(viewModel: PhotoMessageViewModelProtocol) -> UIImage? {
|
||||
public func borderImage(viewModel viewModel: PhotoMessageViewModelProtocol) -> UIImage? {
|
||||
return self.baseStyle.borderImage(viewModel: viewModel)
|
||||
}
|
||||
|
||||
open func placeholderBackgroundImage(viewModel: PhotoMessageViewModelProtocol) -> UIImage {
|
||||
public func placeholderBackgroundImage(viewModel viewModel: PhotoMessageViewModelProtocol) -> UIImage {
|
||||
return viewModel.isIncoming ? self.placeholderBackgroundIncoming : self.placeholderBackgroundOutgoing
|
||||
}
|
||||
|
||||
open func placeholderIconImage(viewModel: PhotoMessageViewModelProtocol) -> (icon: UIImage?, tintColor: UIColor?) {
|
||||
if viewModel.image.value == nil && viewModel.transferStatus.value == .failed {
|
||||
public func placeholderIconImage(viewModel viewModel: PhotoMessageViewModelProtocol) -> (icon: UIImage?, tintColor: UIColor?) {
|
||||
if viewModel.image.value == nil && viewModel.transferStatus.value == .Failed {
|
||||
let tintColor = viewModel.isIncoming ? self.colors.placeholderIconTintIncoming : self.colors.placeholderIconTintOutgoing
|
||||
return (self.placeholderIcon, tintColor)
|
||||
}
|
||||
return (nil, nil)
|
||||
}
|
||||
|
||||
open func tailWidth(viewModel: PhotoMessageViewModelProtocol) -> CGFloat {
|
||||
public func tailWidth(viewModel viewModel: PhotoMessageViewModelProtocol) -> CGFloat {
|
||||
return self.bubbleMasks.tailWidth
|
||||
}
|
||||
|
||||
open func bubbleSize(viewModel: PhotoMessageViewModelProtocol) -> CGSize {
|
||||
public func bubbleSize(viewModel viewModel: PhotoMessageViewModelProtocol) -> CGSize {
|
||||
let aspectRatio = viewModel.imageSize.height > 0 ? viewModel.imageSize.width / viewModel.imageSize.height : 0
|
||||
|
||||
if aspectRatio == 0 || self.sizes.aspectRatioIntervalForSquaredSize.contains(aspectRatio) {
|
||||
return self.sizes.photoSizeSquare
|
||||
} else if aspectRatio < self.sizes.aspectRatioIntervalForSquaredSize.lowerBound {
|
||||
} else if aspectRatio < self.sizes.aspectRatioIntervalForSquaredSize.start {
|
||||
return self.sizes.photoSizePortrait
|
||||
} else {
|
||||
return self.sizes.photoSizeLandscape
|
||||
}
|
||||
}
|
||||
|
||||
open func progressIndicatorColor(viewModel: PhotoMessageViewModelProtocol) -> UIColor {
|
||||
public func progressIndicatorColor(viewModel viewModel: PhotoMessageViewModelProtocol) -> UIColor {
|
||||
return viewModel.isIncoming ? self.colors.progressIndicatorColorIncoming : self.colors.progressIndicatorColorOutgoing
|
||||
}
|
||||
|
||||
open func overlayColor(viewModel: PhotoMessageViewModelProtocol) -> UIColor? {
|
||||
let showsOverlay = viewModel.image.value != nil && (viewModel.transferStatus.value == .transfering || viewModel.status != MessageViewModelStatus.success)
|
||||
public func overlayColor(viewModel viewModel: PhotoMessageViewModelProtocol) -> UIColor? {
|
||||
let showsOverlay = viewModel.image.value != nil && (viewModel.transferStatus.value == .Transfering || viewModel.status != MessageViewModelStatus.Success)
|
||||
return showsOverlay ? self.colors.overlayColor : nil
|
||||
}
|
||||
|
||||
|
|
@ -176,10 +176,10 @@ public extension PhotoMessageCollectionViewCellDefaultStyle { // Default values
|
|||
|
||||
static public func createDefaultBubbleMasks() -> BubbleMasks {
|
||||
return BubbleMasks(
|
||||
incomingTail: UIImage(named: "bubble-incoming-tail", in: Bundle(for: Class.self), compatibleWith: nil)!,
|
||||
incomingNoTail: UIImage(named: "bubble-incoming", in: Bundle(for: Class.self), compatibleWith: nil)!,
|
||||
outgoingTail: UIImage(named: "bubble-outgoing-tail", in: Bundle(for: Class.self), compatibleWith: nil)!,
|
||||
outgoingNoTail: UIImage(named: "bubble-outgoing", in: Bundle(for: Class.self), compatibleWith: nil)!,
|
||||
incomingTail: UIImage(named: "bubble-incoming-tail", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!,
|
||||
incomingNoTail: UIImage(named: "bubble-incoming", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!,
|
||||
outgoingTail: UIImage(named: "bubble-outgoing-tail", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!,
|
||||
outgoingNoTail: UIImage(named: "bubble-outgoing", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!,
|
||||
tailWidth: 6
|
||||
)
|
||||
}
|
||||
|
|
@ -198,8 +198,8 @@ public extension PhotoMessageCollectionViewCellDefaultStyle { // Default values
|
|||
placeholderIconTintIncoming: UIColor.bma_color(rgb: 0xced6dc),
|
||||
placeholderIconTintOutgoing: UIColor.bma_color(rgb: 0x508dfc),
|
||||
progressIndicatorColorIncoming: UIColor.bma_color(rgb: 0x98a3ab),
|
||||
progressIndicatorColorOutgoing: UIColor.white,
|
||||
overlayColor: UIColor.black.withAlphaComponent(0.70)
|
||||
progressIndicatorColorOutgoing: UIColor.whiteColor(),
|
||||
overlayColor: UIColor.blackColor().colorWithAlphaComponent(0.70)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public protocol TextMessageModelProtocol: DecoratedMessageModelProtocol {
|
|||
var text: String { get }
|
||||
}
|
||||
|
||||
open class TextMessageModel<MessageModelT: MessageModelProtocol>: TextMessageModelProtocol {
|
||||
public class TextMessageModel<MessageModelT: MessageModelProtocol>: TextMessageModelProtocol {
|
||||
public var messageModel: MessageModelProtocol {
|
||||
return self._messageModel
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,12 +24,12 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
open class TextMessagePresenter<ViewModelBuilderT, InteractionHandlerT>
|
||||
: BaseMessagePresenter<TextBubbleView, ViewModelBuilderT, InteractionHandlerT> where
|
||||
public class TextMessagePresenter<ViewModelBuilderT, InteractionHandlerT where
|
||||
ViewModelBuilderT: ViewModelBuilderProtocol,
|
||||
ViewModelBuilderT.ViewModelT: TextMessageViewModelProtocol,
|
||||
InteractionHandlerT: BaseMessageInteractionHandlerProtocol,
|
||||
InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT {
|
||||
InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT>
|
||||
: BaseMessagePresenter<TextBubbleView, ViewModelBuilderT, InteractionHandlerT> {
|
||||
public typealias ModelT = ViewModelBuilderT.ModelT
|
||||
public typealias ViewModelT = ViewModelBuilderT.ViewModelT
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ open class TextMessagePresenter<ViewModelBuilderT, InteractionHandlerT>
|
|||
sizingCell: TextMessageCollectionViewCell,
|
||||
baseCellStyle: BaseMessageCollectionViewCellStyleProtocol,
|
||||
textCellStyle: TextMessageCollectionViewCellStyleProtocol,
|
||||
layoutCache: NSCache<AnyObject, AnyObject>) {
|
||||
layoutCache: NSCache) {
|
||||
self.layoutCache = layoutCache
|
||||
self.textCellStyle = textCellStyle
|
||||
super.init(
|
||||
|
|
@ -52,20 +52,20 @@ open class TextMessagePresenter<ViewModelBuilderT, InteractionHandlerT>
|
|||
)
|
||||
}
|
||||
|
||||
let layoutCache: NSCache<AnyObject, AnyObject>
|
||||
let layoutCache: NSCache
|
||||
let textCellStyle: TextMessageCollectionViewCellStyleProtocol
|
||||
|
||||
public final override class func registerCells(_ collectionView: UICollectionView) {
|
||||
collectionView.register(TextMessageCollectionViewCell.self, forCellWithReuseIdentifier: "text-message-incoming")
|
||||
collectionView.register(TextMessageCollectionViewCell.self, forCellWithReuseIdentifier: "text-message-outcoming")
|
||||
public override class func registerCells(collectionView: UICollectionView) {
|
||||
collectionView.registerClass(TextMessageCollectionViewCell.self, forCellWithReuseIdentifier: "text-message-incoming")
|
||||
collectionView.registerClass(TextMessageCollectionViewCell.self, forCellWithReuseIdentifier: "text-message-outcoming")
|
||||
}
|
||||
|
||||
public final override func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell {
|
||||
public override func dequeueCell(collectionView collectionView: UICollectionView, indexPath: NSIndexPath) -> UICollectionViewCell {
|
||||
let identifier = self.messageViewModel.isIncoming ? "text-message-incoming" : "text-message-outcoming"
|
||||
return collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath)
|
||||
return collectionView.dequeueReusableCellWithReuseIdentifier(identifier, forIndexPath: indexPath)
|
||||
}
|
||||
|
||||
open override func createViewModel() -> ViewModelBuilderT.ViewModelT {
|
||||
public override func createViewModel() -> ViewModelBuilderT.ViewModelT {
|
||||
let viewModel = self.viewModelBuilder.createViewModel(self.messageModel)
|
||||
let updateClosure = { [weak self] (old: Any, new: Any) -> () in
|
||||
self?.updateCurrentCell()
|
||||
|
|
@ -85,7 +85,7 @@ open class TextMessagePresenter<ViewModelBuilderT, InteractionHandlerT>
|
|||
return nil
|
||||
}
|
||||
|
||||
open override func configureCell(_ cell: BaseMessageCollectionViewCell<TextBubbleView>, decorationAttributes: ChatItemDecorationAttributes, animated: Bool, additionalConfiguration: (() -> Void)?) {
|
||||
public override func configureCell(cell: BaseMessageCollectionViewCell<TextBubbleView>, decorationAttributes: ChatItemDecorationAttributes, animated: Bool, additionalConfiguration: (() -> Void)?) {
|
||||
guard let cell = cell as? TextMessageCollectionViewCell else {
|
||||
assert(false, "Invalid cell received")
|
||||
return
|
||||
|
|
@ -100,24 +100,32 @@ open class TextMessagePresenter<ViewModelBuilderT, InteractionHandlerT>
|
|||
}
|
||||
|
||||
public func updateCurrentCell() {
|
||||
if let cell = self.textCell, let decorationAttributes = self.decorationAttributes {
|
||||
self.configureCell(cell, decorationAttributes: decorationAttributes, animated: self.itemVisibility != .appearing, additionalConfiguration: nil)
|
||||
if let cell = self.textCell, decorationAttributes = self.decorationAttributes {
|
||||
self.configureCell(cell, decorationAttributes: decorationAttributes, animated: self.itemVisibility != .Appearing, additionalConfiguration: nil)
|
||||
}
|
||||
}
|
||||
|
||||
open override func canShowMenu() -> Bool {
|
||||
public override func canShowMenu() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
open override func canPerformMenuControllerAction(_ action: Selector) -> Bool {
|
||||
let selector = #selector(UIResponderStandardEditActions.copy(_:))
|
||||
public override func canPerformMenuControllerAction(action: Selector) -> Bool {
|
||||
#if swift(>=2.3)
|
||||
let selector = #selector(UIResponderStandardEditActions.copy(_:))
|
||||
#else
|
||||
let selector = #selector(NSObject.copy(_:))
|
||||
#endif
|
||||
return action == selector
|
||||
}
|
||||
|
||||
open override func performMenuControllerAction(_ action: Selector) {
|
||||
let selector = #selector(UIResponderStandardEditActions.copy(_:))
|
||||
public override func performMenuControllerAction(action: Selector) {
|
||||
#if swift(>=2.3)
|
||||
let selector = #selector(UIResponderStandardEditActions.copy(_:))
|
||||
#else
|
||||
let selector = #selector(NSObject.copy(_:))
|
||||
#endif
|
||||
if action == selector {
|
||||
UIPasteboard.general.string = self.messageViewModel.text
|
||||
UIPasteboard.generalPasteboard().string = self.messageViewModel.text
|
||||
} else {
|
||||
assert(false, "Unexpected action")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@
|
|||
import Foundation
|
||||
import Chatto
|
||||
|
||||
open class TextMessagePresenterBuilder<ViewModelBuilderT, InteractionHandlerT>
|
||||
: ChatItemPresenterBuilderProtocol where
|
||||
public class TextMessagePresenterBuilder<ViewModelBuilderT, InteractionHandlerT where
|
||||
ViewModelBuilderT: ViewModelBuilderProtocol,
|
||||
ViewModelBuilderT.ViewModelT: TextMessageViewModelProtocol,
|
||||
InteractionHandlerT: BaseMessageInteractionHandlerProtocol,
|
||||
InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT {
|
||||
InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT>
|
||||
: ChatItemPresenterBuilderProtocol {
|
||||
typealias ViewModelT = ViewModelBuilderT.ViewModelT
|
||||
typealias ModelT = ViewModelBuilderT.ModelT
|
||||
|
||||
|
|
@ -43,14 +43,14 @@ open class TextMessagePresenterBuilder<ViewModelBuilderT, InteractionHandlerT>
|
|||
|
||||
let viewModelBuilder: ViewModelBuilderT
|
||||
let interactionHandler: InteractionHandlerT?
|
||||
let layoutCache = NSCache<AnyObject, AnyObject>()
|
||||
let layoutCache = NSCache()
|
||||
|
||||
lazy var sizingCell: TextMessageCollectionViewCell = {
|
||||
var cell: TextMessageCollectionViewCell? = nil
|
||||
if Thread.isMainThread {
|
||||
if NSThread.isMainThread() {
|
||||
cell = TextMessageCollectionViewCell.sizingCell()
|
||||
} else {
|
||||
DispatchQueue.main.sync(execute: {
|
||||
dispatch_sync(dispatch_get_main_queue(), {
|
||||
cell = TextMessageCollectionViewCell.sizingCell()
|
||||
})
|
||||
}
|
||||
|
|
@ -61,11 +61,11 @@ open class TextMessagePresenterBuilder<ViewModelBuilderT, InteractionHandlerT>
|
|||
public lazy var textCellStyle: TextMessageCollectionViewCellStyleProtocol = TextMessageCollectionViewCellDefaultStyle()
|
||||
public lazy var baseMessageStyle: BaseMessageCollectionViewCellStyleProtocol = BaseMessageCollectionViewCellDefaultStyle()
|
||||
|
||||
open func canHandleChatItem(_ chatItem: ChatItemProtocol) -> Bool {
|
||||
public func canHandleChatItem(chatItem: ChatItemProtocol) -> Bool {
|
||||
return self.viewModelBuilder.canCreateViewModel(fromModel: chatItem)
|
||||
}
|
||||
|
||||
open func createPresenterWithChatItem(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol {
|
||||
public func createPresenterWithChatItem(chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol {
|
||||
assert(self.canHandleChatItem(chatItem))
|
||||
return TextMessagePresenter<ViewModelBuilderT, InteractionHandlerT>(
|
||||
messageModel: chatItem as! ModelT,
|
||||
|
|
@ -78,7 +78,7 @@ open class TextMessagePresenterBuilder<ViewModelBuilderT, InteractionHandlerT>
|
|||
)
|
||||
}
|
||||
|
||||
open var presenterType: ChatItemPresenterProtocol.Type {
|
||||
public var presenterType: ChatItemPresenterProtocol.Type {
|
||||
return TextMessagePresenter<ViewModelBuilderT, InteractionHandlerT>.self
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public protocol TextMessageViewModelProtocol: DecoratedMessageViewModelProtocol
|
|||
var text: String { get }
|
||||
}
|
||||
|
||||
open class TextMessageViewModel<TextMessageModelT: TextMessageModelProtocol>: TextMessageViewModelProtocol {
|
||||
public class TextMessageViewModel<TextMessageModelT: TextMessageModelProtocol>: TextMessageViewModelProtocol {
|
||||
public var text: String {
|
||||
return self.textMessage.text
|
||||
}
|
||||
|
|
@ -40,27 +40,27 @@ open class TextMessageViewModel<TextMessageModelT: TextMessageModelProtocol>: Te
|
|||
self.messageViewModel = messageViewModel
|
||||
}
|
||||
|
||||
open func willBeShown() {
|
||||
public func willBeShown() {
|
||||
// Need to declare empty. Otherwise subclass code won't execute (as of Xcode 7.2)
|
||||
}
|
||||
|
||||
open func wasHidden() {
|
||||
public func wasHidden() {
|
||||
// Need to declare empty. Otherwise subclass code won't execute (as of Xcode 7.2)
|
||||
}
|
||||
}
|
||||
|
||||
open class TextMessageViewModelDefaultBuilder<TextMessageModelT: TextMessageModelProtocol>: ViewModelBuilderProtocol {
|
||||
public init() {}
|
||||
public class TextMessageViewModelDefaultBuilder<TextMessageModelT: TextMessageModelProtocol>: ViewModelBuilderProtocol {
|
||||
public init() { }
|
||||
|
||||
let messageViewModelBuilder = MessageViewModelDefaultBuilder()
|
||||
|
||||
open func createViewModel(_ textMessage: TextMessageModelT) -> TextMessageViewModel<TextMessageModelT> {
|
||||
public func createViewModel(textMessage: TextMessageModelT) -> TextMessageViewModel<TextMessageModelT> {
|
||||
let messageViewModel = self.messageViewModelBuilder.createMessageViewModel(textMessage)
|
||||
let textMessageViewModel = TextMessageViewModel(textMessage: textMessage, messageViewModel: messageViewModel)
|
||||
return textMessageViewModel
|
||||
}
|
||||
|
||||
open func canCreateViewModel(fromModel model: Any) -> Bool {
|
||||
public func canCreateViewModel(fromModel model: Any) -> Bool {
|
||||
return model is TextMessageModelT
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,25 +25,25 @@
|
|||
import UIKit
|
||||
|
||||
public protocol TextBubbleViewStyleProtocol {
|
||||
func bubbleImage(viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIImage
|
||||
func bubbleImageBorder(viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIImage?
|
||||
func textFont(viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIFont
|
||||
func textColor(viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIColor
|
||||
func textInsets(viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIEdgeInsets
|
||||
func bubbleImage(viewModel viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIImage
|
||||
func bubbleImageBorder(viewModel viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIImage?
|
||||
func textFont(viewModel viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIFont
|
||||
func textColor(viewModel viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIColor
|
||||
func textInsets(viewModel viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIEdgeInsets
|
||||
}
|
||||
|
||||
public final class TextBubbleView: UIView, MaximumLayoutWidthSpecificable, BackgroundSizingQueryable {
|
||||
|
||||
public var preferredMaxLayoutWidth: CGFloat = 0
|
||||
public var animationDuration: CFTimeInterval = 0.33
|
||||
public var viewContext: ViewContext = .normal {
|
||||
public var viewContext: ViewContext = .Normal {
|
||||
didSet {
|
||||
if self.viewContext == .sizing {
|
||||
self.textView.dataDetectorTypes = UIDataDetectorTypes()
|
||||
self.textView.isSelectable = false
|
||||
if self.viewContext == .Sizing {
|
||||
self.textView.dataDetectorTypes = .None
|
||||
self.textView.selectable = false
|
||||
} else {
|
||||
self.textView.dataDetectorTypes = .all
|
||||
self.textView.isSelectable = true
|
||||
self.textView.dataDetectorTypes = .All
|
||||
self.textView.selectable = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -93,25 +93,25 @@ public final class TextBubbleView: UIView, MaximumLayoutWidthSpecificable, Backg
|
|||
private var textView: UITextView = {
|
||||
let textView = ChatMessageTextView()
|
||||
UIView.performWithoutAnimation({ () -> Void in // fixes iOS 8 blinking when cell appears
|
||||
textView.backgroundColor = UIColor.clear
|
||||
textView.backgroundColor = UIColor.clearColor()
|
||||
})
|
||||
textView.isEditable = false
|
||||
textView.isSelectable = true
|
||||
textView.dataDetectorTypes = .all
|
||||
textView.editable = false
|
||||
textView.selectable = true
|
||||
textView.dataDetectorTypes = .All
|
||||
textView.scrollsToTop = false
|
||||
textView.isScrollEnabled = false
|
||||
textView.scrollEnabled = false
|
||||
textView.bounces = false
|
||||
textView.bouncesZoom = false
|
||||
textView.showsHorizontalScrollIndicator = false
|
||||
textView.showsVerticalScrollIndicator = false
|
||||
textView.layoutManager.allowsNonContiguousLayout = true
|
||||
textView.isExclusiveTouch = true
|
||||
textView.exclusiveTouch = true
|
||||
textView.textContainer.lineFragmentPadding = 0
|
||||
return textView
|
||||
}()
|
||||
|
||||
public private(set) var isUpdating: Bool = false
|
||||
public func performBatchUpdates(_ updateClosure: @escaping () -> Void, animated: Bool, completion: (() -> Void)?) {
|
||||
public func performBatchUpdates(updateClosure: () -> Void, animated: Bool, completion: (() -> Void)?) {
|
||||
self.isUpdating = true
|
||||
let updateAndRefreshViews = {
|
||||
updateClosure()
|
||||
|
|
@ -122,7 +122,7 @@ public final class TextBubbleView: UIView, MaximumLayoutWidthSpecificable, Backg
|
|||
}
|
||||
}
|
||||
if animated {
|
||||
UIView.animate(withDuration: self.animationDuration, animations: updateAndRefreshViews, completion: { (finished) -> Void in
|
||||
UIView.animateWithDuration(self.animationDuration, animations: updateAndRefreshViews, completion: { (finished) -> Void in
|
||||
completion?()
|
||||
})
|
||||
} else {
|
||||
|
|
@ -131,19 +131,19 @@ public final class TextBubbleView: UIView, MaximumLayoutWidthSpecificable, Backg
|
|||
}
|
||||
|
||||
private func updateViews() {
|
||||
if self.viewContext == .sizing { return }
|
||||
if self.viewContext == .Sizing { return }
|
||||
if isUpdating { return }
|
||||
guard let style = self.style else { return }
|
||||
|
||||
self.updateTextView()
|
||||
let bubbleImage = style.bubbleImage(viewModel: self.textMessageViewModel, isSelected: self.selected)
|
||||
let borderImage = style.bubbleImageBorder(viewModel: self.textMessageViewModel, isSelected: self.selected)
|
||||
if self.bubbleImageView.image != bubbleImage { self.bubbleImageView.image = bubbleImage }
|
||||
if self.bubbleImageView.image != bubbleImage { self.bubbleImageView.image = bubbleImage}
|
||||
if self.borderImageView.image != borderImage { self.borderImageView.image = borderImage }
|
||||
}
|
||||
|
||||
private func updateTextView() {
|
||||
guard let style = self.style, let viewModel = self.textMessageViewModel else { return }
|
||||
guard let style = self.style, viewModel = self.textMessageViewModel else { return }
|
||||
|
||||
let font = style.textFont(viewModel: viewModel, isSelected: self.selected)
|
||||
let textColor = style.textColor(viewModel: viewModel, isSelected: self.selected)
|
||||
|
|
@ -159,7 +159,7 @@ public final class TextBubbleView: UIView, MaximumLayoutWidthSpecificable, Backg
|
|||
self.textView.textColor = textColor
|
||||
self.textView.linkTextAttributes = [
|
||||
NSForegroundColorAttributeName: textColor,
|
||||
NSUnderlineStyleAttributeName : NSUnderlineStyle.styleSingle.rawValue
|
||||
NSUnderlineStyleAttributeName : NSUnderlineStyle.StyleSingle.rawValue
|
||||
]
|
||||
needsToUpdateText = true
|
||||
}
|
||||
|
|
@ -176,7 +176,7 @@ public final class TextBubbleView: UIView, MaximumLayoutWidthSpecificable, Backg
|
|||
return self.style.bubbleImage(viewModel: self.textMessageViewModel, isSelected: self.selected)
|
||||
}
|
||||
|
||||
public override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
public override func sizeThatFits(size: CGSize) -> CGSize {
|
||||
return self.calculateTextBubbleLayout(preferredMaxLayoutWidth: size.width).size
|
||||
}
|
||||
|
||||
|
|
@ -189,8 +189,8 @@ public final class TextBubbleView: UIView, MaximumLayoutWidthSpecificable, Backg
|
|||
self.borderImageView.bma_rect = self.bubbleImageView.bounds
|
||||
}
|
||||
|
||||
public var layoutCache: NSCache<AnyObject, AnyObject>!
|
||||
private func calculateTextBubbleLayout(preferredMaxLayoutWidth: CGFloat) -> TextBubbleLayoutModel {
|
||||
public var layoutCache: NSCache!
|
||||
private func calculateTextBubbleLayout(preferredMaxLayoutWidth preferredMaxLayoutWidth: CGFloat) -> TextBubbleLayoutModel {
|
||||
let layoutContext = TextBubbleLayoutModel.LayoutContext(
|
||||
text: self.textMessageViewModel.text,
|
||||
font: self.style.textFont(viewModel: self.textMessageViewModel, isSelected: self.selected),
|
||||
|
|
@ -198,14 +198,14 @@ public final class TextBubbleView: UIView, MaximumLayoutWidthSpecificable, Backg
|
|||
preferredMaxLayoutWidth: preferredMaxLayoutWidth
|
||||
)
|
||||
|
||||
if let layoutModel = self.layoutCache.object(forKey: layoutContext.hashValue as AnyObject) as? TextBubbleLayoutModel, layoutModel.layoutContext == layoutContext {
|
||||
if let layoutModel = self.layoutCache.objectForKey(layoutContext.hashValue) as? TextBubbleLayoutModel where layoutModel.layoutContext == layoutContext {
|
||||
return layoutModel
|
||||
}
|
||||
|
||||
let layoutModel = TextBubbleLayoutModel(layoutContext: layoutContext)
|
||||
layoutModel.calculateLayout()
|
||||
|
||||
self.layoutCache.setObject(layoutModel, forKey: layoutContext.hashValue as AnyObject)
|
||||
self.layoutCache.setObject(layoutModel, forKey: layoutContext.hashValue)
|
||||
return layoutModel
|
||||
}
|
||||
|
||||
|
|
@ -214,6 +214,7 @@ public final class TextBubbleView: UIView, MaximumLayoutWidthSpecificable, Backg
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private final class TextBubbleLayoutModel {
|
||||
let layoutContext: LayoutContext
|
||||
var textFrame: CGRect = CGRect.zero
|
||||
|
|
@ -235,12 +236,6 @@ private final class TextBubbleLayoutModel {
|
|||
return self.text.hashValue ^ self.textInsets.bma_hashValue ^ self.preferredMaxLayoutWidth.hashValue ^ self.font.hashValue
|
||||
}
|
||||
}
|
||||
|
||||
static func == (lhs: TextBubbleLayoutModel.LayoutContext, rhs: TextBubbleLayoutModel.LayoutContext) -> Bool {
|
||||
let lhsValues = (lhs.text, lhs.textInsets, lhs.font, lhs.preferredMaxLayoutWidth)
|
||||
let rhsValues = (rhs.text, rhs.textInsets, rhs.font, rhs.preferredMaxLayoutWidth)
|
||||
return lhsValues == rhsValues
|
||||
}
|
||||
}
|
||||
|
||||
func calculateLayout() {
|
||||
|
|
@ -253,9 +248,9 @@ private final class TextBubbleLayoutModel {
|
|||
self.size = bubbleSize
|
||||
}
|
||||
|
||||
private func textSizeThatFitsWidth(_ width: CGFloat) -> CGSize {
|
||||
private func textSizeThatFitsWidth(width: CGFloat) -> CGSize {
|
||||
let textContainer: NSTextContainer = {
|
||||
let size = CGSize(width: width, height: .greatestFiniteMagnitude)
|
||||
let size = CGSize(width: width, height: .max)
|
||||
let container = NSTextContainer(size: size)
|
||||
container.lineFragmentPadding = 0
|
||||
return container
|
||||
|
|
@ -269,7 +264,7 @@ private final class TextBubbleLayoutModel {
|
|||
return layoutManager
|
||||
}()
|
||||
|
||||
let rect = layoutManager.usedRect(for: textContainer)
|
||||
let rect = layoutManager.usedRectForTextContainer(textContainer)
|
||||
return rect.size.bma_round()
|
||||
}
|
||||
|
||||
|
|
@ -282,20 +277,26 @@ private final class TextBubbleLayoutModel {
|
|||
}
|
||||
}
|
||||
|
||||
private func == (lhs: TextBubbleLayoutModel.LayoutContext, rhs: TextBubbleLayoutModel.LayoutContext) -> Bool {
|
||||
let lhsValues = (lhs.text, lhs.textInsets, lhs.font, lhs.preferredMaxLayoutWidth)
|
||||
let rhsValues = (rhs.text, rhs.textInsets, rhs.font, rhs.preferredMaxLayoutWidth)
|
||||
return lhsValues == rhsValues
|
||||
}
|
||||
|
||||
/// UITextView with hacks to avoid selection, loupe, define...
|
||||
private final class ChatMessageTextView: UITextView {
|
||||
|
||||
override var canBecomeFirstResponder: Bool {
|
||||
override func canBecomeFirstResponder() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
override func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
|
||||
if type(of: gestureRecognizer) == UILongPressGestureRecognizer.self && gestureRecognizer.delaysTouchesEnded {
|
||||
override func addGestureRecognizer(gestureRecognizer: UIGestureRecognizer) {
|
||||
if gestureRecognizer.dynamicType == UILongPressGestureRecognizer.self && gestureRecognizer.delaysTouchesEnded {
|
||||
super.addGestureRecognizer(gestureRecognizer)
|
||||
}
|
||||
}
|
||||
|
||||
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||
override func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,17 +30,21 @@ public final class TextMessageCollectionViewCell: BaseMessageCollectionViewCell<
|
|||
|
||||
public static func sizingCell() -> TextMessageCollectionViewCell {
|
||||
let cell = TextMessageCollectionViewCell(frame: CGRect.zero)
|
||||
cell.viewContext = .sizing
|
||||
cell.viewContext = .Sizing
|
||||
return cell
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
// MARK: Subclassing (view creation)
|
||||
|
||||
public override func createBubbleView() -> TextBubbleView {
|
||||
return TextBubbleView()
|
||||
}
|
||||
|
||||
public override func performBatchUpdates(_ updateClosure: @escaping () -> Void, animated: Bool, completion: (() -> Void)?) {
|
||||
public override func performBatchUpdates(updateClosure: () -> Void, animated: Bool, completion: (() -> Void)?) {
|
||||
super.performBatchUpdates({ () -> Void in
|
||||
self.bubbleView.performBatchUpdates(updateClosure, animated: false, completion: nil)
|
||||
}, animated: animated, completion: completion)
|
||||
|
|
@ -67,13 +71,13 @@ public final class TextMessageCollectionViewCell: BaseMessageCollectionViewCell<
|
|||
}
|
||||
}
|
||||
|
||||
override public var isSelected: Bool {
|
||||
override public var selected: Bool {
|
||||
didSet {
|
||||
self.bubbleView.selected = self.isSelected
|
||||
self.bubbleView.selected = self.selected
|
||||
}
|
||||
}
|
||||
|
||||
public var layoutCache: NSCache<AnyObject, AnyObject>! {
|
||||
public var layoutCache: NSCache! {
|
||||
didSet {
|
||||
self.bubbleView.layoutCache = self.layoutCache
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
open class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionViewCellStyleProtocol {
|
||||
public class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionViewCellStyleProtocol {
|
||||
typealias Class = TextMessageCollectionViewCellDefaultStyle
|
||||
|
||||
public struct BubbleImages {
|
||||
|
|
@ -33,10 +33,10 @@ open class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionViewC
|
|||
let outgoingTail: () -> UIImage
|
||||
let outgoingNoTail: () -> UIImage
|
||||
public init(
|
||||
incomingTail: @autoclosure @escaping () -> UIImage,
|
||||
incomingNoTail: @autoclosure @escaping () -> UIImage,
|
||||
outgoingTail: @autoclosure @escaping () -> UIImage,
|
||||
outgoingNoTail: @autoclosure @escaping () -> UIImage) {
|
||||
@autoclosure(escaping) incomingTail: () -> UIImage,
|
||||
@autoclosure(escaping) incomingNoTail: () -> UIImage,
|
||||
@autoclosure(escaping) outgoingTail: () -> UIImage,
|
||||
@autoclosure(escaping) outgoingNoTail: () -> UIImage) {
|
||||
self.incomingTail = incomingTail
|
||||
self.incomingNoTail = incomingNoTail
|
||||
self.outgoingTail = outgoingTail
|
||||
|
|
@ -51,9 +51,9 @@ open class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionViewC
|
|||
let incomingInsets: UIEdgeInsets
|
||||
let outgoingInsets: UIEdgeInsets
|
||||
public init(
|
||||
font: @autoclosure @escaping () -> UIFont,
|
||||
incomingColor: @autoclosure @escaping () -> UIColor,
|
||||
outgoingColor: @autoclosure @escaping () -> UIColor,
|
||||
@autoclosure(escaping) font: () -> UIFont,
|
||||
@autoclosure(escaping) incomingColor: () -> UIColor,
|
||||
@autoclosure(escaping) outgoingColor: () -> UIColor,
|
||||
incomingInsets: UIEdgeInsets,
|
||||
outgoingInsets: UIEdgeInsets) {
|
||||
self.font = font
|
||||
|
|
@ -64,6 +64,7 @@ open class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionViewC
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public let bubbleImages: BubbleImages
|
||||
public let textStyle: TextStyle
|
||||
public let baseStyle: BaseMessageCollectionViewCellDefaultStyle
|
||||
|
|
@ -89,23 +90,23 @@ open class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionViewC
|
|||
lazy var incomingColor: UIColor = self.textStyle.incomingColor()
|
||||
lazy var outgoingColor: UIColor = self.textStyle.outgoingColor()
|
||||
|
||||
open func textFont(viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIFont {
|
||||
public func textFont(viewModel viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIFont {
|
||||
return self.font
|
||||
}
|
||||
|
||||
open func textColor(viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIColor {
|
||||
public func textColor(viewModel viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIColor {
|
||||
return viewModel.isIncoming ? self.incomingColor : self.outgoingColor
|
||||
}
|
||||
|
||||
open func textInsets(viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIEdgeInsets {
|
||||
public func textInsets(viewModel viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIEdgeInsets {
|
||||
return viewModel.isIncoming ? self.textStyle.incomingInsets : self.textStyle.outgoingInsets
|
||||
}
|
||||
|
||||
open func bubbleImageBorder(viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIImage? {
|
||||
public func bubbleImageBorder(viewModel viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIImage? {
|
||||
return self.baseStyle.borderImage(viewModel: viewModel)
|
||||
}
|
||||
|
||||
open func bubbleImage(viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIImage {
|
||||
public func bubbleImage(viewModel viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIImage {
|
||||
let key = ImageKey.normal(isIncoming: viewModel.isIncoming, status: viewModel.status, showsTail: viewModel.showsTail, isSelected: isSelected)
|
||||
|
||||
if let image = self.images[key] {
|
||||
|
|
@ -123,18 +124,18 @@ open class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionViewC
|
|||
return UIImage()
|
||||
}
|
||||
|
||||
open func createImage(templateImage image: UIImage, isIncoming: Bool, status: MessageViewModelStatus, isSelected: Bool) -> UIImage {
|
||||
public func createImage(templateImage image: UIImage, isIncoming: Bool, status: MessageViewModelStatus, isSelected: Bool) -> UIImage {
|
||||
var color = isIncoming ? self.baseStyle.baseColorIncoming : self.baseStyle.baseColorOutgoing
|
||||
|
||||
switch status {
|
||||
case .success:
|
||||
case .Success:
|
||||
break
|
||||
case .failed, .sending:
|
||||
color = color.bma_blendWithColor(UIColor.white.withAlphaComponent(0.70))
|
||||
case .Failed, .Sending:
|
||||
color = color.bma_blendWithColor(UIColor.whiteColor().colorWithAlphaComponent(0.70))
|
||||
}
|
||||
|
||||
if isSelected {
|
||||
color = color.bma_blendWithColor(UIColor.black.withAlphaComponent(0.10))
|
||||
color = color.bma_blendWithColor(UIColor.blackColor().colorWithAlphaComponent(0.10))
|
||||
}
|
||||
|
||||
return image.bma_tintWithColor(color)
|
||||
|
|
@ -154,19 +155,19 @@ open class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionViewC
|
|||
}
|
||||
|
||||
private func calculateHash(withHashValues hashes: [Int]) -> Int {
|
||||
return hashes.reduce(0, { 31 &* $0 &+ $1.hashValue })
|
||||
return hashes.reduce(0, combine: { 31 &* $0 &+ $1.hashValue })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func == (lhs: TextMessageCollectionViewCellDefaultStyle.ImageKey, rhs: TextMessageCollectionViewCellDefaultStyle.ImageKey) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.template(lhsValues), .template(rhsValues)):
|
||||
return lhsValues == rhsValues
|
||||
case let (.normal(lhsValues), .normal(rhsValues)):
|
||||
return lhsValues == rhsValues
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
private func == (lhs: TextMessageCollectionViewCellDefaultStyle.ImageKey, rhs: TextMessageCollectionViewCellDefaultStyle.ImageKey) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.template(lhsValues), .template(rhsValues)):
|
||||
return lhsValues == rhsValues
|
||||
case let (.normal(lhsValues), .normal(rhsValues)):
|
||||
return lhsValues == rhsValues
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -174,18 +175,18 @@ public extension TextMessageCollectionViewCellDefaultStyle { // Default values
|
|||
|
||||
static public func createDefaultBubbleImages() -> BubbleImages {
|
||||
return BubbleImages(
|
||||
incomingTail: UIImage(named: "bubble-incoming-tail", in: Bundle(for: Class.self), compatibleWith: nil)!,
|
||||
incomingNoTail: UIImage(named: "bubble-incoming", in: Bundle(for: Class.self), compatibleWith: nil)!,
|
||||
outgoingTail: UIImage(named: "bubble-outgoing-tail", in: Bundle(for: Class.self), compatibleWith: nil)!,
|
||||
outgoingNoTail: UIImage(named: "bubble-outgoing", in: Bundle(for: Class.self), compatibleWith: nil)!
|
||||
incomingTail: UIImage(named: "bubble-incoming-tail", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!,
|
||||
incomingNoTail: UIImage(named: "bubble-incoming", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!,
|
||||
outgoingTail: UIImage(named: "bubble-outgoing-tail", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!,
|
||||
outgoingNoTail: UIImage(named: "bubble-outgoing", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!
|
||||
)
|
||||
}
|
||||
|
||||
static public func createDefaultTextStyle() -> TextStyle {
|
||||
return TextStyle(
|
||||
font: UIFont.systemFont(ofSize: 16),
|
||||
incomingColor: UIColor.black,
|
||||
outgoingColor: UIColor.white,
|
||||
font: UIFont.systemFontOfSize(16),
|
||||
incomingColor: UIColor.blackColor(),
|
||||
outgoingColor: UIColor.whiteColor(),
|
||||
incomingInsets: UIEdgeInsets(top: 10, left: 19, bottom: 10, right: 15),
|
||||
outgoingInsets: UIEdgeInsets(top: 10, left: 15, bottom: 10, right: 19)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>3.0.1</string>
|
||||
<string>2.1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
|
|
|||
|
|
@ -25,17 +25,17 @@
|
|||
import UIKit
|
||||
|
||||
public protocol ChatInputBarDelegate: class {
|
||||
func inputBarShouldBeginTextEditing(_ inputBar: ChatInputBar) -> Bool
|
||||
func inputBarDidBeginEditing(_ inputBar: ChatInputBar)
|
||||
func inputBarDidEndEditing(_ inputBar: ChatInputBar)
|
||||
func inputBarDidChangeText(_ inputBar: ChatInputBar)
|
||||
func inputBarSendButtonPressed(_ inputBar: ChatInputBar)
|
||||
func inputBar(_ inputBar: ChatInputBar, shouldFocusOnItem item: ChatInputItemProtocol) -> Bool
|
||||
func inputBar(_ inputBar: ChatInputBar, didReceiveFocusOnItem item: ChatInputItemProtocol)
|
||||
func inputBarShouldBeginTextEditing(inputBar: ChatInputBar) -> Bool
|
||||
func inputBarDidBeginEditing(inputBar: ChatInputBar)
|
||||
func inputBarDidEndEditing(inputBar: ChatInputBar)
|
||||
func inputBarDidChangeText(inputBar: ChatInputBar)
|
||||
func inputBarSendButtonPressed(inputBar: ChatInputBar)
|
||||
func inputBar(inputBar: ChatInputBar, shouldFocusOnItem item: ChatInputItemProtocol) -> Bool
|
||||
func inputBar(inputBar: ChatInputBar, didReceiveFocusOnItem item: ChatInputItemProtocol)
|
||||
}
|
||||
|
||||
@objc
|
||||
open class ChatInputBar: ReusableXibView {
|
||||
public class ChatInputBar: ReusableXibView {
|
||||
|
||||
public weak var delegate: ChatInputBarDelegate?
|
||||
weak var presenter: ChatInputBarPresenter?
|
||||
|
|
@ -56,8 +56,8 @@ open class ChatInputBar: ReusableXibView {
|
|||
@IBOutlet var constraintsForHiddenSendButton: [NSLayoutConstraint]!
|
||||
@IBOutlet var tabBarContainerHeightConstraint: NSLayoutConstraint!
|
||||
|
||||
class open func loadNib() -> ChatInputBar {
|
||||
let view = Bundle(for: self).loadNibNamed(self.nibName(), owner: nil, options: nil)!.first as! ChatInputBar
|
||||
class public func loadNib() -> ChatInputBar {
|
||||
let view = NSBundle(forClass: self).loadNibNamed(self.nibName(), owner: nil, options: nil)!.first as! ChatInputBar
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.frame = CGRect.zero
|
||||
return view
|
||||
|
|
@ -67,34 +67,34 @@ open class ChatInputBar: ReusableXibView {
|
|||
return "ChatInputBar"
|
||||
}
|
||||
|
||||
open override func awakeFromNib() {
|
||||
public override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
self.topBorderHeightConstraint.constant = 1 / UIScreen.main.scale
|
||||
self.topBorderHeightConstraint.constant = 1 / UIScreen.mainScreen().scale
|
||||
self.textView.scrollsToTop = false
|
||||
self.textView.delegate = self
|
||||
self.scrollView.scrollsToTop = false
|
||||
self.sendButton.isEnabled = false
|
||||
self.sendButton.enabled = false
|
||||
}
|
||||
|
||||
open override func updateConstraints() {
|
||||
public override func updateConstraints() {
|
||||
if self.showsTextView {
|
||||
NSLayoutConstraint.activate(self.constraintsForVisibleTextView)
|
||||
NSLayoutConstraint.deactivate(self.constraintsForHiddenTextView)
|
||||
NSLayoutConstraint.activateConstraints(self.constraintsForVisibleTextView)
|
||||
NSLayoutConstraint.deactivateConstraints(self.constraintsForHiddenTextView)
|
||||
} else {
|
||||
NSLayoutConstraint.deactivate(self.constraintsForVisibleTextView)
|
||||
NSLayoutConstraint.activate(self.constraintsForHiddenTextView)
|
||||
NSLayoutConstraint.deactivateConstraints(self.constraintsForVisibleTextView)
|
||||
NSLayoutConstraint.activateConstraints(self.constraintsForHiddenTextView)
|
||||
}
|
||||
if self.showsSendButton {
|
||||
NSLayoutConstraint.deactivate(self.constraintsForHiddenSendButton)
|
||||
NSLayoutConstraint.activate(self.constraintsForVisibleSendButton)
|
||||
NSLayoutConstraint.deactivateConstraints(self.constraintsForHiddenSendButton)
|
||||
NSLayoutConstraint.activateConstraints(self.constraintsForVisibleSendButton)
|
||||
} else {
|
||||
NSLayoutConstraint.deactivate(self.constraintsForVisibleSendButton)
|
||||
NSLayoutConstraint.activate(self.constraintsForHiddenSendButton)
|
||||
NSLayoutConstraint.deactivateConstraints(self.constraintsForVisibleSendButton)
|
||||
NSLayoutConstraint.activateConstraints(self.constraintsForHiddenSendButton)
|
||||
}
|
||||
super.updateConstraints()
|
||||
}
|
||||
|
||||
open var showsTextView: Bool = true {
|
||||
public var showsTextView: Bool = true {
|
||||
didSet {
|
||||
self.setNeedsUpdateConstraints()
|
||||
self.setNeedsLayout()
|
||||
|
|
@ -102,7 +102,7 @@ open class ChatInputBar: ReusableXibView {
|
|||
}
|
||||
}
|
||||
|
||||
open var showsSendButton: Bool = true {
|
||||
public var showsSendButton: Bool = true {
|
||||
didSet {
|
||||
self.setNeedsUpdateConstraints()
|
||||
self.setNeedsLayout()
|
||||
|
|
@ -113,15 +113,15 @@ open class ChatInputBar: ReusableXibView {
|
|||
public var maxCharactersCount: UInt? // nil -> unlimited
|
||||
|
||||
private func updateIntrinsicContentSizeAnimated() {
|
||||
let options: UIViewAnimationOptions = [.beginFromCurrentState, .allowUserInteraction]
|
||||
UIView.animate(withDuration: 0.25, delay: 0, options: options, animations: { () -> Void in
|
||||
let options: UIViewAnimationOptions = [.BeginFromCurrentState, .AllowUserInteraction, .CurveEaseInOut]
|
||||
UIView.animateWithDuration(0.25, delay: 0, options: options, animations: { () -> Void in
|
||||
self.invalidateIntrinsicContentSize()
|
||||
self.layoutIfNeeded()
|
||||
self.superview?.layoutIfNeeded()
|
||||
}, completion: nil)
|
||||
}
|
||||
|
||||
open override func layoutSubviews() {
|
||||
public override func layoutSubviews() {
|
||||
self.updateConstraints() // Interface rotation or size class changes will reset constraints as defined in interface builder -> constraintsForVisibleTextView will be activated
|
||||
super.layoutSubviews()
|
||||
}
|
||||
|
|
@ -138,10 +138,10 @@ open class ChatInputBar: ReusableXibView {
|
|||
}
|
||||
}
|
||||
|
||||
open func becomeFirstResponderWithInputView(_ inputView: UIView?) {
|
||||
public func becomeFirstResponderWithInputView(inputView: UIView?) {
|
||||
self.textView.inputView = inputView
|
||||
|
||||
if self.textView.isFirstResponder {
|
||||
if self.textView.isFirstResponder() {
|
||||
self.textView.reloadInputViews()
|
||||
} else {
|
||||
self.textView.becomeFirstResponder()
|
||||
|
|
@ -158,27 +158,27 @@ open class ChatInputBar: ReusableXibView {
|
|||
}
|
||||
}
|
||||
|
||||
fileprivate func updateSendButton() {
|
||||
self.sendButton.isEnabled = self.shouldEnableSendButton(self)
|
||||
private func updateSendButton() {
|
||||
self.sendButton.enabled = self.shouldEnableSendButton(self)
|
||||
}
|
||||
|
||||
@IBAction func buttonTapped(_ sender: AnyObject) {
|
||||
@IBAction func buttonTapped(sender: AnyObject) {
|
||||
self.presenter?.onSendButtonPressed()
|
||||
self.delegate?.inputBarSendButtonPressed(self)
|
||||
}
|
||||
|
||||
public func setTextViewPlaceholderAccessibilityIdentifer(_ accessibilityIdentifer: String) {
|
||||
public func setTextViewPlaceholderAccessibilityIdentifer(accessibilityIdentifer: String) {
|
||||
self.textView.setTextPlaceholderAccessibilityIdentifier(accessibilityIdentifer)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ChatInputItemViewDelegate
|
||||
extension ChatInputBar: ChatInputItemViewDelegate {
|
||||
func inputItemViewTapped(_ view: ChatInputItemView) {
|
||||
func inputItemViewTapped(view: ChatInputItemView) {
|
||||
self.focusOnInputItem(view.inputItem)
|
||||
}
|
||||
|
||||
public func focusOnInputItem(_ inputItem: ChatInputItemProtocol) {
|
||||
public func focusOnInputItem(inputItem: ChatInputItemProtocol) {
|
||||
let shouldFocus = self.delegate?.inputBar(self, shouldFocusOnItem: inputItem) ?? true
|
||||
guard shouldFocus else { return }
|
||||
|
||||
|
|
@ -189,7 +189,7 @@ extension ChatInputBar: ChatInputItemViewDelegate {
|
|||
|
||||
// MARK: - ChatInputBarAppearance
|
||||
extension ChatInputBar {
|
||||
public func setAppearance(_ appearance: ChatInputBarAppearance) {
|
||||
public func setAppearance(appearance: ChatInputBarAppearance) {
|
||||
self.textView.font = appearance.textInputAppearance.font
|
||||
self.textView.textColor = appearance.textInputAppearance.textColor
|
||||
self.textView.textContainerInset = appearance.textInputAppearance.textInsets
|
||||
|
|
@ -199,9 +199,9 @@ extension ChatInputBar {
|
|||
self.tabBarInterItemSpacing = appearance.tabBarAppearance.interItemSpacing
|
||||
self.tabBarContentInsets = appearance.tabBarAppearance.contentInsets
|
||||
self.sendButton.contentEdgeInsets = appearance.sendButtonAppearance.insets
|
||||
self.sendButton.setTitle(appearance.sendButtonAppearance.title, for: .normal)
|
||||
self.sendButton.setTitle(appearance.sendButtonAppearance.title, forState: .Normal)
|
||||
appearance.sendButtonAppearance.titleColors.forEach { (state, color) in
|
||||
self.sendButton.setTitleColor(color, for: state.controlState)
|
||||
self.sendButton.setTitleColor(color, forState: state.controlState)
|
||||
}
|
||||
self.sendButton.titleLabel?.font = appearance.sendButtonAppearance.font
|
||||
self.tabBarContainerHeightConstraint.constant = appearance.tabBarAppearance.height
|
||||
|
|
@ -230,30 +230,30 @@ extension ChatInputBar { // Tabar
|
|||
|
||||
// MARK: UITextViewDelegate
|
||||
extension ChatInputBar: UITextViewDelegate {
|
||||
public func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
|
||||
public func textViewShouldBeginEditing(textView: UITextView) -> Bool {
|
||||
return self.delegate?.inputBarShouldBeginTextEditing(self) ?? true
|
||||
}
|
||||
|
||||
public func textViewDidEndEditing(_ textView: UITextView) {
|
||||
public func textViewDidEndEditing(textView: UITextView) {
|
||||
self.presenter?.onDidEndEditing()
|
||||
self.delegate?.inputBarDidEndEditing(self)
|
||||
}
|
||||
|
||||
public func textViewDidBeginEditing(_ textView: UITextView) {
|
||||
public func textViewDidBeginEditing(textView: UITextView) {
|
||||
self.presenter?.onDidBeginEditing()
|
||||
self.delegate?.inputBarDidBeginEditing(self)
|
||||
}
|
||||
|
||||
public func textViewDidChange(_ textView: UITextView) {
|
||||
public func textViewDidChange(textView: UITextView) {
|
||||
self.updateSendButton()
|
||||
self.delegate?.inputBarDidChangeText(self)
|
||||
}
|
||||
|
||||
public func textView(_ textView: UITextView, shouldChangeTextIn nsRange: NSRange, replacementText text: String) -> Bool {
|
||||
public func textView(textView: UITextView, shouldChangeTextInRange nsRange: NSRange, replacementText text: String) -> Bool {
|
||||
let range = self.textView.text.bma_rangeFromNSRange(nsRange)
|
||||
if let maxCharactersCount = self.maxCharactersCount {
|
||||
let currentCount = textView.text.characters.count
|
||||
let rangeLength = textView.text.substring(with: range).characters.count
|
||||
let rangeLength = textView.text.substringWithRange(range).characters.count
|
||||
let nextCount = currentCount - rangeLength + text.characters.count
|
||||
return UInt(nextCount) <= maxCharactersCount
|
||||
}
|
||||
|
|
@ -262,13 +262,12 @@ extension ChatInputBar: UITextViewDelegate {
|
|||
}
|
||||
|
||||
private extension String {
|
||||
func bma_rangeFromNSRange(_ nsRange: NSRange) -> Range<String.Index> {
|
||||
guard
|
||||
let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex),
|
||||
let to16 = utf16.index(from16, offsetBy: nsRange.length, limitedBy: utf16.endIndex),
|
||||
let from = String.Index(from16, within: self),
|
||||
let to = String.Index(to16, within: self)
|
||||
else { return self.startIndex..<self.startIndex }
|
||||
return from ..< to
|
||||
func bma_rangeFromNSRange(nsRange: NSRange) -> Range<String.Index> {
|
||||
let from16 = self.utf16.startIndex.advancedBy(nsRange.location, limit: self.utf16.endIndex)
|
||||
let to16 = from16.advancedBy(nsRange.length, limit: self.utf16.endIndex)
|
||||
if let from = String.Index(from16, within: self), to = String.Index(to16, within: self) {
|
||||
return from ..< to
|
||||
}
|
||||
return self.startIndex...self.startIndex
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,13 +24,13 @@
|
|||
|
||||
public struct ChatInputBarAppearance {
|
||||
public struct SendButtonAppearance {
|
||||
public var font = UIFont.systemFont(ofSize: 16)
|
||||
public var font = UIFont.systemFontOfSize(16)
|
||||
public var insets = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
|
||||
public var title = ""
|
||||
public var titleColors: [UIControlStateWrapper: UIColor] = [
|
||||
UIControlStateWrapper(state: .disabled): UIColor.bma_color(rgb: 0x9AA3AB),
|
||||
UIControlStateWrapper(state: .normal): UIColor.bma_color(rgb: 0x007AFF),
|
||||
UIControlStateWrapper(state: .highlighted): UIColor.bma_color(rgb: 0x007AFF).bma_blendWithColor(UIColor.white.withAlphaComponent(0.4))
|
||||
UIControlStateWrapper(state: .Disabled): UIColor.bma_color(rgb: 0x9AA3AB),
|
||||
UIControlStateWrapper(state: .Normal): UIColor.bma_color(rgb: 0x007AFF),
|
||||
UIControlStateWrapper(state: .Highlighted): UIColor.bma_color(rgb: 0x007AFF).bma_blendWithColor(UIColor.whiteColor().colorWithAlphaComponent(0.4))
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -41,10 +41,10 @@ public struct ChatInputBarAppearance {
|
|||
}
|
||||
|
||||
public struct TextInputAppearance {
|
||||
public var font = UIFont.systemFont(ofSize: 12)
|
||||
public var textColor = UIColor.black
|
||||
public var placeholderFont = UIFont.systemFont(ofSize: 12)
|
||||
public var placeholderColor = UIColor.gray
|
||||
public var font = UIFont.systemFontOfSize(12)
|
||||
public var textColor = UIColor.blackColor()
|
||||
public var placeholderFont = UIFont.systemFontOfSize(12)
|
||||
public var placeholderColor = UIColor.grayColor()
|
||||
public var placeholderText = ""
|
||||
public var textInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
|
||||
}
|
||||
|
|
@ -56,6 +56,7 @@ public struct ChatInputBarAppearance {
|
|||
public init() {}
|
||||
}
|
||||
|
||||
|
||||
// Workaround for SR-2223
|
||||
public struct UIControlStateWrapper: Hashable {
|
||||
|
||||
|
|
|
|||
|
|
@ -29,19 +29,18 @@ protocol ChatInputBarPresenter: class {
|
|||
func onDidBeginEditing()
|
||||
func onDidEndEditing()
|
||||
func onSendButtonPressed()
|
||||
func onDidReceiveFocusOnItem(_ item: ChatInputItemProtocol)
|
||||
func onDidReceiveFocusOnItem(item: ChatInputItemProtocol)
|
||||
}
|
||||
|
||||
@objc
|
||||
public class BasicChatInputBarPresenter: NSObject, ChatInputBarPresenter {
|
||||
@objc public class BasicChatInputBarPresenter: NSObject, ChatInputBarPresenter {
|
||||
let chatInputBar: ChatInputBar
|
||||
let chatInputItems: [ChatInputItemProtocol]
|
||||
let notificationCenter: NotificationCenter
|
||||
let notificationCenter: NSNotificationCenter
|
||||
|
||||
public init(chatInputBar: ChatInputBar,
|
||||
chatInputItems: [ChatInputItemProtocol],
|
||||
chatInputBarAppearance: ChatInputBarAppearance,
|
||||
notificationCenter: NotificationCenter = NotificationCenter.default) {
|
||||
notificationCenter: NSNotificationCenter = NSNotificationCenter.defaultCenter()) {
|
||||
self.chatInputBar = chatInputBar
|
||||
self.chatInputItems = chatInputItems
|
||||
self.chatInputBar.setAppearance(chatInputBarAppearance)
|
||||
|
|
@ -50,16 +49,16 @@ public class BasicChatInputBarPresenter: NSObject, ChatInputBarPresenter {
|
|||
|
||||
self.chatInputBar.presenter = self
|
||||
self.chatInputBar.inputItems = self.chatInputItems
|
||||
self.notificationCenter.addObserver(self, selector: #selector(BasicChatInputBarPresenter.keyboardDidChangeFrame), name: NSNotification.Name.UIKeyboardDidChangeFrame, object: nil)
|
||||
self.notificationCenter.addObserver(self, selector: #selector(BasicChatInputBarPresenter.keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
|
||||
self.notificationCenter.addObserver(self, selector: #selector(BasicChatInputBarPresenter.keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
|
||||
self.notificationCenter.addObserver(self, selector: #selector(BasicChatInputBarPresenter.keyboardDidChangeFrame), name: UIKeyboardDidChangeFrameNotification, object: nil)
|
||||
self.notificationCenter.addObserver(self, selector: #selector(BasicChatInputBarPresenter.keyboardWillHide), name: UIKeyboardWillHideNotification, object: nil)
|
||||
self.notificationCenter.addObserver(self, selector: #selector(BasicChatInputBarPresenter.keyboardWillShow), name: UIKeyboardWillShowNotification, object: nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.notificationCenter.removeObserver(self)
|
||||
}
|
||||
|
||||
fileprivate(set) var focusedItem: ChatInputItemProtocol? {
|
||||
private(set) var focusedItem: ChatInputItemProtocol? {
|
||||
willSet {
|
||||
self.focusedItem?.selected = false
|
||||
}
|
||||
|
|
@ -68,11 +67,11 @@ public class BasicChatInputBarPresenter: NSObject, ChatInputBarPresenter {
|
|||
}
|
||||
}
|
||||
|
||||
fileprivate func updateFirstResponderWithInputItem(_ inputItem: ChatInputItemProtocol) {
|
||||
let responder = self.chatInputBar.textView!
|
||||
private func updateFirstResponderWithInputItem(inputItem: ChatInputItemProtocol) {
|
||||
let responder = self.chatInputBar.textView
|
||||
let inputView = inputItem.inputView
|
||||
responder.inputView = inputView
|
||||
if responder.isFirstResponder {
|
||||
if responder.isFirstResponder() {
|
||||
self.setHeight(forInputView: inputView)
|
||||
responder.reloadInputViews()
|
||||
} else {
|
||||
|
|
@ -80,10 +79,10 @@ public class BasicChatInputBarPresenter: NSObject, ChatInputBarPresenter {
|
|||
}
|
||||
}
|
||||
|
||||
fileprivate func firstKeyboardInputItem() -> ChatInputItemProtocol? {
|
||||
private func firstKeyboardInputItem() -> ChatInputItemProtocol? {
|
||||
var firstKeyboardInputItem: ChatInputItemProtocol? = nil
|
||||
for inputItem in self.chatInputItems {
|
||||
if inputItem.presentationMode == .keyboard {
|
||||
if inputItem.presentationMode == .Keyboard {
|
||||
firstKeyboardInputItem = inputItem
|
||||
break
|
||||
}
|
||||
|
|
@ -98,13 +97,13 @@ public class BasicChatInputBarPresenter: NSObject, ChatInputBarPresenter {
|
|||
guard let keyboardHeight = self.lastKnownKeyboardHeight else { return }
|
||||
|
||||
var mask = inputView.autoresizingMask
|
||||
mask.remove(.flexibleHeight)
|
||||
mask.remove(.FlexibleHeight)
|
||||
inputView.autoresizingMask = mask
|
||||
|
||||
let accessoryViewHeight = self.chatInputBar.textView.inputAccessoryView?.bounds.height ?? 0
|
||||
let inputViewHeight = keyboardHeight - accessoryViewHeight
|
||||
|
||||
if let heightConstraint = inputView.constraints.filter({ $0.firstAttribute == .height }).first {
|
||||
if let heightConstraint = inputView.constraints.filter({ $0.firstAttribute == .Height }).first {
|
||||
heightConstraint.constant = inputViewHeight
|
||||
} else {
|
||||
inputView.frame.size.height = inputViewHeight
|
||||
|
|
@ -114,19 +113,19 @@ public class BasicChatInputBarPresenter: NSObject, ChatInputBarPresenter {
|
|||
private var allowListenToChangeFrameEvents = true
|
||||
|
||||
@objc
|
||||
private func keyboardDidChangeFrame(_ notification: Notification) {
|
||||
private func keyboardDidChangeFrame(notification: NSNotification) {
|
||||
guard self.allowListenToChangeFrameEvents else { return }
|
||||
guard let value = (notification as NSNotification).userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue else { return }
|
||||
self.lastKnownKeyboardHeight = value.cgRectValue.height
|
||||
guard let value = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue else { return }
|
||||
self.lastKnownKeyboardHeight = value.CGRectValue().height
|
||||
}
|
||||
|
||||
@objc
|
||||
private func keyboardWillHide(_ notification: Notification) {
|
||||
private func keyboardWillHide(notification: NSNotification) {
|
||||
self.allowListenToChangeFrameEvents = false
|
||||
}
|
||||
|
||||
@objc
|
||||
private func keyboardWillShow(_ notification: Notification) {
|
||||
private func keyboardWillShow(notification: NSNotification) {
|
||||
self.allowListenToChangeFrameEvents = true
|
||||
}
|
||||
}
|
||||
|
|
@ -148,20 +147,20 @@ extension BasicChatInputBarPresenter {
|
|||
|
||||
func onSendButtonPressed() {
|
||||
if let focusedItem = self.focusedItem {
|
||||
focusedItem.handleInput(self.chatInputBar.inputText as AnyObject)
|
||||
focusedItem.handleInput(self.chatInputBar.inputText)
|
||||
} else if let keyboardItem = self.firstKeyboardInputItem() {
|
||||
keyboardItem.handleInput(self.chatInputBar.inputText as AnyObject)
|
||||
keyboardItem.handleInput(self.chatInputBar.inputText)
|
||||
}
|
||||
self.chatInputBar.inputText = ""
|
||||
}
|
||||
|
||||
func onDidReceiveFocusOnItem(_ item: ChatInputItemProtocol) {
|
||||
guard item.presentationMode != .none else { return }
|
||||
func onDidReceiveFocusOnItem(item: ChatInputItemProtocol) {
|
||||
guard item.presentationMode != .None else { return }
|
||||
guard item !== self.focusedItem else { return }
|
||||
|
||||
self.focusedItem = item
|
||||
self.chatInputBar.showsSendButton = item.showsSendButton
|
||||
self.chatInputBar.showsTextView = item.presentationMode == .keyboard
|
||||
self.chatInputBar.showsTextView = item.presentationMode == .Keyboard
|
||||
self.updateFirstResponderWithInputItem(item)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@
|
|||
import Foundation
|
||||
|
||||
public enum ChatInputItemPresentationMode: UInt {
|
||||
case keyboard
|
||||
case customView
|
||||
case none
|
||||
case Keyboard
|
||||
case CustomView
|
||||
case None
|
||||
}
|
||||
|
||||
public protocol ChatInputItemProtocol: AnyObject {
|
||||
|
|
@ -37,5 +37,5 @@ public protocol ChatInputItemProtocol: AnyObject {
|
|||
var showsSendButton: Bool { get }
|
||||
var selected: Bool { get set }
|
||||
|
||||
func handleInput(_ input: AnyObject)
|
||||
func handleInput(input: AnyObject)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
import Foundation
|
||||
|
||||
protocol ChatInputItemViewDelegate: class {
|
||||
func inputItemViewTapped(_ view: ChatInputItemView)
|
||||
func inputItemViewTapped(view: ChatInputItemView)
|
||||
}
|
||||
|
||||
class ChatInputItemView: UIView {
|
||||
|
|
@ -72,7 +72,7 @@ extension ChatInputItemView {
|
|||
self.inputItem.tabView.frame = self.bounds
|
||||
}
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
return self.inputItem.tabView.intrinsicContentSize
|
||||
override func intrinsicContentSize() -> CGSize {
|
||||
return self.inputItem.tabView.intrinsicContentSize()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
open class ExpandableTextView: UITextView {
|
||||
public class ExpandableTextView: UITextView {
|
||||
|
||||
private let placeholder: UITextView = UITextView()
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ open class ExpandableTextView: UITextView {
|
|||
self.commonInit()
|
||||
}
|
||||
|
||||
override open var contentSize: CGSize {
|
||||
override public var contentSize: CGSize {
|
||||
didSet {
|
||||
self.invalidateIntrinsicContentSize()
|
||||
self.layoutIfNeeded() // needed?
|
||||
|
|
@ -46,55 +46,56 @@ open class ExpandableTextView: UITextView {
|
|||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
NSNotificationCenter.defaultCenter().removeObserver(self)
|
||||
}
|
||||
|
||||
private func commonInit() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(ExpandableTextView.textDidChange), name: NSNotification.Name.UITextViewTextDidChange, object: self)
|
||||
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ExpandableTextView.textDidChange), name: UITextViewTextDidChangeNotification, object: self)
|
||||
self.configurePlaceholder()
|
||||
self.updatePlaceholderVisibility()
|
||||
}
|
||||
|
||||
override open func layoutSubviews() {
|
||||
|
||||
override public func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
self.placeholder.frame = self.bounds
|
||||
}
|
||||
|
||||
override open var intrinsicContentSize: CGSize {
|
||||
override public func intrinsicContentSize() -> CGSize {
|
||||
return self.contentSize
|
||||
}
|
||||
|
||||
override open var text: String! {
|
||||
override public var text: String! {
|
||||
didSet {
|
||||
self.textDidChange()
|
||||
}
|
||||
}
|
||||
|
||||
override open var textContainerInset: UIEdgeInsets {
|
||||
override public var textContainerInset: UIEdgeInsets {
|
||||
didSet {
|
||||
self.configurePlaceholder()
|
||||
}
|
||||
}
|
||||
|
||||
override open var textAlignment: NSTextAlignment {
|
||||
override public var textAlignment: NSTextAlignment {
|
||||
didSet {
|
||||
self.configurePlaceholder()
|
||||
}
|
||||
}
|
||||
|
||||
open func setTextPlaceholder(_ textPlaceholder: String) {
|
||||
public func setTextPlaceholder(textPlaceholder: String) {
|
||||
self.placeholder.text = textPlaceholder
|
||||
}
|
||||
|
||||
open func setTextPlaceholderColor(_ color: UIColor) {
|
||||
public func setTextPlaceholderColor(color: UIColor) {
|
||||
self.placeholder.textColor = color
|
||||
}
|
||||
|
||||
open func setTextPlaceholderFont(_ font: UIFont) {
|
||||
public func setTextPlaceholderFont(font: UIFont) {
|
||||
self.placeholder.font = font
|
||||
}
|
||||
|
||||
open func setTextPlaceholderAccessibilityIdentifier(_ accessibilityIdentifier: String) {
|
||||
public func setTextPlaceholderAccessibilityIdentifier(accessibilityIdentifier: String) {
|
||||
self.placeholder.accessibilityIdentifier = accessibilityIdentifier
|
||||
}
|
||||
|
||||
|
|
@ -108,14 +109,14 @@ open class ExpandableTextView: UITextView {
|
|||
// 2. Paste very long text (so it snaps to nav bar and shows scroll indicators)
|
||||
// 3. Select all and cut
|
||||
// 4. Paste again: Texview it's smaller than it should be
|
||||
self.isScrollEnabled = false
|
||||
self.isScrollEnabled = true
|
||||
self.scrollEnabled = false
|
||||
self.scrollEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
private func scrollToCaret() {
|
||||
if let textRange = self.selectedTextRange {
|
||||
var rect = caretRect(for: textRange.end)
|
||||
var rect = caretRectForPosition(textRange.end)
|
||||
rect = CGRect(origin: rect.origin, size: CGSize(width: rect.width, height: rect.height + textContainerInset.bottom))
|
||||
|
||||
self.scrollRectToVisible(rect, animated: false)
|
||||
|
|
@ -140,11 +141,11 @@ open class ExpandableTextView: UITextView {
|
|||
|
||||
private func configurePlaceholder() {
|
||||
self.placeholder.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.placeholder.isEditable = false
|
||||
self.placeholder.isSelectable = false
|
||||
self.placeholder.isUserInteractionEnabled = false
|
||||
self.placeholder.editable = false
|
||||
self.placeholder.selectable = false
|
||||
self.placeholder.userInteractionEnabled = false
|
||||
self.placeholder.textAlignment = self.textAlignment
|
||||
self.placeholder.textContainerInset = self.textContainerInset
|
||||
self.placeholder.backgroundColor = UIColor.clear
|
||||
self.placeholder.backgroundColor = UIColor.clearColor()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
open class HorizontalStackScrollView: UIScrollView {
|
||||
public class HorizontalStackScrollView: UIScrollView {
|
||||
|
||||
private var arrangedViews: [UIView] = []
|
||||
private var arrangedViewContraints: [NSLayoutConstraint] = []
|
||||
|
|
@ -34,16 +34,16 @@ open class HorizontalStackScrollView: UIScrollView {
|
|||
}
|
||||
}
|
||||
|
||||
func addArrangedViews(_ views: [UIView]) {
|
||||
func addArrangedViews(views: [UIView]) {
|
||||
for view in views {
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.addSubview(view)
|
||||
}
|
||||
self.arrangedViews.append(contentsOf: views)
|
||||
self.arrangedViews.appendContentsOf(views)
|
||||
self.setNeedsUpdateConstraints()
|
||||
}
|
||||
|
||||
override open func updateConstraints() {
|
||||
override public func updateConstraints() {
|
||||
super.updateConstraints()
|
||||
self.removeConstraintsForArrangedViews()
|
||||
self.addConstraintsForArrengedViews()
|
||||
|
|
@ -57,23 +57,23 @@ open class HorizontalStackScrollView: UIScrollView {
|
|||
}
|
||||
|
||||
private func addConstraintsForArrengedViews() {
|
||||
for (index, view) in arrangedViews.enumerated() {
|
||||
for (index, view) in arrangedViews.enumerate() {
|
||||
switch index {
|
||||
case 0:
|
||||
let constraint = NSLayoutConstraint(item: view, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1, constant: 0)
|
||||
let constraint = NSLayoutConstraint(item: view, attribute: .Leading, relatedBy: .Equal, toItem: self, attribute: .Leading, multiplier: 1, constant: 0)
|
||||
self.addConstraint(constraint)
|
||||
self.arrangedViewContraints.append(constraint)
|
||||
case arrangedViews.count-1:
|
||||
let constraint = NSLayoutConstraint(item: view, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1, constant: 0)
|
||||
let constraint = NSLayoutConstraint(item: view, attribute: .Trailing, relatedBy: .Equal, toItem: self, attribute: .Trailing, multiplier: 1, constant: 0)
|
||||
self.addConstraint(constraint)
|
||||
self.arrangedViewContraints.append(constraint)
|
||||
fallthrough
|
||||
default:
|
||||
let constraint = NSLayoutConstraint(item: view, attribute: .leading, relatedBy: .equal, toItem: arrangedViews[index-1], attribute: .trailing, multiplier: 1, constant: self.interItemSpacing)
|
||||
let constraint = NSLayoutConstraint(item: view, attribute: .Leading, relatedBy: .Equal, toItem: arrangedViews[index-1], attribute: .Trailing, multiplier: 1, constant: self.interItemSpacing)
|
||||
self.addConstraint(constraint)
|
||||
self.arrangedViewContraints.append(constraint)
|
||||
}
|
||||
self.addConstraint(NSLayoutConstraint(item: view, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0))
|
||||
self.addConstraint(NSLayoutConstraint(item: view, attribute: .CenterY, relatedBy: .Equal, toItem: self, attribute: .CenterY, multiplier: 1, constant: 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ protocol LiveCameraCaptureSessionProtocol {
|
|||
var captureLayer: AVCaptureVideoPreviewLayer? { get }
|
||||
var isInitialized: Bool { get }
|
||||
var isCapturing: Bool { get }
|
||||
func startCapturing(_ completion: @escaping () -> Void)
|
||||
func stopCapturing(_ completion: @escaping () -> Void)
|
||||
func startCapturing(completion: () -> Void)
|
||||
func stopCapturing(completion: () -> Void)
|
||||
}
|
||||
|
||||
class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
|
||||
|
|
@ -38,39 +38,39 @@ class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
|
|||
var isInitialized: Bool = false
|
||||
|
||||
var isCapturing: Bool {
|
||||
return self.isInitialized && self.captureSession?.isRunning ?? false
|
||||
return self.isInitialized && self.captureSession?.running ?? false
|
||||
}
|
||||
|
||||
deinit {
|
||||
var layer = self.captureLayer
|
||||
layer?.removeFromSuperlayer()
|
||||
var session: AVCaptureSession? = self.isInitialized ? self.captureSession : nil
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
|
||||
// Analogously to AVCaptureSession creation, dealloc can take very long, so let's do it out of the main thread
|
||||
if layer != nil { layer = nil }
|
||||
if session != nil { session = nil }
|
||||
}
|
||||
}
|
||||
|
||||
func startCapturing(_ completion: @escaping () -> Void) {
|
||||
let operation = BlockOperation()
|
||||
func startCapturing(completion: () -> Void) {
|
||||
let operation = NSBlockOperation()
|
||||
operation.addExecutionBlock { [weak operation, weak self] in
|
||||
guard let sSelf = self, let strongOperation = operation, !strongOperation.isCancelled else { return }
|
||||
guard let sSelf = self, strongOperation = operation where !strongOperation.cancelled else { return }
|
||||
sSelf.addInputDevicesIfNeeded()
|
||||
sSelf.captureSession?.startRunning()
|
||||
DispatchQueue.main.async(execute: completion)
|
||||
dispatch_async(dispatch_get_main_queue(), completion)
|
||||
}
|
||||
self.queue.cancelAllOperations()
|
||||
self.queue.addOperation(operation)
|
||||
}
|
||||
|
||||
func stopCapturing(_ completion: @escaping () -> Void) {
|
||||
let operation = BlockOperation()
|
||||
func stopCapturing(completion: () -> Void) {
|
||||
let operation = NSBlockOperation()
|
||||
operation.addExecutionBlock { [weak operation, weak self] in
|
||||
guard let sSelf = self, let strongOperation = operation, !strongOperation.isCancelled else { return }
|
||||
guard let sSelf = self, strongOperation = operation where !strongOperation.cancelled else { return }
|
||||
sSelf.captureSession?.stopRunning()
|
||||
sSelf.removeInputDevices()
|
||||
DispatchQueue.main.async(execute: completion)
|
||||
dispatch_async(dispatch_get_main_queue(), completion)
|
||||
}
|
||||
self.queue.cancelAllOperations()
|
||||
self.queue.addOperation(operation)
|
||||
|
|
@ -78,17 +78,17 @@ class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
|
|||
|
||||
private (set) var captureLayer: AVCaptureVideoPreviewLayer?
|
||||
|
||||
private lazy var queue: OperationQueue = {
|
||||
let queue = OperationQueue()
|
||||
queue.qualityOfService = .userInitiated
|
||||
private lazy var queue: NSOperationQueue = {
|
||||
let queue = NSOperationQueue()
|
||||
queue.qualityOfService = .UserInitiated
|
||||
queue.maxConcurrentOperationCount = 1
|
||||
return queue
|
||||
}()
|
||||
|
||||
private lazy var captureSession: AVCaptureSession? = {
|
||||
assert(!Thread.isMainThread, "This can be very slow, make sure it happens in a background thread")
|
||||
self.isInitialized = true
|
||||
assert(!NSThread.isMainThread(), "This can be very slow, make sure it happens in a background thread")
|
||||
|
||||
self.isInitialized = true
|
||||
#if !(arch(i386) || arch(x86_64))
|
||||
let session = AVCaptureSession()
|
||||
self.captureLayer = AVCaptureVideoPreviewLayer(session: session)
|
||||
|
|
@ -100,9 +100,9 @@ class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
|
|||
}()
|
||||
|
||||
private func addInputDevicesIfNeeded() {
|
||||
assert(!Thread.isMainThread, "This can be very slow, make sure it happens in a background thread")
|
||||
assert(!NSThread.isMainThread(), "This can be very slow, make sure it happens in a background thread")
|
||||
if self.captureSession?.inputs?.count == 0 {
|
||||
let device = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)
|
||||
let device = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
|
||||
do {
|
||||
let input = try AVCaptureDeviceInput(device: device)
|
||||
self.captureSession?.addInput(input)
|
||||
|
|
@ -113,7 +113,7 @@ class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
|
|||
}
|
||||
|
||||
private func removeInputDevices() {
|
||||
assert(!Thread.isMainThread, "This can be very slow, make sure it happens in a background thread")
|
||||
assert(!NSThread.isMainThread(), "This can be very slow, make sure it happens in a background thread")
|
||||
self.captureSession?.inputs?.forEach { (input) in
|
||||
self.captureSession?.removeInput(input as! AVCaptureInput)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ public struct LiveCameraCellAppearance {
|
|||
public var cameraLockImageProvider: () -> UIImage?
|
||||
|
||||
public init(backgroundColor: UIColor,
|
||||
cameraImage: @autoclosure @escaping () -> UIImage?,
|
||||
cameraLockImage: @autoclosure @escaping () -> UIImage?) {
|
||||
@autoclosure(escaping) cameraImage: () -> UIImage?,
|
||||
@autoclosure(escaping) cameraLockImage: () -> UIImage?) {
|
||||
self.backgroundColor = backgroundColor
|
||||
self.cameraImageProvider = cameraImage
|
||||
self.cameraLockImageProvider = cameraLockImage
|
||||
|
|
@ -42,8 +42,8 @@ public struct LiveCameraCellAppearance {
|
|||
public static func createDefaultAppearance() -> LiveCameraCellAppearance {
|
||||
return LiveCameraCellAppearance(
|
||||
backgroundColor: UIColor(red: 24.0/255.0, green: 101.0/255.0, blue: 245.0/255.0, alpha: 1),
|
||||
cameraImage: UIImage(named: "camera", in: Bundle(for: LiveCameraCell.self), compatibleWith: nil),
|
||||
cameraLockImage: UIImage(named: "camera_lock", in: Bundle(for: LiveCameraCell.self), compatibleWith: nil)
|
||||
cameraImage: UIImage(named: "camera", inBundle: NSBundle(forClass: LiveCameraCell.self), compatibleWithTraitCollection: nil),
|
||||
cameraLockImage: UIImage(named: "camera_lock", inBundle: NSBundle(forClass: LiveCameraCell.self), compatibleWithTraitCollection: nil)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -81,44 +81,44 @@ class LiveCameraCell: UICollectionViewCell {
|
|||
self.contentView.layer.insertSublayer(captureLayer, below: self.iconImageView.layer)
|
||||
let animation = CABasicAnimation.bma_fadeInAnimationWithDuration(0.25)
|
||||
let animationKey = "fadeIn"
|
||||
captureLayer.removeAnimation(forKey: animationKey)
|
||||
captureLayer.add(animation, forKey: animationKey)
|
||||
captureLayer.removeAnimationForKey(animationKey)
|
||||
captureLayer.addAnimation(animation, forKey: animationKey)
|
||||
}
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typealias CellCallback = (_ cell: LiveCameraCell) -> Void
|
||||
typealias CellCallback = (cell: LiveCameraCell) -> Void
|
||||
|
||||
var onWasAddedToWindow: CellCallback?
|
||||
var onWasRemovedFromWindow: CellCallback?
|
||||
override func didMoveToWindow() {
|
||||
if let _ = self.window {
|
||||
self.onWasAddedToWindow?(self)
|
||||
self.onWasAddedToWindow?(cell: self)
|
||||
} else {
|
||||
self.onWasRemovedFromWindow?(self)
|
||||
self.onWasRemovedFromWindow?(cell: self)
|
||||
}
|
||||
}
|
||||
|
||||
func updateWithAuthorizationStatus(_ status: AVAuthorizationStatus) {
|
||||
func updateWithAuthorizationStatus(status: AVAuthorizationStatus) {
|
||||
self.authorizationStatus = status
|
||||
self.updateIcon()
|
||||
}
|
||||
|
||||
private var authorizationStatus: AVAuthorizationStatus = .notDetermined
|
||||
private var authorizationStatus: AVAuthorizationStatus = .NotDetermined
|
||||
|
||||
private func configureIcon() {
|
||||
self.iconImageView = UIImageView()
|
||||
self.iconImageView.contentMode = .center
|
||||
self.iconImageView.contentMode = .Center
|
||||
self.contentView.addSubview(self.iconImageView)
|
||||
}
|
||||
|
||||
private func updateIcon() {
|
||||
switch self.authorizationStatus {
|
||||
case .notDetermined, .authorized:
|
||||
case .NotDetermined, .Authorized:
|
||||
self.iconImageView.image = self.appearance.cameraImageProvider()
|
||||
case .restricted, .denied:
|
||||
case .Restricted, .Denied:
|
||||
self.iconImageView.image = self.appearance.cameraLockImageProvider()
|
||||
}
|
||||
self.setNeedsLayout()
|
||||
|
|
|
|||
|
|
@ -29,13 +29,19 @@ public final class LiveCameraCellPresenter {
|
|||
private typealias Class = LiveCameraCellPresenter
|
||||
public typealias AVAuthorizationStatusProvider = () -> AVAuthorizationStatus
|
||||
|
||||
|
||||
private let cellAppearance: LiveCameraCellAppearance
|
||||
private let authorizationStatusProvider: () -> AVAuthorizationStatus
|
||||
public init(cellAppearance: LiveCameraCellAppearance = LiveCameraCellAppearance.createDefaultAppearance(), authorizationStatusProvider: @escaping AVAuthorizationStatusProvider = LiveCameraCellPresenter.createDefaultCameraAuthorizationStatusProvider()) {
|
||||
public init(cellAppearance: LiveCameraCellAppearance, authorizationStatusProvider: AVAuthorizationStatusProvider = LiveCameraCellPresenter.createDefaultCameraAuthorizationStatusProvider()) {
|
||||
self.cellAppearance = cellAppearance
|
||||
self.authorizationStatusProvider = authorizationStatusProvider
|
||||
}
|
||||
|
||||
public convenience init (cellAppearance: LiveCameraCellAppearance?, authorizationStatusProvider: AVAuthorizationStatusProvider = LiveCameraCellPresenter.createDefaultCameraAuthorizationStatusProvider()) {
|
||||
let cellAppearance = cellAppearance ?? LiveCameraCellAppearance.createDefaultAppearance()
|
||||
self.init(cellAppearance: cellAppearance, authorizationStatusProvider: authorizationStatusProvider)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.unsubscribeFromAppNotifications()
|
||||
}
|
||||
|
|
@ -43,21 +49,21 @@ public final class LiveCameraCellPresenter {
|
|||
private static let reuseIdentifier = "LiveCameraCell"
|
||||
private static func createDefaultCameraAuthorizationStatusProvider() -> AVAuthorizationStatusProvider {
|
||||
return {
|
||||
return AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeVideo)
|
||||
return AVCaptureDevice.authorizationStatusForMediaType(AVMediaTypeVideo)
|
||||
}
|
||||
}
|
||||
|
||||
public static func registerCells(collectionView: UICollectionView) {
|
||||
collectionView.register(LiveCameraCell.self, forCellWithReuseIdentifier: Class.reuseIdentifier)
|
||||
public static func registerCells(collectionView collectionView: UICollectionView) {
|
||||
collectionView.registerClass(LiveCameraCell.self, forCellWithReuseIdentifier: Class.reuseIdentifier)
|
||||
}
|
||||
|
||||
public func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell {
|
||||
return collectionView.dequeueReusableCell(withReuseIdentifier: Class.reuseIdentifier, for: indexPath)
|
||||
public func dequeueCell(collectionView collectionView: UICollectionView, indexPath: NSIndexPath) -> UICollectionViewCell {
|
||||
return collectionView.dequeueReusableCellWithReuseIdentifier(Class.reuseIdentifier, forIndexPath: indexPath)
|
||||
}
|
||||
|
||||
private weak var cell: LiveCameraCell?
|
||||
|
||||
public func cellWillBeShown(_ cell: UICollectionViewCell) {
|
||||
public func cellWillBeShown(cell: UICollectionViewCell) {
|
||||
guard let cell = cell as? LiveCameraCell else {
|
||||
assertionFailure("Invalid cell given to presenter")
|
||||
return
|
||||
|
|
@ -67,7 +73,7 @@ public final class LiveCameraCellPresenter {
|
|||
self.startCapturing()
|
||||
}
|
||||
|
||||
public func cellWasHidden(_ cell: UICollectionViewCell) {
|
||||
public func cellWasHidden(cell: UICollectionViewCell) {
|
||||
guard let cell = cell as? LiveCameraCell else {
|
||||
assertionFailure("Invalid cell given to presenter")
|
||||
return
|
||||
|
|
@ -94,14 +100,14 @@ public final class LiveCameraCellPresenter {
|
|||
}
|
||||
|
||||
cameraCell.onWasAddedToWindow = { [weak self] (cell) in
|
||||
guard let sSelf = self, sSelf.cell === cell else { return }
|
||||
guard let sSelf = self where sSelf.cell === cell else { return }
|
||||
if !sSelf.cameraPickerIsVisible {
|
||||
sSelf.startCapturing()
|
||||
}
|
||||
}
|
||||
|
||||
cameraCell.onWasRemovedFromWindow = { [weak self] (cell) in
|
||||
guard let sSelf = self, sSelf.cell === cell else { return }
|
||||
guard let sSelf = self where sSelf.cell === cell else { return }
|
||||
if !sSelf.cameraPickerIsVisible {
|
||||
sSelf.stopCapturing()
|
||||
}
|
||||
|
|
@ -109,11 +115,11 @@ public final class LiveCameraCellPresenter {
|
|||
}
|
||||
|
||||
// MARK: - App Notifications
|
||||
lazy var notificationCenter = NotificationCenter.default
|
||||
lazy var notificationCenter = NSNotificationCenter.defaultCenter()
|
||||
|
||||
private func subscribeToAppNotifications() {
|
||||
self.notificationCenter.addObserver(self, selector: #selector(LiveCameraCellPresenter.handleWillResignActiveNotification), name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
|
||||
self.notificationCenter.addObserver(self, selector: #selector(LiveCameraCellPresenter.handleDidBecomeActiveNotification), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)
|
||||
self.notificationCenter.addObserver(self, selector: #selector(LiveCameraCellPresenter.handleWillResignActiveNotification), name: UIApplicationWillResignActiveNotification, object: nil)
|
||||
self.notificationCenter.addObserver(self, selector: #selector(LiveCameraCellPresenter.handleDidBecomeActiveNotification), name: UIApplicationDidBecomeActiveNotification, object: nil)
|
||||
}
|
||||
|
||||
private func unsubscribeFromAppNotifications() {
|
||||
|
|
@ -124,7 +130,7 @@ public final class LiveCameraCellPresenter {
|
|||
|
||||
@objc
|
||||
private func handleWillResignActiveNotification() {
|
||||
if self.captureSession.isCapturing {
|
||||
if self.captureSession.isCapturing ?? false {
|
||||
self.needsRestoreCaptureSession = true
|
||||
self.stopCapturing()
|
||||
}
|
||||
|
|
@ -167,16 +173,16 @@ public final class LiveCameraCellPresenter {
|
|||
|
||||
private var isCaptureAvailable: Bool {
|
||||
switch self.cameraAuthorizationStatus {
|
||||
case .notDetermined, .restricted, .denied:
|
||||
case .NotDetermined, .Restricted, .Denied:
|
||||
return false
|
||||
case .authorized:
|
||||
case .Authorized:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
lazy var captureSession: LiveCameraCaptureSessionProtocol = LiveCameraCaptureSession()
|
||||
|
||||
private var cameraAuthorizationStatus: AVAuthorizationStatus = .notDetermined {
|
||||
private var cameraAuthorizationStatus: AVAuthorizationStatus = .NotDetermined {
|
||||
didSet {
|
||||
if self.isCaptureAvailable {
|
||||
self.subscribeToAppNotifications()
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
open class PhotosChatInputItem: ChatInputItemProtocol {
|
||||
public class PhotosChatInputItem: ChatInputItemProtocol {
|
||||
typealias Class = PhotosChatInputItem
|
||||
|
||||
public var photoInputHandler: ((UIImage) -> Void)?
|
||||
|
|
@ -42,16 +42,16 @@ open class PhotosChatInputItem: ChatInputItemProtocol {
|
|||
self.inputViewAppearance = inputViewAppearance
|
||||
}
|
||||
|
||||
public static func createDefaultButtonAppearance() -> TabInputButtonAppearance {
|
||||
public class func createDefaultButtonAppearance() -> TabInputButtonAppearance {
|
||||
let images: [UIControlStateWrapper: UIImage] = [
|
||||
UIControlStateWrapper(state: .normal): UIImage(named: "camera-icon-unselected", in: Bundle(for: Class.self), compatibleWith: nil)!,
|
||||
UIControlStateWrapper(state: .selected): UIImage(named: "camera-icon-selected", in: Bundle(for: Class.self), compatibleWith: nil)!,
|
||||
UIControlStateWrapper(state: .highlighted): UIImage(named: "camera-icon-selected", in: Bundle(for: Class.self), compatibleWith: nil)!
|
||||
UIControlStateWrapper(state: .Normal): UIImage(named: "camera-icon-unselected", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!,
|
||||
UIControlStateWrapper(state: .Selected): UIImage(named: "camera-icon-selected", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!,
|
||||
UIControlStateWrapper(state: .Highlighted): UIImage(named: "camera-icon-selected", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!
|
||||
]
|
||||
return TabInputButtonAppearance(images: images, size: nil)
|
||||
}
|
||||
|
||||
public static func createDefaultInputViewAppearance() -> PhotosInputViewAppearance {
|
||||
public class func createDefaultInputViewAppearance() -> PhotosInputViewAppearance {
|
||||
return PhotosInputViewAppearance(liveCameraCellAppearence: LiveCameraCellAppearance.createDefaultAppearance())
|
||||
}
|
||||
|
||||
|
|
@ -65,31 +65,31 @@ open class PhotosChatInputItem: ChatInputItemProtocol {
|
|||
return photosInputView
|
||||
}()
|
||||
|
||||
open var selected = false {
|
||||
public var selected = false {
|
||||
didSet {
|
||||
self.internalTabView.isSelected = self.selected
|
||||
self.internalTabView.selected = self.selected
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ChatInputItemProtocol
|
||||
|
||||
open var presentationMode: ChatInputItemPresentationMode {
|
||||
return .customView
|
||||
public var presentationMode: ChatInputItemPresentationMode {
|
||||
return .CustomView
|
||||
}
|
||||
|
||||
open var showsSendButton: Bool {
|
||||
public var showsSendButton: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
open var inputView: UIView? {
|
||||
public var inputView: UIView? {
|
||||
return self.photosInputView as? UIView
|
||||
}
|
||||
|
||||
open var tabView: UIView {
|
||||
public var tabView: UIView {
|
||||
return self.internalTabView
|
||||
}
|
||||
|
||||
open func handleInput(_ input: AnyObject) {
|
||||
public func handleInput(input: AnyObject) {
|
||||
if let image = input as? UIImage {
|
||||
self.photoInputHandler?(image)
|
||||
}
|
||||
|
|
@ -98,15 +98,15 @@ open class PhotosChatInputItem: ChatInputItemProtocol {
|
|||
|
||||
// MARK: - PhotosInputViewDelegate
|
||||
extension PhotosChatInputItem: PhotosInputViewDelegate {
|
||||
func inputView(_ inputView: PhotosInputViewProtocol, didSelectImage image: UIImage) {
|
||||
func inputView(inputView: PhotosInputViewProtocol, didSelectImage image: UIImage) {
|
||||
self.photoInputHandler?(image)
|
||||
}
|
||||
|
||||
func inputViewDidRequestCameraPermission(_ inputView: PhotosInputViewProtocol) {
|
||||
func inputViewDidRequestCameraPermission(inputView: PhotosInputViewProtocol) {
|
||||
self.cameraPermissionHandler?()
|
||||
}
|
||||
|
||||
func inputViewDidRequestPhotoLibraryPermission(_ inputView: PhotosInputViewProtocol) {
|
||||
func inputViewDidRequestPhotoLibraryPermission(inputView: PhotosInputViewProtocol) {
|
||||
self.photosPermissionHandler?()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ class PhotosInputCameraPicker: NSObject {
|
|||
|
||||
private var completionBlocks: (onImageTaken: ((UIImage?) -> Void)?, onCameraPickerDismissed: (() -> Void)?)?
|
||||
|
||||
func presentCameraPicker(onImageTaken: @escaping (UIImage?) -> Void, onCameraPickerDismissed: @escaping () -> Void) {
|
||||
guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
|
||||
func presentCameraPicker(onImageTaken onImageTaken: (UIImage?) -> Void, onCameraPickerDismissed: () -> Void) {
|
||||
guard UIImagePickerController.isSourceTypeAvailable(.Camera) else {
|
||||
onImageTaken(nil)
|
||||
onCameraPickerDismissed()
|
||||
return
|
||||
|
|
@ -49,23 +49,23 @@ class PhotosInputCameraPicker: NSObject {
|
|||
self.completionBlocks = (onImageTaken: onImageTaken, onCameraPickerDismissed: onCameraPickerDismissed)
|
||||
let controller = UIImagePickerController()
|
||||
controller.delegate = self
|
||||
controller.sourceType = .camera
|
||||
presentingController.present(controller, animated: true, completion:nil)
|
||||
controller.sourceType = .Camera
|
||||
presentingController.presentViewController(controller, animated: true, completion:nil)
|
||||
}
|
||||
|
||||
fileprivate func finishPickingImage(_ image: UIImage?, fromPicker picker: UIImagePickerController) {
|
||||
private func finishPickingImage(image: UIImage?, fromPicker picker: UIImagePickerController) {
|
||||
let (onImageTaken, onCameraPickerDismissed) = self.completionBlocks ?? (nil, nil)
|
||||
picker.dismiss(animated: true, completion: onCameraPickerDismissed)
|
||||
picker.dismissViewControllerAnimated(true, completion: onCameraPickerDismissed)
|
||||
onImageTaken?(image)
|
||||
}
|
||||
}
|
||||
|
||||
extension PhotosInputCameraPicker: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingImage image: UIImage, editingInfo: [String : AnyObject]?) {
|
||||
func imagePickerController(picker: UIImagePickerController, didFinishPickingImage image: UIImage, editingInfo: [String : AnyObject]?) {
|
||||
self.finishPickingImage(image, fromPicker: picker)
|
||||
}
|
||||
|
||||
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
func imagePickerControllerDidCancel(picker: UIImagePickerController) {
|
||||
self.finishPickingImage(nil, fromPicker: picker)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@ class PhotosInputPlaceholderCell: UICollectionViewCell {
|
|||
private var imageView: UIImageView!
|
||||
private func commonInit() {
|
||||
self.imageView = UIImageView()
|
||||
self.imageView.contentMode = .center
|
||||
self.imageView.image = UIImage(named: Constants.imageName, in: Bundle(for: PhotosInputPlaceholderCell.self), compatibleWith: nil)
|
||||
self.imageView.contentMode = .Center
|
||||
self.imageView.image = UIImage(named: Constants.imageName, inBundle: NSBundle(forClass: PhotosInputPlaceholderCell.self), compatibleWithTraitCollection: nil)
|
||||
self.contentView.addSubview(self.imageView)
|
||||
self.contentView.backgroundColor = Constants.backgroundColor
|
||||
}
|
||||
|
|
@ -77,7 +77,7 @@ class PhotosInputCell: UICollectionViewCell {
|
|||
private func commonInit() {
|
||||
self.clipsToBounds = true
|
||||
self.imageView = UIImageView()
|
||||
self.imageView.contentMode = .scaleAspectFill
|
||||
self.imageView.contentMode = .ScaleAspectFill
|
||||
self.contentView.addSubview(self.imageView)
|
||||
self.contentView.backgroundColor = Constants.backgroundColor
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
import UIKit
|
||||
|
||||
protocol PhotosInputCellProviderProtocol {
|
||||
func cellForItemAtIndexPath(_ indexPath: IndexPath) -> UICollectionViewCell
|
||||
func cellForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewCell
|
||||
}
|
||||
|
||||
class PhotosInputPlaceholderCellProvider: PhotosInputCellProviderProtocol {
|
||||
|
|
@ -33,11 +33,11 @@ class PhotosInputPlaceholderCellProvider: PhotosInputCellProviderProtocol {
|
|||
private let collectionView: UICollectionView
|
||||
init(collectionView: UICollectionView) {
|
||||
self.collectionView = collectionView
|
||||
self.collectionView.register(PhotosInputPlaceholderCell.self, forCellWithReuseIdentifier: self.reuseIdentifier)
|
||||
self.collectionView.registerClass(PhotosInputPlaceholderCell.self, forCellWithReuseIdentifier: self.reuseIdentifier)
|
||||
}
|
||||
|
||||
func cellForItemAtIndexPath(_ indexPath: IndexPath) -> UICollectionViewCell {
|
||||
return self.collectionView.dequeueReusableCell(withReuseIdentifier: self.reuseIdentifier, for: indexPath)
|
||||
func cellForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewCell {
|
||||
return self.collectionView.dequeueReusableCellWithReuseIdentifier(self.reuseIdentifier, forIndexPath: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -48,20 +48,20 @@ class PhotosInputCellProvider: PhotosInputCellProviderProtocol {
|
|||
init(collectionView: UICollectionView, dataProvider: PhotosInputDataProviderProtocol) {
|
||||
self.dataProvider = dataProvider
|
||||
self.collectionView = collectionView
|
||||
self.collectionView.register(PhotosInputCell.self, forCellWithReuseIdentifier: self.reuseIdentifier)
|
||||
self.collectionView.registerClass(PhotosInputCell.self, forCellWithReuseIdentifier: self.reuseIdentifier)
|
||||
}
|
||||
|
||||
func cellForItemAtIndexPath(_ indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = self.collectionView.dequeueReusableCell(withReuseIdentifier: self.reuseIdentifier, for: indexPath) as! PhotosInputCell
|
||||
func cellForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewCell {
|
||||
let cell = self.collectionView.dequeueReusableCellWithReuseIdentifier(self.reuseIdentifier, forIndexPath: indexPath) as! PhotosInputCell
|
||||
self.configureCell(cell, atIndexPath: indexPath)
|
||||
return cell
|
||||
}
|
||||
|
||||
private let previewRequests = NSMapTable<PhotosInputCell, NSNumber>.weakToStrongObjects()
|
||||
private func configureCell(_ cell: PhotosInputCell, atIndexPath indexPath: IndexPath) {
|
||||
if let requestID = self.previewRequests.object(forKey: cell) {
|
||||
self.previewRequests.removeObject(forKey: cell)
|
||||
self.dataProvider.cancelPreviewImageRequest(requestID.int32Value)
|
||||
private let previewRequests = NSMapTable.weakToStrongObjectsMapTable()
|
||||
private func configureCell(cell: PhotosInputCell, atIndexPath indexPath: NSIndexPath) {
|
||||
if let requestID = self.previewRequests.objectForKey(cell) as? NSNumber {
|
||||
self.previewRequests.removeObjectForKey(cell)
|
||||
self.dataProvider.cancelPreviewImageRequest(requestID.intValue)
|
||||
}
|
||||
|
||||
let index = indexPath.item - 1
|
||||
|
|
@ -69,17 +69,17 @@ class PhotosInputCellProvider: PhotosInputCellProviderProtocol {
|
|||
var imageProvidedSynchronously = true
|
||||
var requestID: Int32 = -1
|
||||
requestID = self.dataProvider.requestPreviewImageAtIndex(index, targetSize: targetSize) { [weak self, weak cell] image in
|
||||
guard let sSelf = self, let sCell = cell else { return }
|
||||
guard let sSelf = self, sCell = cell else { return }
|
||||
// We can get here even afer calling cancelPreviewImageRequest (looks liek a race condition in PHImageManager)
|
||||
// Also, according to PHImageManager's documentation, this block can be called several times: we may receive an image with a low quality and then receive an update with a better one
|
||||
// This can also be called before returning from requestPreviewImageAtIndex (synchronously) if the image is cached by PHImageManager
|
||||
let imageIsForThisCell = imageProvidedSynchronously || sSelf.previewRequests.object(forKey: sCell)?.int32Value == requestID
|
||||
let imageIsForThisCell = imageProvidedSynchronously || (sSelf.previewRequests.objectForKey(sCell) as? NSNumber)?.intValue == requestID
|
||||
if imageIsForThisCell {
|
||||
sCell.image = image
|
||||
}
|
||||
}
|
||||
imageProvidedSynchronously = false
|
||||
|
||||
self.previewRequests.setObject(NSNumber(value: requestID), forKey:cell)
|
||||
self.previewRequests.setObject(NSNumber(int: requestID), forKey:cell)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,15 +25,15 @@
|
|||
import PhotosUI
|
||||
|
||||
protocol PhotosInputDataProviderDelegate: class {
|
||||
func handlePhotosInpudDataProviderUpdate(_ dataProvider: PhotosInputDataProviderProtocol, updateBlock: @escaping () -> Void)
|
||||
func handlePhotosInpudDataProviderUpdate(dataProvider: PhotosInputDataProviderProtocol, updateBlock: () -> Void)
|
||||
}
|
||||
|
||||
protocol PhotosInputDataProviderProtocol {
|
||||
weak var delegate: PhotosInputDataProviderDelegate? { get set }
|
||||
var count: Int { get }
|
||||
func requestPreviewImageAtIndex(_ index: Int, targetSize: CGSize, completion: @escaping (UIImage) -> Void) -> Int32
|
||||
func requestFullImageAtIndex(_ index: Int, completion: @escaping (UIImage) -> Void)
|
||||
func cancelPreviewImageRequest(_ requestID: Int32)
|
||||
func requestPreviewImageAtIndex(index: Int, targetSize: CGSize, completion: (UIImage) -> Void) -> Int32
|
||||
func requestFullImageAtIndex(index: Int, completion: (UIImage) -> Void)
|
||||
func cancelPreviewImageRequest(requestID: Int32)
|
||||
}
|
||||
|
||||
class PhotosInputPlaceholderDataProvider: PhotosInputDataProviderProtocol {
|
||||
|
|
@ -49,14 +49,14 @@ class PhotosInputPlaceholderDataProvider: PhotosInputDataProviderProtocol {
|
|||
return self.numberOfPlaceholders
|
||||
}
|
||||
|
||||
func requestPreviewImageAtIndex(_ index: Int, targetSize: CGSize, completion: @escaping (UIImage) -> Void) -> Int32 {
|
||||
func requestPreviewImageAtIndex(index: Int, targetSize: CGSize, completion: (UIImage) -> Void) -> Int32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func requestFullImageAtIndex(_ index: Int, completion: @escaping (UIImage) -> Void) {
|
||||
func requestFullImageAtIndex(index: Int, completion: (UIImage) -> Void) {
|
||||
}
|
||||
|
||||
func cancelPreviewImageRequest(_ requestID: Int32) {
|
||||
func cancelPreviewImageRequest(requestID: Int32) {
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -64,52 +64,52 @@ class PhotosInputPlaceholderDataProvider: PhotosInputDataProviderProtocol {
|
|||
class PhotosInputDataProvider: NSObject, PhotosInputDataProviderProtocol, PHPhotoLibraryChangeObserver {
|
||||
weak var delegate: PhotosInputDataProviderDelegate?
|
||||
private var imageManager = PHCachingImageManager()
|
||||
private var fetchResult: PHFetchResult<PHAsset>!
|
||||
private var fetchResult: PHFetchResult!
|
||||
override init() {
|
||||
func fetchOptions(_ predicate: NSPredicate?) -> PHFetchOptions {
|
||||
func fetchOptions(predicate: NSPredicate?) -> PHFetchOptions {
|
||||
let options = PHFetchOptions()
|
||||
options.sortDescriptors = [ NSSortDescriptor(key: "creationDate", ascending: false) ]
|
||||
options.predicate = predicate
|
||||
return options
|
||||
}
|
||||
|
||||
if let userLibraryCollection = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: nil).firstObject {
|
||||
self.fetchResult = PHAsset.fetchAssets(in: userLibraryCollection, options: fetchOptions(NSPredicate(format: "mediaType = \(PHAssetMediaType.image.rawValue)")))
|
||||
if let userLibraryCollection = PHAssetCollection.fetchAssetCollectionsWithType(.SmartAlbum, subtype: .SmartAlbumUserLibrary, options: nil).firstObject as? PHAssetCollection {
|
||||
self.fetchResult = PHAsset.fetchAssetsInAssetCollection(userLibraryCollection, options: fetchOptions(NSPredicate(format: "mediaType = \(PHAssetMediaType.Image.rawValue)")))
|
||||
} else {
|
||||
self.fetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions(nil))
|
||||
self.fetchResult = PHAsset.fetchAssetsWithMediaType(.Image, options: fetchOptions(nil))
|
||||
}
|
||||
super.init()
|
||||
PHPhotoLibrary.shared().register(self)
|
||||
PHPhotoLibrary.sharedPhotoLibrary().registerChangeObserver(self)
|
||||
}
|
||||
|
||||
deinit {
|
||||
PHPhotoLibrary.shared().unregisterChangeObserver(self)
|
||||
PHPhotoLibrary.sharedPhotoLibrary().unregisterChangeObserver(self)
|
||||
}
|
||||
|
||||
var count: Int {
|
||||
return self.fetchResult.count
|
||||
}
|
||||
|
||||
func requestPreviewImageAtIndex(_ index: Int, targetSize: CGSize, completion: @escaping (UIImage) -> Void) -> Int32 {
|
||||
func requestPreviewImageAtIndex(index: Int, targetSize: CGSize, completion: (UIImage) -> Void) -> Int32 {
|
||||
assert(index >= 0 && index < self.fetchResult.count, "Index out of bounds")
|
||||
let asset = self.fetchResult[index]
|
||||
let asset = self.fetchResult[index] as! PHAsset
|
||||
let options = PHImageRequestOptions()
|
||||
options.deliveryMode = .highQualityFormat
|
||||
return self.imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options) { (image, info) in
|
||||
options.deliveryMode = .HighQualityFormat
|
||||
return self.imageManager.requestImageForAsset(asset, targetSize: targetSize, contentMode: .AspectFill, options: options) { (image, info) in
|
||||
if let image = image {
|
||||
completion(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cancelPreviewImageRequest(_ requestID: Int32) {
|
||||
func cancelPreviewImageRequest(requestID: Int32) {
|
||||
self.imageManager.cancelImageRequest(requestID)
|
||||
}
|
||||
|
||||
func requestFullImageAtIndex(_ index: Int, completion: @escaping (UIImage) -> Void) {
|
||||
func requestFullImageAtIndex(index: Int, completion: (UIImage) -> Void) {
|
||||
assert(index >= 0 && index < self.fetchResult.count, "Index out of bounds")
|
||||
let asset = self.fetchResult[index]
|
||||
self.imageManager.requestImageData(for: asset, options: .none) { (data, dataUTI, orientation, info) -> Void in
|
||||
let asset = self.fetchResult[index] as! PHAsset
|
||||
self.imageManager.requestImageDataForAsset(asset, options: .None) { (data, dataUTI, orientation, info) -> Void in
|
||||
if let data = data, let image = UIImage(data: data) {
|
||||
completion(image)
|
||||
}
|
||||
|
|
@ -118,14 +118,14 @@ class PhotosInputDataProvider: NSObject, PhotosInputDataProviderProtocol, PHPhot
|
|||
|
||||
// MARK: PHPhotoLibraryChangeObserver
|
||||
|
||||
func photoLibraryDidChange(_ changeInstance: PHChange) {
|
||||
func photoLibraryDidChange(changeInstance: PHChange) {
|
||||
// Photos may call this method on a background queue; switch to the main queue to update the UI.
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
dispatch_async(dispatch_get_main_queue()) { [weak self] in
|
||||
guard let sSelf = self else { return }
|
||||
|
||||
if let changeDetails = changeInstance.changeDetails(for: sSelf.fetchResult as! PHFetchResult<PHObject>) {
|
||||
if let changeDetails = changeInstance.changeDetailsForFetchResult(sSelf.fetchResult) {
|
||||
let updateBlock = { () -> Void in
|
||||
self?.fetchResult = changeDetails.fetchResultAfterChanges as! PHFetchResult<PHAsset>
|
||||
self?.fetchResult = changeDetails.fetchResultAfterChanges
|
||||
}
|
||||
sSelf.delegate?.handlePhotosInpudDataProviderUpdate(sSelf, updateBlock: updateBlock)
|
||||
}
|
||||
|
|
@ -148,7 +148,7 @@ class PhotosInputWithPlaceholdersDataProvider: PhotosInputDataProviderProtocol,
|
|||
return max(self.photosDataProvider.count, self.placeholdersDataProvider.count)
|
||||
}
|
||||
|
||||
func requestPreviewImageAtIndex(_ index: Int, targetSize: CGSize, completion: @escaping (UIImage) -> Void) -> Int32 {
|
||||
func requestPreviewImageAtIndex(index: Int, targetSize: CGSize, completion: (UIImage) -> Void) -> Int32 {
|
||||
if index < self.photosDataProvider.count {
|
||||
return self.photosDataProvider.requestPreviewImageAtIndex(index, targetSize: targetSize, completion: completion)
|
||||
} else {
|
||||
|
|
@ -156,7 +156,7 @@ class PhotosInputWithPlaceholdersDataProvider: PhotosInputDataProviderProtocol,
|
|||
}
|
||||
}
|
||||
|
||||
func requestFullImageAtIndex(_ index: Int, completion: @escaping (UIImage) -> Void) {
|
||||
func requestFullImageAtIndex(index: Int, completion: (UIImage) -> Void) {
|
||||
if index < self.photosDataProvider.count {
|
||||
return self.photosDataProvider.requestFullImageAtIndex(index, completion: completion)
|
||||
} else {
|
||||
|
|
@ -164,13 +164,13 @@ class PhotosInputWithPlaceholdersDataProvider: PhotosInputDataProviderProtocol,
|
|||
}
|
||||
}
|
||||
|
||||
func cancelPreviewImageRequest(_ requestID: Int32) {
|
||||
func cancelPreviewImageRequest(requestID: Int32) {
|
||||
return self.photosDataProvider.cancelPreviewImageRequest(requestID)
|
||||
}
|
||||
|
||||
// MARK: PhotosInputDataProviderDelegate
|
||||
|
||||
func handlePhotosInpudDataProviderUpdate(_ dataProvider: PhotosInputDataProviderProtocol, updateBlock: @escaping () -> Void) {
|
||||
func handlePhotosInpudDataProviderUpdate(dataProvider: PhotosInputDataProviderProtocol, updateBlock: () -> Void) {
|
||||
self.delegate?.handlePhotosInpudDataProviderUpdate(self, updateBlock: updateBlock)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,26 +39,26 @@ protocol PhotosInputViewProtocol {
|
|||
}
|
||||
|
||||
protocol PhotosInputViewDelegate: class {
|
||||
func inputView(_ inputView: PhotosInputViewProtocol, didSelectImage image: UIImage)
|
||||
func inputViewDidRequestCameraPermission(_ inputView: PhotosInputViewProtocol)
|
||||
func inputViewDidRequestPhotoLibraryPermission(_ inputView: PhotosInputViewProtocol)
|
||||
func inputView(inputView: PhotosInputViewProtocol, didSelectImage image: UIImage)
|
||||
func inputViewDidRequestCameraPermission(inputView: PhotosInputViewProtocol)
|
||||
func inputViewDidRequestPhotoLibraryPermission(inputView: PhotosInputViewProtocol)
|
||||
}
|
||||
|
||||
class PhotosInputView: UIView, PhotosInputViewProtocol {
|
||||
|
||||
fileprivate struct Constants {
|
||||
private struct Constants {
|
||||
static let liveCameraItemIndex = 0
|
||||
}
|
||||
|
||||
fileprivate lazy var collectionViewQueue = SerialTaskQueue()
|
||||
fileprivate var collectionView: UICollectionView!
|
||||
fileprivate var collectionViewLayout: UICollectionViewFlowLayout!
|
||||
fileprivate var dataProvider: PhotosInputDataProviderProtocol!
|
||||
fileprivate var cellProvider: PhotosInputCellProviderProtocol!
|
||||
fileprivate var itemSizeCalculator: PhotosInputViewItemSizeCalculator!
|
||||
private lazy var collectionViewQueue = SerialTaskQueue()
|
||||
private var collectionView: UICollectionView!
|
||||
private var collectionViewLayout: UICollectionViewFlowLayout!
|
||||
private var dataProvider: PhotosInputDataProviderProtocol!
|
||||
private var cellProvider: PhotosInputCellProviderProtocol!
|
||||
private var itemSizeCalculator: PhotosInputViewItemSizeCalculator!
|
||||
|
||||
var cameraAuthorizationStatus: AVAuthorizationStatus {
|
||||
return AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeVideo)
|
||||
return AVCaptureDevice.authorizationStatusForMediaType(AVMediaTypeVideo)
|
||||
}
|
||||
|
||||
var photoLibraryAuthorizationStatus: PHAuthorizationStatus {
|
||||
|
|
@ -91,7 +91,7 @@ class PhotosInputView: UIView, PhotosInputViewProtocol {
|
|||
}
|
||||
|
||||
private func commonInit() {
|
||||
self.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
self.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
|
||||
self.configureCollectionView()
|
||||
self.configureItemSizeCalculator()
|
||||
self.dataProvider = PhotosInputPlaceholderDataProvider()
|
||||
|
|
@ -108,10 +108,10 @@ class PhotosInputView: UIView, PhotosInputViewProtocol {
|
|||
}
|
||||
|
||||
private func requestAccessToVideo() {
|
||||
guard self.cameraAuthorizationStatus != .authorized else { return }
|
||||
guard self.cameraAuthorizationStatus != .Authorized else { return }
|
||||
|
||||
AVCaptureDevice.requestAccess(forMediaType: AVMediaTypeVideo) { (success) -> Void in
|
||||
DispatchQueue.main.async(execute: { () -> Void in
|
||||
AVCaptureDevice.requestAccessForMediaType(AVMediaTypeVideo) { (success) -> Void in
|
||||
dispatch_async(dispatch_get_main_queue(), { () -> Void in
|
||||
self.reloadVideoItem()
|
||||
})
|
||||
}
|
||||
|
|
@ -122,22 +122,22 @@ class PhotosInputView: UIView, PhotosInputViewProtocol {
|
|||
guard let sSelf = self else { return }
|
||||
|
||||
sSelf.collectionView.performBatchUpdates({
|
||||
sSelf.collectionView.reloadItems(at: [IndexPath(item: Constants.liveCameraItemIndex, section: 0)])
|
||||
sSelf.collectionView.reloadItemsAtIndexPaths([NSIndexPath(forItem: Constants.liveCameraItemIndex, inSection: 0)])
|
||||
}, completion: { (finished) in
|
||||
DispatchQueue.main.async(execute: completion)
|
||||
dispatch_async(dispatch_get_main_queue(), completion)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func requestAccessToPhoto() {
|
||||
guard self.photoLibraryAuthorizationStatus != .authorized else {
|
||||
guard self.photoLibraryAuthorizationStatus != .Authorized else {
|
||||
self.replacePlaceholderItemsWithPhotoItems()
|
||||
return
|
||||
}
|
||||
|
||||
PHPhotoLibrary.requestAuthorization { (status: PHAuthorizationStatus) -> Void in
|
||||
if status == PHAuthorizationStatus.authorized {
|
||||
DispatchQueue.main.async(execute: { () -> Void in
|
||||
if status == PHAuthorizationStatus.Authorized {
|
||||
dispatch_async(dispatch_get_main_queue(), { () -> Void in
|
||||
self.replacePlaceholderItemsWithPhotoItems()
|
||||
})
|
||||
}
|
||||
|
|
@ -153,23 +153,23 @@ class PhotosInputView: UIView, PhotosInputViewProtocol {
|
|||
sSelf.dataProvider = newDataProvider
|
||||
sSelf.cellProvider = PhotosInputCellProvider(collectionView: sSelf.collectionView, dataProvider: newDataProvider)
|
||||
sSelf.collectionView.reloadData()
|
||||
DispatchQueue.main.async(execute: completion)
|
||||
dispatch_async(dispatch_get_main_queue(), completion)
|
||||
}
|
||||
}
|
||||
|
||||
func reload() {
|
||||
self.collectionViewQueue.addTask { [weak self] (completion) in
|
||||
self?.collectionView.reloadData()
|
||||
DispatchQueue.main.async(execute: completion)
|
||||
dispatch_async(dispatch_get_main_queue(), completion)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate lazy var cameraPicker: PhotosInputCameraPicker = {
|
||||
private lazy var cameraPicker: PhotosInputCameraPicker = {
|
||||
return PhotosInputCameraPicker(presentingController: self.presentingController)
|
||||
}()
|
||||
|
||||
fileprivate lazy var liveCameraPresenter: LiveCameraCellPresenter = {
|
||||
return LiveCameraCellPresenter(cellAppearance: self.appearance?.liveCameraCellAppearence ?? LiveCameraCellAppearance.createDefaultAppearance())
|
||||
private lazy var liveCameraPresenter: LiveCameraCellPresenter = {
|
||||
return LiveCameraCellPresenter(cellAppearance: self.appearance?.liveCameraCellAppearence)
|
||||
}()
|
||||
}
|
||||
|
||||
|
|
@ -178,7 +178,7 @@ extension PhotosInputView: UICollectionViewDataSource {
|
|||
func configureCollectionView() {
|
||||
self.collectionViewLayout = PhotosInputCollectionViewLayout()
|
||||
self.collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: self.collectionViewLayout)
|
||||
self.collectionView.backgroundColor = UIColor.white
|
||||
self.collectionView.backgroundColor = UIColor.whiteColor()
|
||||
self.collectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
LiveCameraCellPresenter.registerCells(collectionView: self.collectionView)
|
||||
|
||||
|
|
@ -186,17 +186,17 @@ extension PhotosInputView: UICollectionViewDataSource {
|
|||
self.collectionView.delegate = self
|
||||
|
||||
self.addSubview(self.collectionView)
|
||||
self.addConstraint(NSLayoutConstraint(item: self.collectionView, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1, constant: 0))
|
||||
self.addConstraint(NSLayoutConstraint(item: self.collectionView, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1, constant: 0))
|
||||
self.addConstraint(NSLayoutConstraint(item: self.collectionView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: 0))
|
||||
self.addConstraint(NSLayoutConstraint(item: self.collectionView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1, constant: 0))
|
||||
self.addConstraint(NSLayoutConstraint(item: self.collectionView, attribute: .Leading, relatedBy: .Equal, toItem: self, attribute: .Leading, multiplier: 1, constant: 0))
|
||||
self.addConstraint(NSLayoutConstraint(item: self.collectionView, attribute: .Trailing, relatedBy: .Equal, toItem: self, attribute: .Trailing, multiplier: 1, constant: 0))
|
||||
self.addConstraint(NSLayoutConstraint(item: self.collectionView, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: 0))
|
||||
self.addConstraint(NSLayoutConstraint(item: self.collectionView, attribute: .Bottom, relatedBy: .Equal, toItem: self, attribute: .Bottom, multiplier: 1, constant: 0))
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return self.dataProvider.count + 1
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
|
||||
var cell: UICollectionViewCell
|
||||
if indexPath.item == Constants.liveCameraItemIndex {
|
||||
cell = self.liveCameraPresenter.dequeueCell(collectionView: collectionView, indexPath: indexPath)
|
||||
|
|
@ -208,9 +208,9 @@ extension PhotosInputView: UICollectionViewDataSource {
|
|||
}
|
||||
|
||||
extension PhotosInputView: UICollectionViewDelegateFlowLayout {
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
|
||||
if indexPath.item == Constants.liveCameraItemIndex {
|
||||
if self.cameraAuthorizationStatus != .authorized {
|
||||
if self.cameraAuthorizationStatus != .Authorized {
|
||||
self.delegate?.inputViewDidRequestCameraPermission(self)
|
||||
} else {
|
||||
self.liveCameraPresenter.cameraPickerWillAppear()
|
||||
|
|
@ -225,7 +225,7 @@ extension PhotosInputView: UICollectionViewDelegateFlowLayout {
|
|||
})
|
||||
}
|
||||
} else {
|
||||
if self.photoLibraryAuthorizationStatus != .authorized {
|
||||
if self.photoLibraryAuthorizationStatus != .Authorized {
|
||||
self.delegate?.inputViewDidRequestPhotoLibraryPermission(self)
|
||||
} else {
|
||||
self.dataProvider.requestFullImageAtIndex(indexPath.item - 1) { image in
|
||||
|
|
@ -235,25 +235,25 @@ extension PhotosInputView: UICollectionViewDelegateFlowLayout {
|
|||
}
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
|
||||
return self.itemSizeCalculator.itemSizeForWidth(collectionView.bounds.width, atIndex: indexPath.item)
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
|
||||
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat {
|
||||
return self.itemSizeCalculator.interitemSpace
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
|
||||
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat {
|
||||
return self.itemSizeCalculator.interitemSpace
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
|
||||
if indexPath.item == Constants.liveCameraItemIndex {
|
||||
self.liveCameraPresenter.cellWillBeShown(cell)
|
||||
}
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
|
||||
func collectionView(collectionView: UICollectionView, didEndDisplayingCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
|
||||
if indexPath.item == Constants.liveCameraItemIndex {
|
||||
self.liveCameraPresenter.cellWasHidden(cell)
|
||||
}
|
||||
|
|
@ -261,20 +261,20 @@ extension PhotosInputView: UICollectionViewDelegateFlowLayout {
|
|||
}
|
||||
|
||||
extension PhotosInputView: PhotosInputDataProviderDelegate {
|
||||
func handlePhotosInpudDataProviderUpdate(_ dataProvider: PhotosInputDataProviderProtocol, updateBlock: @escaping () -> Void) {
|
||||
func handlePhotosInpudDataProviderUpdate(dataProvider: PhotosInputDataProviderProtocol, updateBlock: () -> Void) {
|
||||
self.collectionViewQueue.addTask { [weak self] (completion) in
|
||||
guard let sSelf = self else { return }
|
||||
|
||||
updateBlock()
|
||||
sSelf.collectionView.reloadData()
|
||||
DispatchQueue.main.async(execute: completion)
|
||||
dispatch_async(dispatch_get_main_queue(), completion)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class PhotosInputCollectionViewLayout: UICollectionViewFlowLayout {
|
||||
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
|
||||
private override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
|
||||
return newBounds.width != self.collectionView?.bounds.width
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ struct PhotosInputViewItemSizeCalculator {
|
|||
var itemsPerRow: Int = 0
|
||||
var interitemSpace: CGFloat = 0
|
||||
|
||||
func itemSizeForWidth(_ width: CGFloat, atIndex index: Int) -> CGSize {
|
||||
func itemSizeForWidth(width: CGFloat, atIndex index: Int) -> CGSize {
|
||||
let availableWidth = width - self.interitemSpace * CGFloat((self.itemsPerRow - 1))
|
||||
if availableWidth <= 0 {
|
||||
return CGSize.zero
|
||||
|
|
|
|||
|
|
@ -24,22 +24,22 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
@objc open class ReusableXibView: UIView {
|
||||
@objc public class ReusableXibView: UIView {
|
||||
|
||||
func loadViewFromNib() -> UIView {
|
||||
let bundle = Bundle(for: type(of: self))
|
||||
let nib = UINib(nibName:type(of: self).nibName(), bundle: bundle)
|
||||
let view = nib.instantiate(withOwner: nil, options: nil).first as! UIView
|
||||
let bundle = NSBundle(forClass: self.dynamicType)
|
||||
let nib = UINib(nibName:self.dynamicType.nibName(), bundle: bundle)
|
||||
let view = nib.instantiateWithOwner(nil, options: nil).first as! UIView
|
||||
return view
|
||||
}
|
||||
|
||||
override open func awakeAfter(using aDecoder: NSCoder) -> Any? {
|
||||
override public func awakeAfterUsingCoder(aDecoder: NSCoder) -> AnyObject? {
|
||||
if self.subviews.count > 0 {
|
||||
return self
|
||||
}
|
||||
|
||||
let bundle = Bundle(for: type(of: self))
|
||||
if let loadedView = bundle.loadNibNamed(type(of: self).nibName(), owner: nil, options: nil)?.first as? UIView {
|
||||
let bundle = NSBundle(forClass: self.dynamicType)
|
||||
if let loadedView = bundle.loadNibNamed(self.dynamicType.nibName(), owner: nil, options: nil)?.first as? UIView {
|
||||
loadedView.frame = frame
|
||||
loadedView.autoresizingMask = autoresizingMask
|
||||
loadedView.translatesAutoresizingMaskIntoConstraints = translatesAutoresizingMaskIntoConstraints
|
||||
|
|
|
|||
|
|
@ -38,10 +38,10 @@ public class TabInputButton: UIButton {
|
|||
|
||||
static public func makeInputButton(withAppearance appearance: TabInputButtonAppearance, accessibilityID: String? = nil) -> TabInputButton {
|
||||
let images = appearance.images
|
||||
let button = TabInputButton(type: .custom)
|
||||
button.isExclusiveTouch = true
|
||||
let button = TabInputButton(type: .Custom)
|
||||
button.exclusiveTouch = true
|
||||
images.forEach { (state, image) in
|
||||
button.setImage(image, for: state.controlState)
|
||||
button.setImage(image, forState: state.controlState)
|
||||
}
|
||||
if let accessibilityIdentifier = accessibilityID {
|
||||
button.accessibilityIdentifier = accessibilityIdentifier
|
||||
|
|
@ -52,10 +52,10 @@ public class TabInputButton: UIButton {
|
|||
|
||||
private var size: CGSize?
|
||||
|
||||
public override var intrinsicContentSize: CGSize {
|
||||
public override func intrinsicContentSize() -> CGSize {
|
||||
if let size = self.size {
|
||||
return size
|
||||
}
|
||||
return super.intrinsicContentSize
|
||||
return super.intrinsicContentSize()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
open class TextChatInputItem {
|
||||
public class TextChatInputItem {
|
||||
typealias Class = TextChatInputItem
|
||||
public var textInputHandler: ((String) -> Void)?
|
||||
|
||||
|
|
@ -33,22 +33,22 @@ open class TextChatInputItem {
|
|||
self.buttonAppearance = tabInputButtonAppearance
|
||||
}
|
||||
|
||||
public static func createDefaultButtonAppearance() -> TabInputButtonAppearance {
|
||||
public class func createDefaultButtonAppearance() -> TabInputButtonAppearance {
|
||||
let images: [UIControlStateWrapper: UIImage] = [
|
||||
UIControlStateWrapper(state: .normal): UIImage(named: "text-icon-unselected", in: Bundle(for: TextChatInputItem.self), compatibleWith: nil)!,
|
||||
UIControlStateWrapper(state: .selected): UIImage(named: "text-icon-selected", in: Bundle(for: TextChatInputItem.self), compatibleWith: nil)!,
|
||||
UIControlStateWrapper(state: .highlighted): UIImage(named: "text-icon-selected", in: Bundle(for: TextChatInputItem.self), compatibleWith: nil)!
|
||||
UIControlStateWrapper(state: .Normal): UIImage(named: "text-icon-unselected", inBundle: NSBundle(forClass: TextChatInputItem.self), compatibleWithTraitCollection: nil)!,
|
||||
UIControlStateWrapper(state: .Selected): UIImage(named: "text-icon-selected", inBundle: NSBundle(forClass: TextChatInputItem.self), compatibleWithTraitCollection: nil)!,
|
||||
UIControlStateWrapper(state: .Highlighted): UIImage(named: "text-icon-selected", inBundle: NSBundle(forClass: TextChatInputItem.self), compatibleWithTraitCollection: nil)!
|
||||
]
|
||||
return TabInputButtonAppearance(images: images, size: nil)
|
||||
}
|
||||
|
||||
lazy fileprivate var internalTabView: TabInputButton = {
|
||||
lazy private var internalTabView: TabInputButton = {
|
||||
return TabInputButton.makeInputButton(withAppearance: self.buttonAppearance, accessibilityID: "text.chat.input.view")
|
||||
}()
|
||||
|
||||
open var selected = false {
|
||||
public var selected = false {
|
||||
didSet {
|
||||
self.internalTabView.isSelected = self.selected
|
||||
self.internalTabView.selected = self.selected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -56,7 +56,7 @@ open class TextChatInputItem {
|
|||
// MARK: - ChatInputItemProtocol
|
||||
extension TextChatInputItem : ChatInputItemProtocol {
|
||||
public var presentationMode: ChatInputItemPresentationMode {
|
||||
return .keyboard
|
||||
return .Keyboard
|
||||
}
|
||||
|
||||
public var showsSendButton: Bool {
|
||||
|
|
@ -71,7 +71,7 @@ extension TextChatInputItem : ChatInputItemProtocol {
|
|||
return self.internalTabView
|
||||
}
|
||||
|
||||
public func handleInput(_ input: AnyObject) {
|
||||
public func handleInput(input: AnyObject) {
|
||||
if let text = input as? String {
|
||||
self.textInputHandler?(text)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,12 +36,12 @@ public class Observable<T> {
|
|||
didSet {
|
||||
self.cleanDeadObservers()
|
||||
for observer in self.observers {
|
||||
observer.closure(oldValue, self.value)
|
||||
observer.closure(old: oldValue, new: self.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func observe(_ observer: AnyObject, closure: @escaping (_ old: T, _ new: T) -> ()) {
|
||||
public func observe(observer: AnyObject, closure: (old: T, new: T) -> ()) {
|
||||
self.observers.append(Observer(owner: observer, closure: closure))
|
||||
self.cleanDeadObservers()
|
||||
}
|
||||
|
|
@ -55,8 +55,8 @@ public class Observable<T> {
|
|||
|
||||
private struct Observer<T> {
|
||||
weak var owner: AnyObject?
|
||||
let closure: (_ old: T, _ new: T) -> ()
|
||||
init (owner: AnyObject, closure: @escaping (_ old: T, _ new: T) -> ()) {
|
||||
let closure: (old: T, new: T) -> ()
|
||||
init (owner: AnyObject, closure: (old: T, new: T) -> ()) {
|
||||
self.owner = owner
|
||||
self.closure = closure
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,7 +101,6 @@
|
|||
rotationAnimation.duration = 1;
|
||||
rotationAnimation.cumulative = YES;
|
||||
rotationAnimation.repeatCount = HUGE_VALF;
|
||||
rotationAnimation.removedOnCompletion = NO;
|
||||
[_bgLayer addAnimation:rotationAnimation forKey:@"rotationAnimation"];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,26 +25,26 @@
|
|||
import Foundation
|
||||
import CoreGraphics
|
||||
|
||||
private let scale = UIScreen.main.scale
|
||||
private let scale = UIScreen.mainScreen().scale
|
||||
|
||||
public enum HorizontalAlignment {
|
||||
case left
|
||||
case center
|
||||
case right
|
||||
case Left
|
||||
case Center
|
||||
case Right
|
||||
}
|
||||
|
||||
public enum VerticalAlignment {
|
||||
case top
|
||||
case center
|
||||
case bottom
|
||||
case Top
|
||||
case Center
|
||||
case Bottom
|
||||
}
|
||||
|
||||
public extension CGSize {
|
||||
func bma_insetBy(dx: CGFloat, dy: CGFloat) -> CGSize {
|
||||
func bma_insetBy(dx dx: CGFloat, dy: CGFloat) -> CGSize {
|
||||
return CGSize(width: self.width - dx, height: self.height - dy)
|
||||
}
|
||||
|
||||
func bma_outsetBy(dx: CGFloat, dy: CGFloat) -> CGSize {
|
||||
func bma_outsetBy(dx dx: CGFloat, dy: CGFloat) -> CGSize {
|
||||
return self.bma_insetBy(dx: -dx, dy: -dy)
|
||||
}
|
||||
}
|
||||
|
|
@ -59,21 +59,21 @@ public extension CGSize {
|
|||
|
||||
// Horizontal alignment
|
||||
switch xAlignament {
|
||||
case .left:
|
||||
case .Left:
|
||||
originX = 0
|
||||
case .center:
|
||||
case .Center:
|
||||
originX = containerRect.midX - self.width / 2.0
|
||||
case .right:
|
||||
case .Right:
|
||||
originX = containerRect.maxY - self.width
|
||||
}
|
||||
|
||||
// Vertical alignment
|
||||
switch yAlignment {
|
||||
case .top:
|
||||
case .Top:
|
||||
originY = 0
|
||||
case .center:
|
||||
case .Center:
|
||||
originY = containerRect.midY - self.height / 2.0
|
||||
case .bottom:
|
||||
case .Bottom:
|
||||
originY = containerRect.maxY - self.height
|
||||
}
|
||||
|
||||
|
|
@ -105,8 +105,9 @@ public extension CGRect {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public extension CGPoint {
|
||||
func bma_offsetBy(dx: CGFloat, dy: CGFloat) -> CGPoint {
|
||||
func bma_offsetBy(dx dx: CGFloat, dy: CGFloat) -> CGPoint {
|
||||
return CGPoint(x: self.x + dx, y: self.y + dy)
|
||||
}
|
||||
}
|
||||
|
|
@ -146,38 +147,39 @@ public extension UIEdgeInsets {
|
|||
|
||||
}
|
||||
|
||||
|
||||
public extension UIImage {
|
||||
|
||||
public func bma_tintWithColor(_ color: UIColor) -> UIImage {
|
||||
public func bma_tintWithColor(color: UIColor) -> UIImage {
|
||||
let rect = CGRect(origin: CGPoint.zero, size: self.size)
|
||||
UIGraphicsBeginImageContextWithOptions(rect.size, false, self.scale)
|
||||
let context = UIGraphicsGetCurrentContext()!
|
||||
color.setFill()
|
||||
context.fill(rect)
|
||||
self.draw(in: rect, blendMode: .destinationIn, alpha: 1)
|
||||
CGContextFillRect(context, rect)
|
||||
self.drawInRect(rect, blendMode: .DestinationIn, alpha: 1)
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()!
|
||||
UIGraphicsEndImageContext()
|
||||
return image.resizableImage(withCapInsets: self.capInsets)
|
||||
return image.resizableImageWithCapInsets(self.capInsets)
|
||||
}
|
||||
|
||||
public func bma_blendWithColor(_ color: UIColor) -> UIImage {
|
||||
public func bma_blendWithColor(color: UIColor) -> UIImage {
|
||||
let rect = CGRect(origin: CGPoint.zero, size: self.size)
|
||||
UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.main.scale)
|
||||
UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.mainScreen().scale)
|
||||
let context = UIGraphicsGetCurrentContext()!
|
||||
context.translateBy(x: 0, y: rect.height)
|
||||
context.scaleBy(x: 1.0, y: -1.0)
|
||||
context.setBlendMode(.normal)
|
||||
context.draw(self.cgImage!, in: rect)
|
||||
context.clip(to: rect, mask: self.cgImage!)
|
||||
CGContextTranslateCTM(context, 0, rect.height)
|
||||
CGContextScaleCTM(context, 1.0, -1.0)
|
||||
CGContextSetBlendMode(context, .Normal)
|
||||
CGContextDrawImage(context, rect, self.CGImage!)
|
||||
CGContextClipToMask(context, rect, self.CGImage!)
|
||||
color.setFill()
|
||||
context.addRect(rect)
|
||||
context.drawPath(using: .fill)
|
||||
CGContextAddRect(context, rect)
|
||||
CGContextDrawPath(context, .Fill)
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()!
|
||||
UIGraphicsEndImageContext()
|
||||
return image.resizableImage(withCapInsets: self.capInsets)
|
||||
return image.resizableImageWithCapInsets(self.capInsets)
|
||||
}
|
||||
|
||||
public static func bma_imageWithColor(_ color: UIColor, size: CGSize) -> UIImage {
|
||||
public static func bma_imageWithColor(color: UIColor, size: CGSize) -> UIImage {
|
||||
let rect = CGRect(origin: CGPoint.zero, size: size)
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 0)
|
||||
color.setFill()
|
||||
|
|
@ -189,11 +191,11 @@ public extension UIImage {
|
|||
}
|
||||
|
||||
public extension UIColor {
|
||||
static func bma_color(rgb: Int) -> UIColor {
|
||||
static func bma_color(rgb rgb: Int) -> UIColor {
|
||||
return UIColor(red: CGFloat((rgb & 0xFF0000) >> 16) / 255.0, green: CGFloat((rgb & 0xFF00) >> 8) / 255.0, blue: CGFloat((rgb & 0xFF)) / 255.0, alpha: 1.0)
|
||||
}
|
||||
|
||||
public func bma_blendWithColor(_ color: UIColor) -> UIColor {
|
||||
public func bma_blendWithColor(color: UIColor) -> UIColor {
|
||||
var r1: CGFloat = 0, r2: CGFloat = 0
|
||||
var g1: CGFloat = 0, g2: CGFloat = 0
|
||||
var b1: CGFloat = 0, b2: CGFloat = 0
|
||||
|
|
|
|||
|
|
@ -33,12 +33,11 @@ class BaseMessagePresenterTests: XCTestCase {
|
|||
let decorationAttributes = ChatItemDecorationAttributes(bottomMargin: 0, showsTail: false, canShowAvatar: false)
|
||||
var interactionHandler: PhotoMessageTestHandler!
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
let viewModelBuilder = PhotoMessageViewModelDefaultBuilder<PhotoMessageModel<MessageModel>>()
|
||||
let sizingCell = PhotoMessageCollectionViewCell.sizingCell()
|
||||
let photoStyle = PhotoMessageCollectionViewCellDefaultStyle()
|
||||
let baseStyle = BaseMessageCollectionViewCellDefaultStyle()
|
||||
let messageModel = MessageModel(uid: "uid", senderId: "senderId", type: "photo-message", isIncoming: true, date: Date(), status: .success)
|
||||
let messageModel = MessageModel(uid: "uid", senderId: "senderId", type: "photo-message", isIncoming: true, date: NSDate(), status: .Success)
|
||||
let photoMessageModel = PhotoMessageModel(messageModel: messageModel, imageSize: CGSize(width: 30, height: 30), image: UIImage())
|
||||
self.interactionHandler = PhotoMessageTestHandler()
|
||||
self.presenter = PhotoMessagePresenter(messageModel: photoMessageModel, viewModelBuilder: viewModelBuilder, interactionHandler: self.interactionHandler, sizingCell: sizingCell, baseCellStyle: baseStyle, photoCellStyle: photoStyle)
|
||||
|
|
@ -61,14 +60,14 @@ class BaseMessagePresenterTests: XCTestCase {
|
|||
func testThat_WhenCellIsBeginLongPressOnBubble_ThenInteractionHandlerHandlesEvent() {
|
||||
let cell = PhotoMessageCollectionViewCell(frame: CGRect.zero)
|
||||
self.presenter.configureCell(cell, decorationAttributes: self.decorationAttributes)
|
||||
cell.onBubbleLongPressBegan?(cell)
|
||||
cell.onBubbleLongPressBegan?(cell: cell)
|
||||
XCTAssertTrue(self.interactionHandler.didHandleBeginLongPressOnBubble)
|
||||
}
|
||||
|
||||
func testThat_WhenCellIsEndLongPressOnBubble_ThenInteractionHandlerHandlesEvent() {
|
||||
let cell = PhotoMessageCollectionViewCell(frame: CGRect.zero)
|
||||
self.presenter.configureCell(cell, decorationAttributes: self.decorationAttributes)
|
||||
cell.onBubbleLongPressEnded?(cell)
|
||||
cell.onBubbleLongPressEnded?(cell: cell)
|
||||
XCTAssertTrue(self.interactionHandler.didHandleEndLongPressOnBubble)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import XCTest
|
|||
class PhotoMessagePresenterBuilderTests: XCTestCase {
|
||||
|
||||
func testThat_CreatesPresenter() {
|
||||
let messageModel = MessageModel(uid: "uid", senderId: "senderId", type: "photo-message", isIncoming: true, date: Date(), status: .success)
|
||||
let messageModel = MessageModel(uid: "uid", senderId: "senderId", type: "photo-message", isIncoming: true, date: NSDate(), status: .Success)
|
||||
let photoMessageModel = PhotoMessageModel(messageModel: messageModel, imageSize: CGSize(width: 30, height: 30), image: UIImage())
|
||||
let builder = PhotoMessagePresenterBuilder(viewModelBuilder: PhotoMessageViewModelDefaultBuilder<PhotoMessageModel<MessageModel>>(), interactionHandler: PhotoMessageTestHandler())
|
||||
XCTAssertNotNil(builder.createPresenterWithChatItem(photoMessageModel))
|
||||
|
|
|
|||
|
|
@ -31,12 +31,11 @@ class PhotoMessagePresenterTests: XCTestCase, UICollectionViewDataSource {
|
|||
let decorationAttributes = ChatItemDecorationAttributes(bottomMargin: 0, showsTail: false, canShowAvatar: false)
|
||||
let testImage = UIImage()
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
let viewModelBuilder = PhotoMessageViewModelDefaultBuilder<PhotoMessageModel<MessageModel>>()
|
||||
let sizingCell = PhotoMessageCollectionViewCell.sizingCell()
|
||||
let photoStyle = PhotoMessageCollectionViewCellDefaultStyle()
|
||||
let baseStyle = BaseMessageCollectionViewCellDefaultStyle()
|
||||
let messageModel = MessageModel(uid: "uid", senderId: "senderId", type: "photo-message", isIncoming: true, date: NSDate() as Date, status: .success)
|
||||
let messageModel = MessageModel(uid: "uid", senderId: "senderId", type: "photo-message", isIncoming: true, date: NSDate(), status: .Success)
|
||||
let photoMessageModel = PhotoMessageModel(messageModel: messageModel, imageSize: CGSize(width: 30, height: 30), image: self.testImage)
|
||||
self.presenter = PhotoMessagePresenter(messageModel: photoMessageModel, viewModelBuilder: viewModelBuilder, interactionHandler: PhotoMessageTestHandler(), sizingCell: sizingCell, baseCellStyle: baseStyle, photoCellStyle: photoStyle)
|
||||
}
|
||||
|
|
@ -61,18 +60,18 @@ class PhotoMessagePresenterTests: XCTestCase, UICollectionViewDataSource {
|
|||
PhotoMessagePresenter<PhotoMessageViewModelDefaultBuilder<PhotoMessageModel<MessageModel>>, PhotoMessageTestHandler>.registerCells(collectionView)
|
||||
collectionView.dataSource = self
|
||||
collectionView.reloadData()
|
||||
XCTAssertNotNil(self.presenter.dequeueCell(collectionView: collectionView, indexPath: IndexPath(item: 0, section: 0)))
|
||||
XCTAssertNotNil(self.presenter.dequeueCell(collectionView: collectionView, indexPath: NSIndexPath(forItem: 0, inSection: 0)))
|
||||
collectionView.dataSource = nil
|
||||
}
|
||||
|
||||
// MARK: Helpers
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
return self.presenter.dequeueCell(collectionView: collectionView, indexPath: indexPath as IndexPath)
|
||||
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
|
||||
return self.presenter.dequeueCell(collectionView: collectionView, indexPath: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,27 +79,27 @@ class PhotoMessageTestHandler: BaseMessageInteractionHandlerProtocol {
|
|||
typealias ViewModelT = PhotoMessageViewModel<PhotoMessageModel<MessageModel>>
|
||||
|
||||
var didHandleTapOnFailIcon = false
|
||||
func userDidTapOnFailIcon(viewModel: ViewModelT, failIconView: UIView) {
|
||||
func userDidTapOnFailIcon(viewModel viewModel: ViewModelT, failIconView: UIView) {
|
||||
self.didHandleTapOnFailIcon = true
|
||||
}
|
||||
|
||||
var didHandleTapOnAvatar = false
|
||||
func userDidTapOnAvatar(viewModel: ViewModelT) {
|
||||
func userDidTapOnAvatar(viewModel viewModel: ViewModelT) {
|
||||
self.didHandleTapOnAvatar = true
|
||||
}
|
||||
|
||||
var didHandleTapOnBubble = false
|
||||
func userDidTapOnBubble(viewModel: ViewModelT) {
|
||||
func userDidTapOnBubble(viewModel viewModel: ViewModelT) {
|
||||
self.didHandleTapOnBubble = true
|
||||
}
|
||||
|
||||
var didHandleBeginLongPressOnBubble = false
|
||||
func userDidBeginLongPressOnBubble(viewModel: ViewModelT) {
|
||||
func userDidBeginLongPressOnBubble(viewModel viewModel: ViewModelT) {
|
||||
self.didHandleBeginLongPressOnBubble = true
|
||||
}
|
||||
|
||||
var didHandleEndLongPressOnBubble = false
|
||||
func userDidEndLongPressOnBubble(viewModel: ViewModelT) {
|
||||
func userDidEndLongPressOnBubble(viewModel viewModel: ViewModelT) {
|
||||
self.didHandleEndLongPressOnBubble = true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import XCTest
|
|||
class TextMessagePresenterBuilderTests: XCTestCase {
|
||||
|
||||
func testThat_CreatesPresenter() {
|
||||
let messageModel = MessageModel(uid: "uid", senderId: "senderId", type: "text-message", isIncoming: true, date: Date(), status: .success)
|
||||
let messageModel = MessageModel(uid: "uid", senderId: "senderId", type: "text-message", isIncoming: true, date: NSDate(), status: .Success)
|
||||
let textMessageModel = TextMessageModel(messageModel: messageModel, text: "Some text")
|
||||
let builder = TextMessagePresenterBuilder(viewModelBuilder: TextMessageViewModelDefaultBuilder<TextMessageModel<MessageModel>>(), interactionHandler: TextMessageTestHandler())
|
||||
XCTAssertNotNil(builder.createPresenterWithChatItem(textMessageModel))
|
||||
|
|
|
|||
|
|
@ -31,12 +31,11 @@ class TextMessagePresenterTests: XCTestCase, UICollectionViewDataSource {
|
|||
var presenter: TextMessagePresenter<TextMessageViewModelDefaultBuilder<TextMessageModel<MessageModel>>, TextMessageTestHandler>!
|
||||
let decorationAttributes = ChatItemDecorationAttributes(bottomMargin: 0, showsTail: false, canShowAvatar: false)
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
let viewModelBuilder = TextMessageViewModelDefaultBuilder<TextMessageModel<MessageModel>>()
|
||||
let sizingCell = TextMessageCollectionViewCell.sizingCell()
|
||||
let textStyle = TextMessageCollectionViewCellDefaultStyle()
|
||||
let baseStyle = BaseMessageCollectionViewCellDefaultStyle()
|
||||
let messageModel = MessageModel(uid: "uid", senderId: "senderId", type: "text-message", isIncoming: true, date: NSDate() as Date, status: .success)
|
||||
let messageModel = MessageModel(uid: "uid", senderId: "senderId", type: "text-message", isIncoming: true, date: NSDate(), status: .Success)
|
||||
let textMessageModel = TextMessageModel(messageModel: messageModel, text: "Some text")
|
||||
self.presenter = TextMessagePresenter(messageModel: textMessageModel, viewModelBuilder: viewModelBuilder, interactionHandler: TextMessageTestHandler(), sizingCell: sizingCell, baseCellStyle: baseStyle, textCellStyle: textStyle, layoutCache: NSCache())
|
||||
}
|
||||
|
|
@ -47,7 +46,7 @@ class TextMessagePresenterTests: XCTestCase, UICollectionViewDataSource {
|
|||
TextMessagePresenter<TextMessageViewModelDefaultBuilder<TextMessageModel<MessageModel>>, TextMessageTestHandler>.registerCells(collectionView)
|
||||
collectionView.dataSource = self
|
||||
collectionView.reloadData()
|
||||
XCTAssertNotNil(self.presenter.dequeueCell(collectionView: collectionView, indexPath: IndexPath(item: 0, section: 0)))
|
||||
XCTAssertNotNil(self.presenter.dequeueCell(collectionView: collectionView, indexPath: NSIndexPath(forItem: 0, inSection: 0)))
|
||||
collectionView.dataSource = nil
|
||||
}
|
||||
|
||||
|
|
@ -82,35 +81,35 @@ class TextMessagePresenterTests: XCTestCase, UICollectionViewDataSource {
|
|||
|
||||
// MARK: Helpers
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
return self.presenter.dequeueCell(collectionView: collectionView, indexPath: indexPath as IndexPath)
|
||||
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
|
||||
return self.presenter.dequeueCell(collectionView: collectionView, indexPath: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
class TextMessageTestHandler: BaseMessageInteractionHandlerProtocol {
|
||||
typealias ViewModelT = TextMessageViewModel<TextMessageModel<MessageModel>>
|
||||
|
||||
func userDidTapOnFailIcon(viewModel: ViewModelT, failIconView: UIView) {
|
||||
func userDidTapOnFailIcon(viewModel viewModel: ViewModelT, failIconView: UIView) {
|
||||
|
||||
}
|
||||
|
||||
func userDidTapOnAvatar(viewModel: ViewModelT) {
|
||||
func userDidTapOnAvatar(viewModel viewModel: ViewModelT) {
|
||||
|
||||
}
|
||||
|
||||
func userDidTapOnBubble(viewModel: ViewModelT) {
|
||||
func userDidTapOnBubble(viewModel viewModel: ViewModelT) {
|
||||
|
||||
}
|
||||
|
||||
func userDidBeginLongPressOnBubble(viewModel: ViewModelT) {
|
||||
func userDidBeginLongPressOnBubble(viewModel viewModel: ViewModelT) {
|
||||
|
||||
}
|
||||
|
||||
func userDidEndLongPressOnBubble(viewModel: ViewModelT) {
|
||||
func userDidEndLongPressOnBubble(viewModel viewModel: ViewModelT) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>3.0.1</string>
|
||||
<string>2.1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class ChatInputBarTests: XCTestCase {
|
|||
return itemView
|
||||
}
|
||||
|
||||
private func simulateTapOnTextViewForDelegate(_ textViewDelegate: UITextViewDelegate) {
|
||||
private func simulateTapOnTextViewForDelegate(textViewDelegate: UITextViewDelegate) {
|
||||
let dummyTextView = UITextView()
|
||||
let shouldBeginEditing = textViewDelegate.textViewShouldBeginEditing?(dummyTextView) ?? true
|
||||
guard shouldBeginEditing else { return }
|
||||
|
|
@ -57,22 +57,22 @@ class ChatInputBarTests: XCTestCase {
|
|||
}
|
||||
|
||||
func testThat_WhenInputTextChanged_BarEnablesSendButton() {
|
||||
self.bar.sendButton.isEnabled = false
|
||||
self.bar.sendButton.enabled = false
|
||||
self.bar.inputText = "!"
|
||||
XCTAssertTrue(self.bar.sendButton.isEnabled)
|
||||
XCTAssertTrue(self.bar.sendButton.enabled)
|
||||
}
|
||||
|
||||
func testThat_WhenInputTextBecomesEmpty_BarDisablesSendButton() {
|
||||
self.bar.sendButton.isEnabled = true
|
||||
self.bar.sendButton.enabled = true
|
||||
self.bar.inputText = ""
|
||||
XCTAssertFalse(self.bar.sendButton.isEnabled)
|
||||
XCTAssertFalse(self.bar.sendButton.enabled)
|
||||
}
|
||||
|
||||
// MARK: - Presenter tests
|
||||
func testThat_WhenItemViewTapped_ItNotifiesPresenterThatNewItemReceivedFocus() {
|
||||
self.setupPresenter()
|
||||
let item = MockInputItem()
|
||||
self.bar.inputItemViewTapped(createItemView(inputItem: item))
|
||||
self.bar.inputItemViewTapped(createItemView(item))
|
||||
|
||||
XCTAssertTrue(self.presenter.onDidReceiveFocusOnItemCalled)
|
||||
XCTAssertTrue(self.presenter.itemThatReceivedFocus === item)
|
||||
|
|
@ -91,21 +91,21 @@ class ChatInputBarTests: XCTestCase {
|
|||
}
|
||||
|
||||
func testThat_GivenTextViewHasNoText_WhenTextViewDidChange_ItDisablesSendButton() {
|
||||
self.bar.sendButton.isEnabled = true
|
||||
self.bar.sendButton.enabled = true
|
||||
|
||||
self.bar.textView.text = ""
|
||||
self.bar.textViewDidChange(self.bar.textView)
|
||||
|
||||
XCTAssertFalse(self.bar.sendButton.isEnabled)
|
||||
XCTAssertFalse(self.bar.sendButton.enabled)
|
||||
}
|
||||
|
||||
func testThat_WhenTextViewDidChange_ItEnablesSendButton() {
|
||||
self.bar.sendButton.isEnabled = false
|
||||
self.bar.sendButton.enabled = false
|
||||
|
||||
self.bar.textView.text = "!"
|
||||
self.bar.textViewDidChange(self.bar.textView)
|
||||
|
||||
XCTAssertTrue(self.bar.sendButton.isEnabled)
|
||||
XCTAssertTrue(self.bar.sendButton.enabled)
|
||||
}
|
||||
|
||||
func testThat_WhenSendButtonTapped_ItNotifiesPresenter() {
|
||||
|
|
@ -118,7 +118,7 @@ class ChatInputBarTests: XCTestCase {
|
|||
func testThat_WhenItemViewTapped_ItNotifiesDelegateThatNewItemReceivedFocus() {
|
||||
self.setupDelegate()
|
||||
let item = MockInputItem()
|
||||
self.bar.inputItemViewTapped(createItemView(inputItem: item))
|
||||
self.bar.inputItemViewTapped(createItemView(item))
|
||||
|
||||
XCTAssertTrue(self.delegate.inputBarDidReceiveFocusOnItemCalled)
|
||||
XCTAssertTrue(self.delegate.focusedItem === item)
|
||||
|
|
@ -158,14 +158,14 @@ class ChatInputBarTests: XCTestCase {
|
|||
self.bar.inputText = " "
|
||||
self.bar.textViewDidChange(self.bar.textView)
|
||||
XCTAssertTrue(closureCalled)
|
||||
XCTAssertFalse(self.bar.sendButton.isEnabled)
|
||||
XCTAssertFalse(self.bar.sendButton.enabled)
|
||||
}
|
||||
|
||||
func testThat_WhenItemViewTapped_ItReceivesFocuesByDefault() {
|
||||
self.setupPresenter()
|
||||
|
||||
let item = MockInputItem()
|
||||
self.bar.inputItemViewTapped(createItemView(inputItem: item))
|
||||
self.bar.inputItemViewTapped(createItemView(item))
|
||||
|
||||
XCTAssertTrue(self.presenter.onDidReceiveFocusOnItemCalled)
|
||||
XCTAssertTrue(self.presenter.itemThatReceivedFocus === item)
|
||||
|
|
@ -176,7 +176,7 @@ class ChatInputBarTests: XCTestCase {
|
|||
self.delegate.inputBarShouldFocusOnItemResult = true
|
||||
|
||||
let item = MockInputItem()
|
||||
self.bar.inputItemViewTapped(createItemView(inputItem: item))
|
||||
self.bar.inputItemViewTapped(createItemView(item))
|
||||
|
||||
XCTAssertTrue(self.delegate.inputBarShouldFocusOnItemCalled)
|
||||
XCTAssertTrue(self.delegate.inputBarDidReceiveFocusOnItemCalled)
|
||||
|
|
@ -188,7 +188,7 @@ class ChatInputBarTests: XCTestCase {
|
|||
self.delegate.inputBarShouldFocusOnItemResult = false
|
||||
|
||||
let item = MockInputItem()
|
||||
self.bar.inputItemViewTapped(createItemView(inputItem: item))
|
||||
self.bar.inputItemViewTapped(createItemView(item))
|
||||
|
||||
XCTAssertTrue(self.delegate.inputBarShouldFocusOnItemCalled)
|
||||
XCTAssertFalse(self.delegate.inputBarDidReceiveFocusOnItemCalled)
|
||||
|
|
@ -241,7 +241,7 @@ class FakeChatInputBarPresenter: ChatInputBarPresenter {
|
|||
|
||||
var onDidReceiveFocusOnItemCalled = false
|
||||
var itemThatReceivedFocus: ChatInputItemProtocol?
|
||||
func onDidReceiveFocusOnItem(_ item: ChatInputItemProtocol) {
|
||||
func onDidReceiveFocusOnItem(item: ChatInputItemProtocol) {
|
||||
self.onDidReceiveFocusOnItemCalled = true
|
||||
self.itemThatReceivedFocus = item
|
||||
}
|
||||
|
|
@ -250,41 +250,41 @@ class FakeChatInputBarPresenter: ChatInputBarPresenter {
|
|||
class FakeChatInputBarDelegate: ChatInputBarDelegate {
|
||||
var inputBarShouldBeginTextEditingCalled = false
|
||||
var inputBarShouldBeginTextEditingResult = true
|
||||
func inputBarShouldBeginTextEditing(_ inputBar: ChatInputBar) -> Bool {
|
||||
func inputBarShouldBeginTextEditing(inputBar: ChatInputBar) -> Bool {
|
||||
self.inputBarShouldBeginTextEditingCalled = true
|
||||
return self.inputBarShouldBeginTextEditingResult
|
||||
}
|
||||
|
||||
var inputBarDidBeginEditingCalled = false
|
||||
func inputBarDidBeginEditing(_ inputBar: ChatInputBar) {
|
||||
func inputBarDidBeginEditing(inputBar: ChatInputBar) {
|
||||
self.inputBarDidBeginEditingCalled = true
|
||||
}
|
||||
|
||||
var inputBarDidEndEditingCalled = false
|
||||
func inputBarDidEndEditing(_ inputBar: ChatInputBar) {
|
||||
func inputBarDidEndEditing(inputBar: ChatInputBar) {
|
||||
self.inputBarDidEndEditingCalled = true
|
||||
}
|
||||
|
||||
var inputBarDidChangeTextCalled = false
|
||||
func inputBarDidChangeText(_ inputBar: ChatInputBar) {
|
||||
func inputBarDidChangeText(inputBar: ChatInputBar) {
|
||||
self.inputBarDidChangeTextCalled = true
|
||||
}
|
||||
|
||||
var inputBarSendButtonPressedCalled = false
|
||||
func inputBarSendButtonPressed(_ inputBar: ChatInputBar) {
|
||||
func inputBarSendButtonPressed(inputBar: ChatInputBar) {
|
||||
self.inputBarSendButtonPressedCalled = true
|
||||
}
|
||||
|
||||
var inputBarShouldFocusOnItemCalled = false
|
||||
var inputBarShouldFocusOnItemResult = true
|
||||
func inputBar(_ inputBar: ChatInputBar, shouldFocusOnItem item: ChatInputItemProtocol) -> Bool {
|
||||
func inputBar(inputBar: ChatInputBar, shouldFocusOnItem item: ChatInputItemProtocol) -> Bool {
|
||||
self.inputBarShouldFocusOnItemCalled = true
|
||||
return self.inputBarShouldFocusOnItemResult
|
||||
}
|
||||
|
||||
var inputBarDidReceiveFocusOnItemCalled = false
|
||||
var focusedItem: ChatInputItemProtocol?
|
||||
func inputBar(_ inputBar: ChatInputBar, didReceiveFocusOnItem item: ChatInputItemProtocol) {
|
||||
func inputBar(inputBar: ChatInputBar, didReceiveFocusOnItem item: ChatInputItemProtocol) {
|
||||
self.inputBarDidReceiveFocusOnItemCalled = true
|
||||
self.focusedItem = item
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,13 +31,13 @@ class ChatInputItemTests: XCTestCase {
|
|||
@objc
|
||||
class MockInputItem: NSObject, ChatInputItemProtocol {
|
||||
var selected = false
|
||||
var presentationMode: ChatInputItemPresentationMode = .keyboard
|
||||
var presentationMode: ChatInputItemPresentationMode = .Keyboard
|
||||
var showsSendButton = false
|
||||
var inputView: UIView? = nil
|
||||
let tabView = UIView()
|
||||
|
||||
private(set) var handledInput: AnyObject?
|
||||
func handleInput(_ input: AnyObject) {
|
||||
func handleInput(input: AnyObject) {
|
||||
self.handledInput = input
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class ChatInputItemViewTests: XCTestCase {
|
|||
|
||||
class MockInputItemViewDelegate: ChatInputItemViewDelegate {
|
||||
var itemViewTapped = false
|
||||
func inputItemViewTapped(_ view: ChatInputItemView) {
|
||||
func inputItemViewTapped(view: ChatInputItemView) {
|
||||
self.itemViewTapped = true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ class ChatInputPresenterTests: XCTestCase {
|
|||
|
||||
func testThat_GivenItemHasNonePresentationMode_WhenItemReceivesFocus_ItDoesntBecomeFocused() {
|
||||
let item = MockInputItem()
|
||||
item.presentationMode = .none
|
||||
item.presentationMode = .None
|
||||
self.presenter.onDidReceiveFocusOnItem(item)
|
||||
XCTAssertNil(self.presenter.focusedItem)
|
||||
}
|
||||
|
|
@ -95,7 +95,7 @@ class ChatInputPresenterTests: XCTestCase {
|
|||
func testThat_GivenItemHasKeyboardPresentationMode_WhenItemReceivesFocus_PresenterShowsTextView() {
|
||||
self.bar.showsTextView = false
|
||||
let item = MockInputItem()
|
||||
item.presentationMode = .keyboard
|
||||
item.presentationMode = .Keyboard
|
||||
self.presenter.onDidReceiveFocusOnItem(item)
|
||||
XCTAssertTrue(self.bar.showsTextView)
|
||||
}
|
||||
|
|
@ -103,7 +103,7 @@ class ChatInputPresenterTests: XCTestCase {
|
|||
func testThat_GivenItemHasCustomViewPresentationMode_WhenItemReceivesFocus_PresenterHidesTextView() {
|
||||
self.bar.showsTextView = true
|
||||
let item = MockInputItem()
|
||||
item.presentationMode = .customView
|
||||
item.presentationMode = .CustomView
|
||||
self.presenter.onDidReceiveFocusOnItem(item)
|
||||
XCTAssertFalse(self.bar.showsTextView)
|
||||
}
|
||||
|
|
@ -111,7 +111,7 @@ class ChatInputPresenterTests: XCTestCase {
|
|||
func testThat_GivenItemHasNonePresentationMode_WhenItemReceivesFocus_PresenterDoesntHideTextView() {
|
||||
self.bar.showsTextView = true
|
||||
let item = MockInputItem()
|
||||
item.presentationMode = .none
|
||||
item.presentationMode = .None
|
||||
self.presenter.onDidReceiveFocusOnItem(item)
|
||||
XCTAssertTrue(self.bar.showsTextView)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class LiveCameraCellPresenterTests: XCTestCase {
|
|||
|
||||
var presenter: LiveCameraCellPresenter!
|
||||
var cell: LiveCameraCell!
|
||||
var cameraAuthorizationStatus: AVAuthorizationStatus = .notDetermined
|
||||
var cameraAuthorizationStatus: AVAuthorizationStatus = .NotDetermined
|
||||
var cameraAuthorizationStatusProvider: LiveCameraCellPresenter.AVAuthorizationStatusProvider!
|
||||
|
||||
override func setUp() {
|
||||
|
|
@ -38,7 +38,7 @@ class LiveCameraCellPresenterTests: XCTestCase {
|
|||
self.cameraAuthorizationStatusProvider = { [unowned self] in
|
||||
return self.cameraAuthorizationStatus
|
||||
}
|
||||
self.presenter = LiveCameraCellPresenter(authorizationStatusProvider: self.cameraAuthorizationStatusProvider)
|
||||
self.presenter = LiveCameraCellPresenter(cellAppearance: nil, authorizationStatusProvider: self.cameraAuthorizationStatusProvider)
|
||||
self.cell = LiveCameraCell()
|
||||
}
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ class LiveCameraCellPresenterTests: XCTestCase {
|
|||
func testThat_WhenAuthorizationStatusIsNotDetermined_CaptureDoesntStart() {
|
||||
let mockCaptureSession = MockLiveCameraCaptureSession()
|
||||
self.presenter.captureSession = mockCaptureSession
|
||||
self.cameraAuthorizationStatus = .notDetermined
|
||||
self.cameraAuthorizationStatus = .NotDetermined
|
||||
|
||||
self.presenter.cellWillBeShown(self.cell)
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ class LiveCameraCellPresenterTests: XCTestCase {
|
|||
func testThat_WhenAuthorizationStatusIsRestricted_CaptureDoesntStart() {
|
||||
let mockCaptureSession = MockLiveCameraCaptureSession()
|
||||
self.presenter.captureSession = mockCaptureSession
|
||||
self.cameraAuthorizationStatus = .restricted
|
||||
self.cameraAuthorizationStatus = .Restricted
|
||||
|
||||
self.presenter.cellWillBeShown(self.cell)
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ class LiveCameraCellPresenterTests: XCTestCase {
|
|||
func testThat_WhenAuthorizationStatusIsDenied_CaptureDoesntStart() {
|
||||
let mockCaptureSession = MockLiveCameraCaptureSession()
|
||||
self.presenter.captureSession = mockCaptureSession
|
||||
self.cameraAuthorizationStatus = .denied
|
||||
self.cameraAuthorizationStatus = .Denied
|
||||
|
||||
self.presenter.cellWillBeShown(self.cell)
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ class LiveCameraCellPresenterTests: XCTestCase {
|
|||
func testThat_WhenAuthorizationStatusIsAuthorized_CaptureStarts() {
|
||||
let mockCaptureSession = MockLiveCameraCaptureSession()
|
||||
self.presenter.captureSession = mockCaptureSession
|
||||
self.cameraAuthorizationStatus = .authorized
|
||||
self.cameraAuthorizationStatus = .Authorized
|
||||
|
||||
self.presenter.cellWillBeShown(self.cell)
|
||||
|
||||
|
|
@ -94,7 +94,7 @@ class LiveCameraCellPresenterTests: XCTestCase {
|
|||
let mockCaptureSession = MockLiveCameraCaptureSession()
|
||||
mockCaptureSession.isCapturing = true
|
||||
self.presenter.captureSession = mockCaptureSession
|
||||
self.cameraAuthorizationStatus = .authorized
|
||||
self.cameraAuthorizationStatus = .Authorized
|
||||
|
||||
self.presenter.cellWillBeShown(self.cell)
|
||||
self.presenter.cellWasHidden(self.cell)
|
||||
|
|
@ -108,10 +108,10 @@ class LiveCameraCellPresenterTests: XCTestCase {
|
|||
mockCaptureSession.isCapturing = true
|
||||
self.presenter.captureSession = mockCaptureSession
|
||||
|
||||
self.cameraAuthorizationStatus = .authorized
|
||||
self.cameraAuthorizationStatus = .Authorized
|
||||
self.presenter.cellWillBeShown(self.cell)
|
||||
|
||||
self.presenter.notificationCenter.post(name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
|
||||
self.presenter.notificationCenter.postNotificationName(UIApplicationWillResignActiveNotification, object: nil)
|
||||
|
||||
XCTAssertFalse(mockCaptureSession.isCapturing)
|
||||
}
|
||||
|
|
@ -121,11 +121,11 @@ class LiveCameraCellPresenterTests: XCTestCase {
|
|||
mockCaptureSession.isCapturing = true
|
||||
self.presenter.captureSession = mockCaptureSession
|
||||
|
||||
self.cameraAuthorizationStatus = .authorized
|
||||
self.cameraAuthorizationStatus = .Authorized
|
||||
self.presenter.cellWillBeShown(self.cell)
|
||||
|
||||
self.presenter.notificationCenter.post(name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
|
||||
self.presenter.notificationCenter.post(name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)
|
||||
self.presenter.notificationCenter.postNotificationName(UIApplicationWillResignActiveNotification, object: nil)
|
||||
self.presenter.notificationCenter.postNotificationName(UIApplicationDidBecomeActiveNotification, object: nil)
|
||||
|
||||
XCTAssertTrue(mockCaptureSession.isCapturing)
|
||||
}
|
||||
|
|
@ -135,12 +135,12 @@ class LiveCameraCellPresenterTests: XCTestCase {
|
|||
mockCaptureSession.isCapturing = false
|
||||
self.presenter.captureSession = mockCaptureSession
|
||||
|
||||
self.cameraAuthorizationStatus = .authorized
|
||||
self.cameraAuthorizationStatus = .Authorized
|
||||
self.presenter.cellWillBeShown(self.cell)
|
||||
self.presenter.cellWasHidden(self.cell)
|
||||
|
||||
self.presenter.notificationCenter.post(name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
|
||||
self.presenter.notificationCenter.post(name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)
|
||||
self.presenter.notificationCenter.postNotificationName(UIApplicationWillResignActiveNotification, object: nil)
|
||||
self.presenter.notificationCenter.postNotificationName(UIApplicationDidBecomeActiveNotification, object: nil)
|
||||
|
||||
XCTAssertFalse(mockCaptureSession.isCapturing)
|
||||
}
|
||||
|
|
@ -148,7 +148,7 @@ class LiveCameraCellPresenterTests: XCTestCase {
|
|||
func testThat_WhenCellIsRemovedFromWindow_ThenCaptureIsStopped() {
|
||||
let mockCaptureSession = MockLiveCameraCaptureSession()
|
||||
self.presenter.captureSession = mockCaptureSession
|
||||
self.cameraAuthorizationStatus = .authorized
|
||||
self.cameraAuthorizationStatus = .Authorized
|
||||
|
||||
self.presenter.cellWillBeShown(self.cell)
|
||||
|
||||
|
|
@ -159,7 +159,7 @@ class LiveCameraCellPresenterTests: XCTestCase {
|
|||
func testThat_WhenReusedCellIsRemovedFromWindow_ThenCaptureIsNotStopped() {
|
||||
let mockCaptureSession = MockLiveCameraCaptureSession()
|
||||
self.presenter.captureSession = mockCaptureSession
|
||||
self.cameraAuthorizationStatus = .authorized
|
||||
self.cameraAuthorizationStatus = .Authorized
|
||||
|
||||
let firstCell = LiveCameraCell()
|
||||
self.presenter.cellWillBeShown(firstCell)
|
||||
|
|
@ -172,7 +172,7 @@ class LiveCameraCellPresenterTests: XCTestCase {
|
|||
func testThat_WhenCellIsReaddedToWindow_ThenCaputreIsRestarted() {
|
||||
let mockCaptureSession = MockLiveCameraCaptureSession()
|
||||
self.presenter.captureSession = mockCaptureSession
|
||||
self.cameraAuthorizationStatus = .authorized
|
||||
self.cameraAuthorizationStatus = .Authorized
|
||||
|
||||
self.presenter.cellWillBeShown(self.cell)
|
||||
self.cell.didMoveToWindow()
|
||||
|
|
@ -186,14 +186,14 @@ class LiveCameraCellPresenterTests: XCTestCase {
|
|||
func testThat_WhenReusedCellIsReaddedToWindow_ThenCaptureIsNotRestarted() {
|
||||
let mockCaptureSession = MockLiveCameraCaptureSession()
|
||||
self.presenter.captureSession = mockCaptureSession
|
||||
self.cameraAuthorizationStatus = .authorized
|
||||
self.cameraAuthorizationStatus = .Authorized
|
||||
|
||||
let firstCell = LiveCameraCell()
|
||||
self.presenter.cellWillBeShown(firstCell)
|
||||
self.presenter.cellWillBeShown(self.cell)
|
||||
self.cell.didMoveToWindow()
|
||||
|
||||
firstCell.willMove(toWindow: UIWindow())
|
||||
firstCell.willMoveToWindow(UIWindow())
|
||||
XCTAssertFalse(mockCaptureSession.isCapturing)
|
||||
}
|
||||
}
|
||||
|
|
@ -209,7 +209,7 @@ private class MockLiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
|
|||
var isInitialized: Bool = false
|
||||
var isCapturing: Bool = false
|
||||
|
||||
func startCapturing(_ completion: @escaping () -> Void) {
|
||||
func startCapturing(completion: () -> Void) {
|
||||
guard !self.isCapturing else { return }
|
||||
|
||||
self.isInitialized = true
|
||||
|
|
@ -217,7 +217,7 @@ private class MockLiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
|
|||
completion()
|
||||
}
|
||||
|
||||
func stopCapturing(_ completion: @escaping () -> Void) {
|
||||
func stopCapturing(completion: () -> Void) {
|
||||
guard self.isCapturing else { return }
|
||||
|
||||
self.isInitialized = true
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class PhotosChatInputItemTests: XCTestCase {
|
|||
}
|
||||
|
||||
func testThat_PresentationModeIsCustomView() {
|
||||
XCTAssertEqual(self.inputItem.presentationMode, ChatInputItemPresentationMode.customView)
|
||||
XCTAssertEqual(self.inputItem.presentationMode, ChatInputItemPresentationMode.CustomView)
|
||||
}
|
||||
|
||||
func testThat_SendButtonDisabledForPhotosInputItem() {
|
||||
|
|
@ -54,7 +54,7 @@ class PhotosChatInputItemTests: XCTestCase {
|
|||
self.inputItem.photoInputHandler = { image in
|
||||
handled = true
|
||||
}
|
||||
self.inputItem.handleInput(5 as AnyObject)
|
||||
self.inputItem.handleInput(5)
|
||||
XCTAssertFalse(handled)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class TextChatInputItemTests: XCTestCase {
|
|||
self.inputItem.textInputHandler = { text in
|
||||
handled = true
|
||||
}
|
||||
self.inputItem.handleInput("text" as AnyObject)
|
||||
self.inputItem.handleInput("text")
|
||||
XCTAssertTrue(handled)
|
||||
}
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ class TextChatInputItemTests: XCTestCase {
|
|||
self.inputItem.textInputHandler = { text in
|
||||
handled = true
|
||||
}
|
||||
self.inputItem.handleInput(5 as AnyObject)
|
||||
self.inputItem.handleInput(5)
|
||||
XCTAssertFalse(handled)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
C33FBFAE1BDE441C008E3545 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C33FBFAC1BDE441C008E3545 /* Main.storyboard */; };
|
||||
C33FBFB01BDE441C008E3545 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C33FBFAF1BDE441C008E3545 /* Assets.xcassets */; };
|
||||
C33FBFB31BDE441C008E3545 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C33FBFB11BDE441C008E3545 /* LaunchScreen.storyboard */; };
|
||||
C33FBFC91BDE441C008E3545 /* ChattoAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FBFC81BDE441C008E3545 /* ChattoAppUITests.swift */; };
|
||||
C341D42E1C9635DF00FD3463 /* TimeSeparatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C341D42B1C9635DF00FD3463 /* TimeSeparatorModel.swift */; };
|
||||
C341D42F1C9635DF00FD3463 /* TimeSeparatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C341D42C1C9635DF00FD3463 /* TimeSeparatorPresenter.swift */; };
|
||||
C341D4301C9635DF00FD3463 /* TimeSeparatorCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C341D42D1C9635DF00FD3463 /* TimeSeparatorCollectionViewCell.swift */; };
|
||||
|
|
@ -44,6 +45,13 @@
|
|||
remoteGlobalIDString = C33FBFA41BDE441C008E3545;
|
||||
remoteInfo = ChattoApp;
|
||||
};
|
||||
C33FBFC51BDE441C008E3545 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = C33FBF9D1BDE441C008E3545 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = C33FBFA41BDE441C008E3545;
|
||||
remoteInfo = ChattoApp;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
|
|
@ -71,6 +79,9 @@
|
|||
C33FBFB41BDE441C008E3545 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
C33FBFB91BDE441C008E3545 /* ChattoAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ChattoAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C33FBFBF1BDE441C008E3545 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
C33FBFC41BDE441C008E3545 /* ChattoAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ChattoAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C33FBFC81BDE441C008E3545 /* ChattoAppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChattoAppUITests.swift; sourceTree = "<group>"; };
|
||||
C33FBFCA1BDE441C008E3545 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
C341D42B1C9635DF00FD3463 /* TimeSeparatorModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TimeSeparatorModel.swift; path = "Time Separator/TimeSeparatorModel.swift"; sourceTree = "<group>"; };
|
||||
C341D42C1C9635DF00FD3463 /* TimeSeparatorPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TimeSeparatorPresenter.swift; path = "Time Separator/TimeSeparatorPresenter.swift"; sourceTree = "<group>"; };
|
||||
C341D42D1C9635DF00FD3463 /* TimeSeparatorCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TimeSeparatorCollectionViewCell.swift; path = "Time Separator/TimeSeparatorCollectionViewCell.swift"; sourceTree = "<group>"; };
|
||||
|
|
@ -112,6 +123,13 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
C33FBFC11BDE441C008E3545 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
|
|
@ -138,6 +156,7 @@
|
|||
children = (
|
||||
C33FBFA71BDE441C008E3545 /* ChattoApp */,
|
||||
C33FBFBC1BDE441C008E3545 /* ChattoAppTests */,
|
||||
C33FBFC71BDE441C008E3545 /* ChattoAppUITests */,
|
||||
C33FBFA61BDE441C008E3545 /* Products */,
|
||||
0852C8B139C7CFA0A1C22090 /* Frameworks */,
|
||||
B616EDF620454A787C7E7D84 /* Pods */,
|
||||
|
|
@ -149,6 +168,7 @@
|
|||
children = (
|
||||
C33FBFA51BDE441C008E3545 /* ChattoApp.app */,
|
||||
C33FBFB91BDE441C008E3545 /* ChattoAppTests.xctest */,
|
||||
C33FBFC41BDE441C008E3545 /* ChattoAppUITests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -174,6 +194,15 @@
|
|||
path = ChattoAppTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C33FBFC71BDE441C008E3545 /* ChattoAppUITests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C33FBFC81BDE441C008E3545 /* ChattoAppUITests.swift */,
|
||||
C33FBFCA1BDE441C008E3545 /* Info.plist */,
|
||||
);
|
||||
path = ChattoAppUITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C341D42A1C96359000FD3463 /* Time Separator */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -278,6 +307,24 @@
|
|||
productReference = C33FBFB91BDE441C008E3545 /* ChattoAppTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
C33FBFC31BDE441C008E3545 /* ChattoAppUITests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = C33FBFD31BDE441C008E3545 /* Build configuration list for PBXNativeTarget "ChattoAppUITests" */;
|
||||
buildPhases = (
|
||||
C33FBFC01BDE441C008E3545 /* Sources */,
|
||||
C33FBFC11BDE441C008E3545 /* Frameworks */,
|
||||
C33FBFC21BDE441C008E3545 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
C33FBFC61BDE441C008E3545 /* PBXTargetDependency */,
|
||||
);
|
||||
name = ChattoAppUITests;
|
||||
productName = ChattoAppUITests;
|
||||
productReference = C33FBFC41BDE441C008E3545 /* ChattoAppUITests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.ui-testing";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
|
|
@ -290,11 +337,13 @@
|
|||
TargetAttributes = {
|
||||
C33FBFA41BDE441C008E3545 = {
|
||||
CreatedOnToolsVersion = 7.1;
|
||||
LastSwiftMigration = 0800;
|
||||
};
|
||||
C33FBFB81BDE441C008E3545 = {
|
||||
CreatedOnToolsVersion = 7.1;
|
||||
LastSwiftMigration = 0800;
|
||||
TestTargetID = C33FBFA41BDE441C008E3545;
|
||||
};
|
||||
C33FBFC31BDE441C008E3545 = {
|
||||
CreatedOnToolsVersion = 7.1;
|
||||
TestTargetID = C33FBFA41BDE441C008E3545;
|
||||
};
|
||||
};
|
||||
|
|
@ -314,6 +363,7 @@
|
|||
targets = (
|
||||
C33FBFA41BDE441C008E3545 /* ChattoApp */,
|
||||
C33FBFB81BDE441C008E3545 /* ChattoAppTests */,
|
||||
C33FBFC31BDE441C008E3545 /* ChattoAppUITests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
|
@ -337,6 +387,13 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
C33FBFC21BDE441C008E3545 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
|
|
@ -380,7 +437,7 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n";
|
||||
shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
F8D7533B1E7B2E137B143EBD /* [CP] Copy Pods Resources */ = {
|
||||
|
|
@ -437,6 +494,14 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
C33FBFC01BDE441C008E3545 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C33FBFC91BDE441C008E3545 /* ChattoAppUITests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
|
|
@ -445,6 +510,11 @@
|
|||
target = C33FBFA41BDE441C008E3545 /* ChattoApp */;
|
||||
targetProxy = C33FBFBA1BDE441C008E3545 /* PBXContainerItemProxy */;
|
||||
};
|
||||
C33FBFC61BDE441C008E3545 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = C33FBFA41BDE441C008E3545 /* ChattoApp */;
|
||||
targetProxy = C33FBFC51BDE441C008E3545 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
|
|
@ -470,7 +540,6 @@
|
|||
C33FBFCB1BDE441C008E3545 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
|
|
@ -511,7 +580,7 @@
|
|||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 3.0;
|
||||
SWIFT_VERSION = 2.3;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
|
|
@ -519,7 +588,6 @@
|
|||
C33FBFCC1BDE441C008E3545 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
|
|
@ -553,7 +621,7 @@
|
|||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 3.0;
|
||||
SWIFT_VERSION = 2.3;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
|
|
@ -563,6 +631,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 2CCB5DAFC70B636492325895 /* Pods-ChattoApp.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||
INFOPLIST_FILE = ChattoApp/Info.plist;
|
||||
|
|
@ -576,6 +645,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 6DABD92E2BA40464C1727DA2 /* Pods-ChattoApp.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||
INFOPLIST_FILE = ChattoApp/Info.plist;
|
||||
|
|
@ -609,6 +679,30 @@
|
|||
};
|
||||
name = Release;
|
||||
};
|
||||
C33FBFD41BDE441C008E3545 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
INFOPLIST_FILE = ChattoAppUITests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.badoo.ChattoAppUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
TEST_TARGET_NAME = ChattoApp;
|
||||
USES_XCTRUNNER = YES;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
C33FBFD51BDE441C008E3545 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
INFOPLIST_FILE = ChattoAppUITests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.badoo.ChattoAppUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
TEST_TARGET_NAME = ChattoApp;
|
||||
USES_XCTRUNNER = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
|
|
@ -639,6 +733,15 @@
|
|||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
C33FBFD31BDE441C008E3545 /* Build configuration list for PBXNativeTarget "ChattoAppUITests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
C33FBFD41BDE441C008E3545 /* Debug */,
|
||||
C33FBFD51BDE441C008E3545 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = C33FBF9D1BDE441C008E3545 /* Project object */;
|
||||
|
|
|
|||
|
|
@ -39,6 +39,16 @@
|
|||
ReferencedContainer = "container:ChattoApp.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "C33FBFC31BDE441C008E3545"
|
||||
BuildableName = "ChattoAppUITests.xctest"
|
||||
BlueprintName = "ChattoAppUITests"
|
||||
ReferencedContainer = "container:ChattoApp.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
|
|
|
|||
|
|
@ -29,31 +29,33 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
|
||||
|
||||
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
func applicationWillResignActive(application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
func applicationDidEnterBackground(application: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
func applicationWillEnterForeground(application: UIApplication) {
|
||||
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
func applicationDidBecomeActive(application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
func applicationWillTerminate(application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import Foundation
|
|||
import ChattoAdditions
|
||||
|
||||
class BaseMessageCollectionViewCellAvatarStyle: BaseMessageCollectionViewCellDefaultStyle {
|
||||
override func avatarSize(viewModel: MessageViewModelProtocol) -> CGSize {
|
||||
override func avatarSize(viewModel viewModel: MessageViewModelProtocol) -> CGSize {
|
||||
// Display avatar for both incoming and outgoing messages for demo purpose
|
||||
return CGSize(width: 35, height: 35)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,24 +36,24 @@ class BaseMessageHandler {
|
|||
init (messageSender: FakeMessageSender) {
|
||||
self.messageSender = messageSender
|
||||
}
|
||||
func userDidTapOnFailIcon(viewModel: DemoMessageViewModelProtocol) {
|
||||
func userDidTapOnFailIcon(viewModel viewModel: DemoMessageViewModelProtocol) {
|
||||
print("userDidTapOnFailIcon")
|
||||
self.messageSender.sendMessage(viewModel.messageModel)
|
||||
}
|
||||
|
||||
func userDidTapOnAvatar(viewModel: MessageViewModelProtocol) {
|
||||
func userDidTapOnAvatar(viewModel viewModel: MessageViewModelProtocol) {
|
||||
print("userDidTapOnAvatar")
|
||||
}
|
||||
|
||||
func userDidTapOnBubble(viewModel: DemoMessageViewModelProtocol) {
|
||||
func userDidTapOnBubble(viewModel viewModel: DemoMessageViewModelProtocol) {
|
||||
print("userDidTapOnBubble")
|
||||
}
|
||||
|
||||
func userDidBeginLongPressOnBubble(viewModel: DemoMessageViewModelProtocol) {
|
||||
func userDidBeginLongPressOnBubble(viewModel viewModel: DemoMessageViewModelProtocol) {
|
||||
print("userDidBeginLongPressOnBubble")
|
||||
}
|
||||
|
||||
func userDidEndLongPressOnBubble(viewModel: DemoMessageViewModelProtocol) {
|
||||
func userDidEndLongPressOnBubble(viewModel viewModel: DemoMessageViewModelProtocol) {
|
||||
print("userDidEndLongPressOnBubble")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,14 +30,14 @@ final class ChatItemsDemoDecorator: ChatItemsDecoratorProtocol {
|
|||
struct Constants {
|
||||
static let shortSeparation: CGFloat = 3
|
||||
static let normalSeparation: CGFloat = 10
|
||||
static let timeIntervalThresholdToIncreaseSeparation: TimeInterval = 120
|
||||
static let timeIntervalThresholdToIncreaseSeparation: NSTimeInterval = 120
|
||||
}
|
||||
|
||||
func decorateItems(_ chatItems: [ChatItemProtocol]) -> [DecoratedChatItem] {
|
||||
func decorateItems(chatItems: [ChatItemProtocol]) -> [DecoratedChatItem] {
|
||||
var decoratedChatItems = [DecoratedChatItem]()
|
||||
let calendar = Calendar.current
|
||||
let calendar = NSCalendar.currentCalendar()
|
||||
|
||||
for (index, chatItem) in chatItems.enumerated() {
|
||||
for (index, chatItem) in chatItems.enumerate() {
|
||||
let next: ChatItemProtocol? = (index + 1 < chatItems.count) ? chatItems[index + 1] : nil
|
||||
let prev: ChatItemProtocol? = (index > 0) ? chatItems[index - 1] : nil
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ final class ChatItemsDemoDecorator: ChatItemsDecoratorProtocol {
|
|||
}
|
||||
|
||||
if let previousMessage = prev as? MessageModelProtocol {
|
||||
addTimeSeparator = !calendar.isDate(currentMessage.date, inSameDayAs: previousMessage.date)
|
||||
addTimeSeparator = calendar.compareDate(currentMessage.date, toDate: previousMessage.date, toUnitGranularity: NSCalendarUnit.Day) != NSComparisonResult.OrderedSame
|
||||
} else {
|
||||
addTimeSeparator = true
|
||||
}
|
||||
|
|
@ -77,13 +77,13 @@ final class ChatItemsDemoDecorator: ChatItemsDecoratorProtocol {
|
|||
chatItem: chatItem,
|
||||
decorationAttributes: ChatItemDecorationAttributes(bottomMargin: bottomMargin, showsTail: showsTail, canShowAvatar: showsTail))
|
||||
)
|
||||
decoratedChatItems.append(contentsOf: additionalItems)
|
||||
decoratedChatItems.appendContentsOf(additionalItems)
|
||||
}
|
||||
|
||||
return decoratedChatItems
|
||||
}
|
||||
|
||||
func separationAfterItem(_ current: ChatItemProtocol?, next: ChatItemProtocol?) -> CGFloat {
|
||||
func separationAfterItem(current: ChatItemProtocol?, next: ChatItemProtocol?) -> CGFloat {
|
||||
guard let nexItem = next else { return 0 }
|
||||
guard let currentMessage = current as? MessageModelProtocol else { return Constants.normalSeparation }
|
||||
guard let nextMessage = nexItem as? MessageModelProtocol else { return Constants.normalSeparation }
|
||||
|
|
@ -92,14 +92,14 @@ final class ChatItemsDemoDecorator: ChatItemsDecoratorProtocol {
|
|||
return 0
|
||||
} else if currentMessage.senderId != nextMessage.senderId {
|
||||
return Constants.normalSeparation
|
||||
} else if nextMessage.date.timeIntervalSince(currentMessage.date) > Constants.timeIntervalThresholdToIncreaseSeparation {
|
||||
} else if nextMessage.date.timeIntervalSinceDate(currentMessage.date) > Constants.timeIntervalThresholdToIncreaseSeparation {
|
||||
return Constants.normalSeparation
|
||||
} else {
|
||||
return Constants.shortSeparation
|
||||
}
|
||||
}
|
||||
|
||||
func showsStatusForMessage(_ message: MessageModelProtocol) -> Bool {
|
||||
return message.status == .failed || message.status == .sending
|
||||
func showsStatusForMessage(message: MessageModelProtocol) -> Bool {
|
||||
return message.status == .Failed || message.status == .Sending
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import UIKit
|
|||
|
||||
class ConversationsViewController: UITableViewController {
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
|
||||
|
||||
var initialCount = 0
|
||||
let pageSize = 50
|
||||
|
|
@ -39,17 +39,18 @@ class ConversationsViewController: UITableViewController {
|
|||
} else if segue.identifier == "10000 messages" {
|
||||
initialCount = 10000
|
||||
} else if segue.identifier == "overview" {
|
||||
dataSource = FakeDataSource(messages: TutorialMessageFactory.createMessages(), pageSize: pageSize)
|
||||
dataSource = FakeDataSource(messages: TutorialMessageFactory.createMessages().map { $0 }, pageSize: pageSize)
|
||||
} else {
|
||||
assert(false, "segue not handled!")
|
||||
}
|
||||
|
||||
|
||||
let chatController = { () -> DemoChatViewController? in
|
||||
if let controller = segue.destination as? DemoChatViewController {
|
||||
if let controller = segue.destinationViewController as? DemoChatViewController {
|
||||
return controller
|
||||
}
|
||||
if let tabController = segue.destination as? UITabBarController,
|
||||
let controller = tabController.viewControllers?.first as? DemoChatViewController {
|
||||
if let tabController = segue.destinationViewController as? UITabBarController,
|
||||
controller = tabController.viewControllers?.first as? DemoChatViewController {
|
||||
return controller
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -42,9 +42,9 @@ class DemoChatViewController: BaseChatViewController {
|
|||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
let image = UIImage(named: "bubble-incoming-tail-border", in: Bundle(for: DemoChatViewController.self), compatibleWith: nil)?.bma_tintWithColor(.blue)
|
||||
let image = UIImage(named: "bubble-incoming-tail-border", inBundle: NSBundle(forClass: DemoChatViewController.self), compatibleWithTraitCollection: nil)?.bma_tintWithColor(UIColor.blueColor())
|
||||
super.chatItemsDecorator = ChatItemsDemoDecorator()
|
||||
let addIncomingMessageButton = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(DemoChatViewController.addRandomIncomingMessage))
|
||||
let addIncomingMessageButton = UIBarButtonItem(image: image, style: .Plain, target: self, action: #selector(DemoChatViewController.addRandomIncomingMessage))
|
||||
self.navigationItem.rightBarButtonItem = addIncomingMessageButton
|
||||
}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue