Compare commits

..

29 Commits

Author SHA1 Message Date
Grigory 3adb53a269 Merge pull request #1 from TouchInstinct/bugfix/firstLoading
firstLoading fixed
2016-12-16 19:49:56 +03:00
Grigory Ulanov 71c792461e firstLoading fixed 2016-12-16 19:40:16 +03:00
Grigory Ulanov 62ac08bb6e Merge branch 'Progress_animation_fix' 2016-11-22 12:24:36 +03:00
Grigory Ulanov 67feae7fc0 Merge branch 'BatchUpdates_at_pagination' 2016-11-22 12:24:32 +03:00
Grigory Ulanov 75887d2ebc progress animation fixed 2016-11-22 12:24:13 +03:00
Grigory 8d06ed86cd Merge branch 'master' into BatchUpdates_at_pagination 2016-11-21 13:40:37 +03:00
Diego Sánchez a12bdf5873 Fix code coverage (#254)
* Code coverage test

* Adds .codecov.yml

* Fixes .codecov.yml
2016-11-19 01:31:09 +00:00
Diego Sánchez 0673909be5 Swiflint 0.13 fixes & Xcode 8 b2 compatibility (#253)
* Updates swiftlint config

* Fixes swiftlint erros and warnings

* Makes ChattoApp compatible with Xcode 8 b2

* Carthage compatibility with Xcode 8 b2

* Updates .travis.yml to use stable image with Xcode 8.1
2016-11-18 22:12:40 +00:00
Grigory Ulanov 7c9c6fd252 Pagination batchUpdates instead of reload 2016-11-17 18:36:59 +03:00
Diego Sanchez 9ead01b7ea Fix in Changelog 2016-11-14 18:02:37 +00:00
Diego Sánchez cc7c77dc21 Release 3.0.1 (#250)
* Updates CHANGELOG

* Bumps version to 3.0.1

* Updates Podfile to use Swift 3.0.1. Runs pod update

* Configures framework projects to use Swift 3.0.1

* Uses xcode8.1sneakpeek image in travis
2016-11-14 17:21:53 +00:00
Diego Sanchez dee74e230e Uses xcode8.1sneakpeek image in travis 2016-11-14 16:06:49 +00:00
Diego Sanchez 482be82f80 Configures framework projects to use Swift 3.0.1 2016-11-14 16:05:42 +00:00
Diego Sanchez 5693e332d1 Updates Podfile to use Swift 3.0.1. Runs pod update 2016-11-14 16:05:41 +00:00
Diego Sanchez be72adeae3 Bumps version to 3.0.1 2016-11-14 16:05:41 +00:00
Diego Sanchez d4221fc7e6 Updates CHANGELOG 2016-11-14 16:05:41 +00:00
0xpablo dceee05d27 Use new Calendar API (#249)
Use new Calendar API to add the time separator if two dates are in a different day.
2016-11-08 11:24:27 +00:00
Diego Sánchez 34a85527de Avoid crash when receiving a nil indexPath (#248) 2016-11-06 19:38:12 +00:00
geegaset fa43ed65bb Avoids using CaptureSession in simulator (#235)
App crashes on iOS 10 simulator if so
2016-10-10 12:09:40 +01:00
Diego Sánchez e269794da0 Makes source code compatible with Xcode 8.1 b2 (#233) 2016-10-05 11:18:11 +01:00
Zhao Wang 5e2827465e Fixes weird linker issue when using Carthage (#232) 2016-10-04 10:51:10 +01:00
Diego Sánchez 55885a5fd6 Adds exclusive touch to bubble view (#223)
* Adds exclusive touch to bubble view

* Sanitises quotes in Podfile

* Runs pod update
2016-09-26 22:42:33 +01:00
Alexsander Khitev bb600dbf22 Sets Swift version to 3 in ChattoApp Podfile (#222)
* switched Swift version to 3 in ChattoApp podfile

* removed description
2016-09-22 20:22:19 +01:00
Diego Sanchez 4ec15a8812 Removes ChattoAppUITests target 2016-09-21 16:36:23 +01:00
Diego Sanchez 14ebd59529 Disables UI tests in ChattoApp due to unstable environment in travis 2016-09-21 16:11:08 +01:00
Diego Sanchez cbb32790a7 Adds .swift-version file 2016-09-21 13:57:36 +01:00
Diego Sanchez bfeaf80042 Updates readme and changelog 2016-09-21 13:48:02 +01:00
Diego Sanchez fa264996e8 Bumps version to 3.0.0 2016-09-21 13:47:50 +01:00
Diego Sánchez d1b01327d1 Swift 3 migration (#220)
* Runs the Swift 3 migrator in ChattoApp.

Affects ChattoApp, ChattoAppTests, Chatto and ChattoAdditions. Does not migrate ChattoTests or ChattoAdditionsTests

* Configures Chatto And ChattoAdditions projects to use Swift 3

* Updates .travis.yml to use Xcode 8 and iPhone 7

* Uses flatMap instead of filter and force cast

* Fixes createCollectionViewLayout not being a function

* Removes useless init overrides

* Fix for new implicit unwrapping optional non-propagation rule

* Removes useless casting

* Audits accessor levels in Observable

* Fixes UIControlState.Normal replaced by UIControlState()

* Favours private over fileprivate where possible

* Audits open/public access level

* Removes conditional if for Swift 2

* Removes label from simulateTapOnTextViewForDelegate

* Audits open/public access levels
2016-09-21 12:58:25 +01:00
131 changed files with 1890 additions and 2013 deletions

3
.codecov.yml Normal file
View File

@ -0,0 +1,3 @@
ignore:
- "./Chatto/Tests"
- "./ChattoAdditions/Tests"

1
.swift-version Normal file
View File

@ -0,0 +1 @@
3.0.1

View File

@ -1,4 +1,9 @@
disabled_rules: # rule identifiers to exclude from running
opt_in_rules:
- closure_spacing
- overridden_super_call
- redundant_nil_coalesing
- explicit_init
disabled_rules:
- file_length
- force_cast
- function_body_length

View File

@ -1,12 +1,10 @@
language: objective-c
osx_image: xcode7.3
osx_image: xcode8.1
script:
- set -o pipefail
- 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
- 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

View File

@ -1,3 +1,12 @@
### 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)

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "Chatto"
s.version = "2.1.0"
s.version = "3.0.1"
s.summary = "Chat framework in Swift"
s.description = <<-DESC
Lightweight chat framework to build Chat apps

View File

@ -426,7 +426,7 @@
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 2.3;
SWIFT_VERSION = 3.0;
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 = 2.3;
SWIFT_VERSION = 3.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";

View File

@ -25,62 +25,62 @@
import UIKit
public enum ChatItemVisibility {
case Hidden
case Appearing
case Visible
case hidden
case appearing
case visible
}
public class BaseChatItemPresenter<CellT: UICollectionViewCell>: ChatItemPresenterProtocol {
open class BaseChatItemPresenter<CellT: UICollectionViewCell>: ChatItemPresenterProtocol {
public final weak var cell: CellT?
public init() { }
public init() {}
public class func registerCells(collectionView: UICollectionView) {
open class func registerCells(_ collectionView: UICollectionView) {
assert(false, "Implement in subclass")
}
public var canCalculateHeightInBackground: Bool {
open var canCalculateHeightInBackground: Bool {
return false
}
public func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat {
open func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat {
assert(false, "Implement in subclass")
return 0
}
public func dequeueCell(collectionView collectionView: UICollectionView, indexPath: NSIndexPath) -> UICollectionViewCell {
open func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell {
assert(false, "Implemenent in subclass")
return UICollectionViewCell()
}
public func configureCell(cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) {
open 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!")
}
}
public func cellWillBeShown() {
open func cellWillBeShown() {
// Hook for subclasses
}
public func shouldShowMenu() -> Bool {
open 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 @@ public class BaseChatItemPresenter<CellT: UICollectionViewCell>: ChatItemPresent
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 @@ public class BaseChatItemPresenter<CellT: UICollectionViewCell>: ChatItemPresent
}
}
public func cellWasHidden() {
open func cellWasHidden() {
// Hook for subclasses. Here we are not visible for real.
}
public func canPerformMenuControllerAction(action: Selector) -> Bool {
open func canPerformMenuControllerAction(_ action: Selector) -> Bool {
return false
}
public func performMenuControllerAction(action: Selector) {
open func performMenuControllerAction(_ action: Selector) {
assert(self.canPerformMenuControllerAction(action))
}
}

View File

@ -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 {

View File

@ -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 collectionView: UICollectionView, indexPath: NSIndexPath) -> UICollectionViewCell
func configureCell(cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?)
func cellWillBeShown(cell: UICollectionViewCell) // optional
func cellWasHidden(cell: UICollectionViewCell) // optional
func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> 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 }
}

View File

@ -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.registerClass(DummyCollectionViewCell.self, forCellWithReuseIdentifier: "cell-id-unhandled-message")
class func registerCells(_ collectionView: UICollectionView) {
collectionView.register(DummyCollectionViewCell.self, forCellWithReuseIdentifier: "cell-id-unhandled-message")
}
var canCalculateHeightInBackground: Bool {
@ -39,14 +39,13 @@ class DummyChatItemPresenter: ChatItemPresenterProtocol {
return 0
}
func dequeueCell(collectionView collectionView: UICollectionView, indexPath: NSIndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCellWithReuseIdentifier("cell-id-unhandled-message", forIndexPath: indexPath)
func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCell(withReuseIdentifier: "cell-id-unhandled-message", for: indexPath)
}
func configureCell(cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) {
cell.hidden = true
func configureCell(_ cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) {
cell.isHidden = true
}
}
class DummyCollectionViewCell: UICollectionViewCell {}

View File

@ -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: UpdateType) {
public func enqueueModelUpdate(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 where (self.chatDataSource?.chatItems.count ?? 0) > preferredMaxMessageCount else { return }
guard let preferredMaxMessageCount = self.constants.preferredMaxMessageCount, (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() -> [NSIndexPath: UICollectionViewCell] {
var visibleCells: [NSIndexPath: UICollectionViewCell] = [:]
self.collectionView.indexPathsForVisibleItems().forEach({ (indexPath) in
if let cell = self.collectionView.cellForItemAtIndexPath(indexPath) {
private func visibleCellsFromCollectionViewApi() -> [IndexPath: UICollectionViewCell] {
var visibleCells: [IndexPath: UICollectionViewCell] = [:]
self.collectionView.indexPathsForVisibleItems.forEach({ (indexPath) in
if let cell = self.collectionView.cellForItem(at: indexPath) {
visibleCells[indexPath] = cell
}
})
return visibleCells
}
private func visibleCellsAreValid(changes changes: CollectionChanges) -> Bool {
private func visibleCellsAreValid(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,18 +128,19 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
private enum ScrollAction {
case scrollToBottom
case preservePosition(rectForReferenceIndexPathBeforeUpdate: CGRect?, referenceIndexPathAfterUpdate: NSIndexPath?)
case preservePosition(rectForReferenceIndexPathBeforeUpdate: CGRect?, referenceIndexPathAfterUpdate: IndexPath?)
}
func performBatchUpdates(updateModelClosure updateModelClosure: () -> Void,
func performBatchUpdates(updateModelClosure: @escaping () -> Void,
changes: CollectionChanges,
updateType: UpdateType,
completion: () -> Void) {
completion: @escaping () -> Void) {
let usesBatchUpdates: Bool
let animateBatchUpdates: Bool
do { // Recover from too fast updates...
let visibleCellsAreValid = self.visibleCellsAreValid(changes: changes)
let wantsReloadData = updateType != .Normal
let wantsReloadData = ![UpdateType.normal, UpdateType.pagination, UpdateType.firstLoad].contains(updateType)
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
@ -158,11 +159,12 @@ 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)
@ -178,7 +180,7 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
if myCompletionExecuted { return }
myCompletionExecuted = true
dispatch_async(dispatch_get_main_queue(), { () -> Void in
DispatchQueue.main.async(execute: { () -> Void in
// Reduces inconsistencies before next update: https://github.com/diegosanchezr/UICollectionViewStressing
completion()
})
@ -186,36 +188,46 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
}
if usesBatchUpdates {
UIView.animateWithDuration(self.constants.updatesAnimationDuration, animations: { () -> Void in
let batchUpdates = {
self.unfinishedBatchUpdatesCount += 1
self.collectionView.performBatchUpdates({ () -> Void in
updateModelClosure()
self.updateVisibleCells(changes) // For instace, to support removal of tails
self.collectionView.deleteItemsAtIndexPaths(Array(changes.deletedIndexPaths))
self.collectionView.insertItemsAtIndexPaths(Array(changes.insertedIndexPaths))
self.collectionView.deleteItems(at: Array(changes.deletedIndexPaths))
self.collectionView.insertItems(at: Array(changes.insertedIndexPaths))
for move in changes.movedIndexPaths {
self.collectionView.moveItemAtIndexPath(move.indexPathOld, toIndexPath: move.indexPathNew)
self.collectionView.moveItem(at: move.indexPathOld, to: 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 {
dispatch_async(dispatch_get_main_queue(), onAllBatchUpdatesFinished)
DispatchQueue.main.async(execute: 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.prepareLayout()
self.collectionView.collectionViewLayout.prepare()
}
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)
@ -226,16 +238,16 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
}
}
private func updateModels(newItems newItems: [ChatItemProtocol], oldItems: ChatItemCompanionCollection, updateType: UpdateType, completion: () -> Void) {
private func updateModels(newItems: [ChatItemProtocol], oldItems: ChatItemCompanionCollection, updateType: UpdateType, completion: @escaping () -> 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: () -> Void) -> () = { [weak self] modelUpdate in
let perfomBatchUpdates: (_ changes: CollectionChanges, _ updateModelClosure: @escaping () -> Void) -> () = { [weak self] (changes, updateModelClosure) in
self?.performBatchUpdates(
updateModelClosure: modelUpdate.updateModelClosure,
changes: modelUpdate.changes,
updateModelClosure: updateModelClosure,
changes: changes,
updateType: updateType,
completion: { () -> Void in
self?.autoLoadingEnabled = true
@ -251,19 +263,19 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
}
if performInBackground {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
DispatchQueue.global(qos: .userInitiated).async { () -> Void in
let modelUpdate = createModelUpdate()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
perfomBatchUpdates(changes: modelUpdate.changes, updateModelClosure: modelUpdate.updateModelClosure)
DispatchQueue.main.async(execute: { () -> Void in
perfomBatchUpdates(modelUpdate.changes, modelUpdate.updateModelClosure)
})
}
} else {
let modelUpdate = createModelUpdate()
perfomBatchUpdates(changes: modelUpdate.changes, updateModelClosure: modelUpdate.updateModelClosure)
perfomBatchUpdates(modelUpdate.changes, modelUpdate.updateModelClosure)
}
}
private func createModelUpdates(newItems newItems: [ChatItemProtocol], oldItems: ChatItemCompanionCollection, collectionViewWidth: CGFloat) -> (changes: CollectionChanges, updateModelClosure: () -> Void) {
private func createModelUpdates(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)
@ -284,7 +296,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] where oldChatItemCompanion.chatItem === chatItem {
if let oldChatItemCompanion = oldItems[decoratedChatItem.uid], oldChatItemCompanion.chatItem === chatItem {
presenter = oldChatItemCompanion.presenter
} else {
presenter = self.createPresenterForChatItem(decoratedChatItem.chatItem)
@ -293,22 +305,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 intermediateLayoutData: [IntermediateItemLayoutData]) -> ChatCollectionViewLayoutModel {
func createLayoutModel(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 = !NSThread.isMainThread()
let isInbackground = !Thread.isMainThread
var intermediateLayoutData = [IntermediateItemLayoutData]()
var itemsForMainThread = [(index: Int, itemCompanion: ChatItemCompanion)]()
for (index, itemCompanion) in items.enumerate() {
for (index, itemCompanion) in items.enumerated() {
var height: CGFloat?
let bottomMargin: CGFloat = itemCompanion.decorationAttributes?.bottomMargin ?? 0
if !isInbackground || itemCompanion.presenter.canCalculateHeightInBackground {
@ -320,7 +332,7 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
}
if itemsForMainThread.count > 0 {
dispatch_sync(dispatch_get_main_queue(), { () -> Void in
DispatchQueue.main.sync(execute: { () -> Void in
for (index, itemCompanion) in itemsForMainThread {
let height = itemCompanion.presenter.heightForCell(maximumWidth: collectionViewWidth, decorationAttributes: itemCompanion.decorationAttributes)
intermediateLayoutData[index].height = height

View File

@ -26,11 +26,12 @@ 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
}
public func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
@objc(collectionView:cellForItemAtIndexPath:)
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let presenter = self.presenterForIndexPath(indexPath)
let cell = presenter.dequeueCell(collectionView: collectionView, indexPath: indexPath)
let decorationAttributes = self.decorationAttributesForIndexPath(indexPath)
@ -38,16 +39,17 @@ extension BaseChatViewController: ChatCollectionViewLayoutDelegate {
return cell
}
public func collectionView(collectionView: UICollectionView, didEndDisplayingCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
@objc(collectionView:didEndDisplayingCell:forItemAtIndexPath:)
open func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// 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.objectForKey(cell) as? ChatItemPresenterProtocol {
self.presentersByCell.removeObjectForKey(cell)
if let oldPresenterForCell = self.presentersByCell.object(forKey: cell) as? ChatItemPresenterProtocol {
self.presentersByCell.removeObject(forKey: cell)
oldPresenterForCell.cellWasHidden(cell)
}
if self.updatesConfig.fastUpdates {
if let visibleCell = self.visibleCells[indexPath] where visibleCell === cell {
if let visibleCell = self.visibleCells[indexPath], visibleCell === cell {
self.visibleCells[indexPath] = nil
} else {
self.visibleCells.forEach({ (indexPath, storedCell) in
@ -60,7 +62,8 @@ extension BaseChatViewController: ChatCollectionViewLayoutDelegate {
}
}
public func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
@objc(collectionView:willDisplayCell:forItemAtIndexPath:)
open func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// Here indexPath should always referer to updated data source.
let presenter = self.presenterForIndexPath(indexPath)
@ -80,23 +83,29 @@ extension BaseChatViewController: ChatCollectionViewLayoutDelegate {
}
}
public func collectionView(collectionView: UICollectionView, shouldShowMenuForItemAtIndexPath indexPath: NSIndexPath) -> Bool {
return self.presenterForIndexPath(indexPath).shouldShowMenu() ?? false
@objc(collectionView:shouldShowMenuForItemAtIndexPath:)
open func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool {
return self.presenterForIndexPath(indexPath).shouldShowMenu()
}
public func collectionView(collectionView: UICollectionView, canPerformAction action: Selector, forItemAtIndexPath indexPath: NSIndexPath, withSender sender: AnyObject?) -> Bool {
return self.presenterForIndexPath(indexPath).canPerformMenuControllerAction(action) ?? 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, performAction action: Selector, forItemAtIndexPath indexPath: NSIndexPath, withSender sender: AnyObject?) {
@objc(collectionView:performAction:forItemAtIndexPath:withSender:)
open func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) {
self.presenterForIndexPath(indexPath).performMenuControllerAction(action)
}
func presenterForIndexPath(indexPath: NSIndexPath) -> ChatItemPresenterProtocol {
func presenterForIndexPath(_ indexPath: IndexPath) -> 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()
@ -104,11 +113,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: NSIndexPath) -> ChatItemDecorationAttributesProtocol? {
public func decorationAttributesForIndexPath(_ indexPath: IndexPath) -> ChatItemDecorationAttributesProtocol? {
return self.chatItemCompanionCollection[indexPath.item].decorationAttributes
}
}

View File

@ -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.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)
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)
}
public func isScrolledAtTop() -> Bool {
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)
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)
}
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: NSIndexPath, atEdge edge: CellVerticalEdge) -> Bool {
if let attributes = self.collectionView.collectionViewLayout.layoutAttributesForItemAtIndexPath(indexPath) {
public func isIndexPathVisible(_ indexPath: IndexPath, atEdge edge: CellVerticalEdge) -> Bool {
if let attributes = self.collectionView.collectionViewLayout.layoutAttributesForItem(at: indexPath) {
let visibleRect = self.visibleRect()
let intersection = visibleRect.intersect(attributes.frame)
if edge == .Top {
return CGFloat.abs(intersection.minY - attributes.frame.minY) < CGFloat.bma_epsilon
let intersection = visibleRect.intersection(attributes.frame)
if edge == .top {
return abs(intersection.minY - attributes.frame.minY) < CGFloat.bma_epsilon
} else {
return CGFloat.abs(intersection.maxY - attributes.frame.maxY) < CGFloat.bma_epsilon
return 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 animated: Bool) {
public func scrollToBottom(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.animateWithDuration(self.constants.updatesAnimationDuration, animations: { () -> Void in
UIView.animate(withDuration: 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 oldRefRect: CGRect?, newRefRect: CGRect?) {
guard let oldRefRect = oldRefRect, newRefRect = newRefRect else {
public func scrollToPreservePosition(oldRefRect: CGRect?, newRefRect: CGRect?) {
guard let oldRefRect = oldRefRect, let newRefRect = newRefRect else {
return
}
let diffY = newRefRect.minY - oldRefRect.minY
self.collectionView.contentOffset = CGPoint(x: 0, y: self.collectionView.contentOffset.y + diffY)
}
public func scrollViewDidScroll(scrollView: UIScrollView) {
if self.collectionView.dragging {
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
if self.collectionView.isDragging {
self.autoLoadMoreContentIfNeeded()
}
}
public func scrollViewDidScrollToTop(scrollView: UIScrollView) {
public func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
self.autoLoadMoreContentIfNeeded()
}

View File

@ -24,14 +24,14 @@
import UIKit
public class BaseChatViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
open class BaseChatViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
public typealias ChatItemCompanionCollection = ReadOnlyOrderedDictionary<ChatItemCompanion>
public struct Constants {
public var updatesAnimationDuration: NSTimeInterval = 0.33
public var updatesAnimationDuration: TimeInterval = 0.33
public var defaultContentInsets = UIEdgeInsets(top: 10, left: 0, bottom: 10, right: 0)
public var defaultScrollIndicatorInsets = UIEdgeInsetsZero
public var defaultScrollIndicatorInsets = UIEdgeInsets.zero
public var preferredMaxMessageCount: Int? = 500 // If not nil, will ask data source to reduce number of messages when limit is reached. @see ChatDataSourceDelegateProtocol
public var preferredMaxMessageCountAdjustment: Int = 400 // When the above happens, will ask to adjust with this value. It may be wise for this to be smaller to reduce number of adjustments
public var autoloadingFractionalThreshold: CGFloat = 0.05 // in [0, 1]
@ -54,12 +54,12 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
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 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
self.collectionView?.dataSource = nil
}
public override func loadView() {
open override func loadView() {
self.view = BaseChatViewControllerView() // http://stackoverflow.com/questions/24596031/uiviewcontroller-with-inputaccessoryview-is-not-deallocated
self.view.backgroundColor = UIColor.whiteColor()
self.view.backgroundColor = UIColor.white
}
override public func viewDidLoad() {
override open func viewDidLoad() {
super.viewDidLoad()
self.addCollectionView()
self.addInputViews()
@ -91,39 +91,39 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
public var endsEditingWhenTappingOnChatBackground = true
@objc
public func userDidTapOnCollectionView() {
open func userDidTapOnCollectionView() {
if self.endsEditingWhenTappingOnChatBackground {
self.view.endEditing(true)
}
}
public override func viewWillAppear(animated: Bool) {
open override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.keyboardTracker.startTracking()
}
public override func viewWillDisappear(animated: Bool) {
open 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.clearColor()
self.collectionView.keyboardDismissMode = .Interactive
self.collectionView.backgroundColor = UIColor.clear
self.collectionView.keyboardDismissMode = .interactive
self.collectionView.showsVerticalScrollIndicator = true
self.collectionView.showsHorizontalScrollIndicator = false
self.collectionView.allowsSelection = false
self.collectionView.translatesAutoresizingMaskIntoConstraints = false
self.collectionView.autoresizingMask = .None
self.collectionView.autoresizingMask = UIViewAutoresizing()
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 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
private var inputContainerBottomConstraint: NSLayoutConstraint!
private func addInputViews() {
self.inputContainer = UIView(frame: CGRect.zero)
self.inputContainer.autoresizingMask = .None
self.inputContainer.autoresizingMask = UIViewAutoresizing()
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
public func setupKeyboardTracker() {
open func setupKeyboardTracker() {
let layoutBlock = { [weak self] (bottomMargin: CGFloat) in
guard let sSelf = self else { return }
sSelf.isAdjustingInputContainer = true
@ -170,11 +170,11 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
(self.view as? BaseChatViewControllerView)?.bmaInputAccessoryView = self.keyboardTracker?.trackingView
}
var notificationCenter = NSNotificationCenter.defaultCenter()
var notificationCenter = NotificationCenter.default
var keyboardTracker: KeyboardTracker!
public var isFirstLayout: Bool = true
override public func viewDidLayoutSubviews() {
public private(set) var isFirstLayout: Bool = true
override open func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.adjustCollectionViewInsets()
@ -185,7 +185,8 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
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 hidesBottomBarWhenPushed && navigationController?.viewControllers.count > 1 && navigationController?.viewControllers.last == self {
if self.hidesBottomBarWhenPushed && (navigationController?.viewControllers.count ?? 0) > 1 && navigationController?.viewControllers.last == self {
self.inputContainerBottomConstraint.constant = 0
} else {
self.inputContainerBottomConstraint.constant = self.bottomLayoutGuide.length
@ -194,7 +195,7 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
}
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 }
@ -203,7 +204,7 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
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
@ -239,9 +240,9 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
}
}
func rectAtIndexPath(indexPath: NSIndexPath?) -> CGRect? {
func rectAtIndexPath(_ indexPath: IndexPath?) -> CGRect? {
if let indexPath = indexPath {
return self.collectionView.collectionViewLayout.layoutAttributesForItemAtIndexPath(indexPath)?.frame
return self.collectionView.collectionViewLayout.layoutAttributesForItem(at: indexPath)?.frame
}
return nil
}
@ -250,8 +251,8 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
var accessoryViewRevealer: AccessoryViewRevealer!
public private(set) var inputContainer: UIView!
var presenterFactory: ChatItemPresenterFactoryProtocol!
let presentersByCell = NSMapTable(keyOptions: .WeakMemory, valueOptions: .WeakMemory)
var visibleCells: [NSIndexPath: UICollectionViewCell] = [:] // @see visibleCellsAreValid(changes:)
let presentersByCell = NSMapTable<UICollectionViewCell, AnyObject>(keyOptions: .weakMemory, valueOptions: .weakMemory)
var visibleCells: [IndexPath: UICollectionViewCell] = [:] // @see visibleCellsAreValid(changes:)
public internal(set) var updateQueue: SerialTaskQueueProtocol = SerialTaskQueue()
@ -263,7 +264,7 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
*/
public var chatItemsDecorator: ChatItemsDecoratorProtocol?
public var createCollectionViewLayout: UICollectionViewLayout {
open func createCollectionViewLayout() -> UICollectionViewLayout {
let layout = ChatCollectionViewLayout()
layout.delegate = self
return layout
@ -273,17 +274,17 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
// MARK: Subclass overrides
public func createPresenterFactory() -> ChatItemPresenterFactoryProtocol {
open func createPresenterFactory() -> ChatItemPresenterFactoryProtocol {
// Default implementation
return ChatItemPresenterFactory(presenterBuildersByType: self.createPresenterBuilders())
}
public func createPresenterBuilders() -> [ChatItemType: [ChatItemPresenterBuilderProtocol]] {
open func createPresenterBuilders() -> [ChatItemType: [ChatItemPresenterBuilderProtocol]] {
assert(false, "Override in subclass")
return [ChatItemType: [ChatItemPresenterBuilderProtocol]]()
}
public func createChatInputView() -> UIView {
open func createChatInputView() -> UIView {
assert(false, "Override in subclass")
return UIView()
}
@ -292,20 +293,20 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
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
*/
public func referenceIndexPathsToRestoreScrollPositionOnUpdate(itemsBeforeUpdate itemsBeforeUpdate: ChatItemCompanionCollection, changes: CollectionChanges) -> (beforeUpdate: NSIndexPath?, afterUpdate: NSIndexPath?) {
open func referenceIndexPathsToRestoreScrollPositionOnUpdate(itemsBeforeUpdate: ChatItemCompanionCollection, changes: CollectionChanges) -> (beforeUpdate: IndexPath?, afterUpdate: IndexPath?) {
let firstItemMoved = changes.movedIndexPaths.first
return (firstItemMoved?.indexPathOld, firstItemMoved?.indexPathNew)
return (firstItemMoved?.indexPathOld as IndexPath?, firstItemMoved?.indexPathNew as IndexPath?)
}
}
extension BaseChatViewController { // Rotation
public override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
let shouldScrollToBottom = self.isScrolledAtBottom()
let referenceIndexPath = self.collectionView.indexPathsForVisibleItems().first
let referenceIndexPath = self.collectionView.indexPathsForVisibleItems.first
let oldRect = self.rectAtIndexPath(referenceIndexPath)
coordinator.animateAlongsideTransition({ (context) -> Void in
coordinator.animate(alongsideTransition: { (context) -> Void in
if shouldScrollToBottom {
self.scrollToBottom(animated: false)
} else {

View File

@ -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: (rawTranslation: CGFloat) -> CGFloat) {
public let translationTransform: (_ rawTranslation: CGFloat) -> CGFloat
public init(angleThresholdInRads: CGFloat, translationTransform: @escaping (_ rawTranslation: CGFloat) -> CGFloat) {
self.angleThresholdInRads = angleThresholdInRads
self.translationTransform = translationTransform
}
@ -68,51 +68,51 @@ class AccessoryViewRevealer: NSObject, UIGestureRecognizerDelegate {
var isEnabled: Bool = true {
didSet {
self.panRecognizer.enabled = self.isEnabled
self.panRecognizer.isEnabled = 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.translationInView(self.collectionView)
self.revealAccessoryView(atOffset: self.config.translationTransform(rawTranslation: -translation.x))
case .Ended, .Cancelled, .Failed:
case .changed:
let translation = panRecognizer.translation(in: self.collectionView)
self.revealAccessoryView(atOffset: self.config.translationTransform(-translation.x))
case .ended, .cancelled, .failed:
self.revealAccessoryView(atOffset: 0)
default:
break
}
}
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith 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.translationInView(self.collectionView)
let x = CGFloat.abs(translation.x), y = CGFloat.abs(translation.y)
let translation = self.panRecognizer.translation(in: self.collectionView)
let x = abs(translation.x), y = 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().filter({$0 is AccessoryViewRevealable}).map({$0 as! AccessoryViewRevealable})
let cells: [AccessoryViewRevealable] = self.collectionView.visibleCells.flatMap({ $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 where cell.allowAccessoryViewRevealing {
for cell in self.collectionView.visibleCells {
if let cell = cell as? AccessoryViewRevealable, cell.allowAccessoryViewRevealing {
cell.revealAccessoryView(withOffset: offset, animated: offset == 0)
}
}

View File

@ -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.enumerate() {
let indexPath = NSIndexPath(forItem: index, inSection: 0)
for (index, layoutData) in itemsLayoutData.enumerated() {
let indexPath = IndexPath(item: index, section: 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(forCellWithIndexPath: indexPath)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = frame
layoutAttributes.append(attributes)
layoutAttributesBySectionAndItem[0].append(attributes)
@ -62,27 +62,26 @@ public struct ChatCollectionViewLayoutModel {
}
}
public class ChatCollectionViewLayout: UICollectionViewLayout {
open 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
public override func invalidateLayout() {
open override func invalidateLayout() {
super.invalidateLayout()
self.layoutNeedsUpdate = true
}
public override func prepareLayout() {
super.prepareLayout()
open override func prepare() {
super.prepare()
guard self.layoutNeedsUpdate else { return }
guard let delegate = self.delegate else { return }
var oldLayoutModel = self.layoutModel
self.layoutModel = delegate.chatCollectionViewLayoutModel()
self.layoutNeedsUpdate = false
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in
DispatchQueue.global(qos: .default).async { () -> 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 {
@ -92,15 +91,15 @@ public class ChatCollectionViewLayout: UICollectionViewLayout {
}
}
public override func collectionViewContentSize() -> CGSize {
open override var collectionViewContentSize: CGSize {
return self.layoutModel?.contentSize ?? .zero
}
override public func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return self.layoutModel.layoutAttributes.filter { $0.frame.intersects(rect) }
}
public override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
if indexPath.section < self.layoutModel.layoutAttributesBySectionAndItem.count && indexPath.item < self.layoutModel.layoutAttributesBySectionAndItem[indexPath.section].count {
return self.layoutModel.layoutAttributesBySectionAndItem[indexPath.section][indexPath.item]
}
@ -108,7 +107,7 @@ public class ChatCollectionViewLayout: UICollectionViewLayout {
return nil
}
public override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return self.layoutModel.calculatedForWidth != newBounds.width
}
}

View File

@ -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 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: Int?, focusPosition: Double, completion:((didAdjust: Bool)) -> Void) // If you want, implement message count contention for performance, otherwise just call completion(false)
}

View File

@ -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)

View File

@ -29,14 +29,14 @@ public protocol UniqueIdentificable {
}
public struct CollectionChangeMove: Equatable, Hashable {
public let indexPathOld: NSIndexPath
public let indexPathNew: NSIndexPath
public init(indexPathOld: NSIndexPath, indexPathNew: NSIndexPath) {
public let indexPathOld: IndexPath
public let indexPathNew: IndexPath
public init(indexPathOld: IndexPath, indexPathNew: IndexPath) {
self.indexPathOld = indexPathOld
self.indexPathNew = indexPathNew
}
public var hashValue: Int { return indexPathOld.hash ^ indexPathNew.hash }
public var hashValue: Int { return indexPathOld.hashValue ^ indexPathNew.hashValue }
}
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<NSIndexPath>
public let deletedIndexPaths: Set<NSIndexPath>
public let insertedIndexPaths: Set<IndexPath>
public let deletedIndexPaths: Set<IndexPath>
public let movedIndexPaths: [CollectionChangeMove]
init(insertedIndexPaths: Set<NSIndexPath>, deletedIndexPaths: Set<NSIndexPath>, movedIndexPaths: [CollectionChangeMove]) {
init(insertedIndexPaths: Set<IndexPath>, deletedIndexPaths: Set<IndexPath>, movedIndexPaths: [CollectionChangeMove]) {
self.insertedIndexPaths = insertedIndexPaths
self.deletedIndexPaths = deletedIndexPaths
self.movedIndexPaths = movedIndexPaths
}
}
func generateChanges(oldCollection oldCollection: [UniqueIdentificable], newCollection: [UniqueIdentificable]) -> CollectionChanges {
func generateIndexesById(uids: [String]) -> [String: Int] {
func generateChanges(oldCollection: [UniqueIdentificable], newCollection: [UniqueIdentificable]) -> CollectionChanges {
func generateIndexesById(_ uids: [String]) -> [String: Int] {
var map = [String: Int](minimumCapacity: uids.count)
for (index, uid) in uids.enumerate() {
for (index, uid) in uids.enumerated() {
map[uid] = index
}
return map
@ -68,25 +68,25 @@ func generateChanges(oldCollection oldCollection: [UniqueIdentificable], newColl
let newIds = newCollection.map { $0.uid }
let oldIndexsById = generateIndexesById(oldIds)
let newIndexsById = generateIndexesById(newIds)
var deletedIndexPaths = Set<NSIndexPath>()
var insertedIndexPaths = Set<NSIndexPath>()
var deletedIndexPaths = Set<IndexPath>()
var insertedIndexPaths = Set<IndexPath>()
var movedIndexPaths = [CollectionChangeMove]()
// Deletetions
for oldId in oldIds {
let isDeleted = newIndexsById[oldId] == nil
if isDeleted {
deletedIndexPaths.insert(NSIndexPath(forItem: oldIndexsById[oldId]!, inSection: 0))
deletedIndexPaths.insert(IndexPath(item: oldIndexsById[oldId]!, section: 0))
}
}
// Insertions and movements
for newId in newIds {
let newIndex = newIndexsById[newId]!
let newIndexPath = NSIndexPath(forItem: newIndex, inSection: 0)
let newIndexPath = IndexPath(item: newIndex, section: 0)
if let oldIndex = oldIndexsById[newId] {
if oldIndex != newIndex {
movedIndexPaths.append(CollectionChangeMove(indexPathOld: NSIndexPath(forItem: oldIndex, inSection: 0), indexPathNew: newIndexPath))
movedIndexPaths.append(CollectionChangeMove(indexPathOld: IndexPath(item: oldIndex, section: 0), indexPathNew: newIndexPath))
}
} else {
// It's new
@ -97,14 +97,14 @@ func generateChanges(oldCollection oldCollection: [UniqueIdentificable], newColl
return CollectionChanges(insertedIndexPaths: insertedIndexPaths, deletedIndexPaths: deletedIndexPaths, movedIndexPaths: movedIndexPaths)
}
func updated<T: Any>(collection collection: [NSIndexPath: T], withChanges changes: CollectionChanges) -> [NSIndexPath: T] {
func updated<T: Any>(collection: [IndexPath: T], withChanges changes: CollectionChanges) -> [IndexPath: T] {
var result = collection
changes.deletedIndexPaths.forEach { (indexPath) in
result[indexPath] = nil
}
var movedDestinations = Set<NSIndexPath>()
var movedDestinations = Set<IndexPath>()
changes.movedIndexPaths.forEach { (move) in
result[move.indexPathNew] = collection[move.indexPathOld]
movedDestinations.insert(move.indexPathNew)

View File

@ -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: NSNotificationCenter
private var notificationCenter: NotificationCenter
typealias LayoutBlock = (bottomMargin: CGFloat) -> Void
typealias LayoutBlock = (_ bottomMargin: CGFloat) -> Void
private var layoutBlock: LayoutBlock
init(viewController: UIViewController, inputContainer: UIView, layoutBlock: LayoutBlock, notificationCenter: NSNotificationCenter) {
init(viewController: UIViewController, inputContainer: UIView, layoutBlock: @escaping LayoutBlock, notificationCenter: NotificationCenter) {
self.view = viewController.view
self.layoutBlock = layoutBlock
self.inputContainer = inputContainer
self.notificationCenter = notificationCenter
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)
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)
}
deinit {
@ -79,60 +79,60 @@ class KeyboardTracker {
}
@objc
private func keyboardWillShow(notification: NSNotification) {
private func keyboardWillShow(_ notification: Notification) {
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: NSNotification) {
private func keyboardDidShow(_ notification: Notification) {
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: NSNotification) {
private func keyboardWillChangeFrame(_ notification: Notification) {
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: NSNotification) {
private func keyboardWillHide(_ notification: Notification) {
guard self.isTracking else { return }
self.keyboardStatus = .Hidden
self.keyboardStatus = .hidden
self.layoutInputAtBottom()
}
private func bottomConstraintFromNotification(notification: NSNotification) -> CGFloat {
guard let rect = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.CGRectValue() else { return 0 }
private func bottomConstraintFromNotification(_ notification: Notification) -> CGFloat {
guard let rect = ((notification as NSNotification).userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { return 0 }
guard rect.height > 0 else { return 0 }
let rectInView = self.view.convertRect(rect, fromView: nil)
let rectInView = self.view.convert(rect, from: 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.convertRect(self.keyboardTrackerView.bounds, fromView: self.keyboardTrackerView)
let trackingViewRect = self.view.convert(self.keyboardTrackerView.bounds, from: 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(bottomMargin: constraint)
self.layoutBlock(constraint)
self.isPerformingForcedLayout = false
}
}
@ -185,14 +185,14 @@ private class KeyboardTrackingView: UIView {
self.commonInit()
}
private func commonInit() {
self.autoresizingMask = .FlexibleHeight
self.userInteractionEnabled = false
self.backgroundColor = UIColor.clearColor()
self.hidden = true
func commonInit() {
self.autoresizingMask = .flexibleHeight
self.isUserInteractionEnabled = false
self.backgroundColor = UIColor.clear
self.isHidden = true
}
private var preferredSize: CGSize = .zero {
var preferredSize: CGSize = .zero {
didSet {
if oldValue != self.preferredSize {
self.invalidateIntrinsicContentSize()
@ -201,30 +201,30 @@ private class KeyboardTrackingView: UIView {
}
}
private override func intrinsicContentSize() -> CGSize {
override var intrinsicContentSize: CGSize {
return self.preferredSize
}
override func willMoveToSuperview(newSuperview: UIView?) {
override func willMove(toSuperview 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.willMoveToSuperview(newSuperview)
super.willMove(toSuperview: newSuperview)
}
private override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
guard let object = object, superview = self.superview else { return }
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 }
if object === superview {
guard let sChange = change else { return }
let oldCenter = (sChange[NSKeyValueChangeOldKey] as! NSValue).CGPointValue()
let newCenter = (sChange[NSKeyValueChangeNewKey] as! NSValue).CGPointValue()
let oldCenter = (sChange[NSKeyValueChangeKey.oldKey] as! NSValue).cgPointValue
let newCenter = (sChange[NSKeyValueChangeKey.newKey] as! NSValue).cgPointValue
if oldCenter != newCenter {
self.positionChangedCallback?()
}

View File

@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.1.0</string>
<string>3.0.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>

View File

@ -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 where T: UniqueIdentificable>: CollectionType {
public struct ReadOnlyOrderedDictionary<T>: Collection where T: UniqueIdentificable {
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.enumerate() {
for (index, item) in items.enumerated() {
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,8 +52,20 @@ public struct ReadOnlyOrderedDictionary<T where T: UniqueIdentificable>: Collect
return nil
}
public func generate() -> IndexingGenerator<[T]> {
return self.items.generate()
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 var startIndex: Int {

View File

@ -24,10 +24,10 @@
import Foundation
public typealias TaskClosure = (completion: () -> Void) -> Void
public typealias TaskClosure = (_ completion: @escaping () -> Void) -> Void
public protocol SerialTaskQueueProtocol {
func addTask(task: TaskClosure)
func addTask(_ task: @escaping TaskClosure)
func start()
func stop()
func flushQueue()
@ -43,7 +43,7 @@ public final class SerialTaskQueue: SerialTaskQueueProtocol {
public init() {}
public func addTask(task: TaskClosure) {
public func addTask(_ task: @escaping 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(completion: { [weak self] () -> Void in
firstTask({ [weak self] () -> Void in
self?.isBusy = false
self?.maybeExecuteNextTask()
})

View File

@ -24,9 +24,9 @@
import Foundation
private let scale = UIScreen.mainScreen().scale
private let scale = UIScreen.main.scale
infix operator >=~ { }
infix operator >=~
func >=~ (lhs: CGFloat, rhs: CGFloat) -> Bool {
return round(lhs * scale) >= round(rhs * scale)
}

View File

@ -30,6 +30,7 @@ class BaseChatItemPresenterTests: XCTestCase {
var presenter: BaseChatItemPresenter<UICollectionViewCell>!
override func setUp() {
super.setUp()
self.presenter = BaseChatItemPresenter()
}

View File

@ -24,7 +24,7 @@ THE SOFTWARE.
@testable import Chatto
func createFakeChatItems(count count: Int) -> [ChatItemProtocol] {
func createFakeChatItems(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 preferredMaxCount: Int?, focusPosition: Double, completion:(didAdjust: Bool) -> Void) {
func adjustNumberOfMessages(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.registerClass(FakeCell.self, forCellWithReuseIdentifier: "fake-cell")
override class func registerCells(_ collectionView: UICollectionView) {
collectionView.register(FakeCell.self, forCellWithReuseIdentifier: "fake-cell")
}
override func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat {
return 10
}
override func dequeueCell(collectionView collectionView: UICollectionView, indexPath: NSIndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCellWithReuseIdentifier("fake-cell", forIndexPath: indexPath)
override func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCell(withReuseIdentifier: "fake-cell", for: indexPath as IndexPath)
}
override func configureCell(cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) {
override func configureCell(_ cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) {
let fakeCell = cell as! FakeCell
fakeCell.backgroundColor = UIColor.redColor()
fakeCell.backgroundColor = UIColor.red
}
}
@ -127,13 +127,14 @@ class FakeChatItem: ChatItemProtocol {
}
final class SerialTaskQueueTestHelper: SerialTaskQueueProtocol {
var onAllTasksFinished: (() -> Void)?
var isBusy = false
var isStopped = true
var tasksQueue = [TaskClosure]()
func addTask(task: TaskClosure) {
func addTask(_ task: @escaping TaskClosure) {
self.tasksQueue.append(task)
self.maybeExecuteNextTask()
}
@ -160,7 +161,7 @@ final class SerialTaskQueueTestHelper: SerialTaskQueueProtocol {
if !self.isEmpty {
let firstTask = self.tasksQueue.removeFirst()
self.isBusy = true
firstTask(completion: { [weak self] () -> Void in
firstTask({ [weak self] () -> Void in
self?.isBusy = false
self?.maybeExecuteNextTask()
})

View File

@ -61,7 +61,7 @@ class ChatViewControllerTests: XCTestCase {
}
func testThat_WhenDataSourceChanges_ThenCollectionViewUpdatesAsynchronously() {
let asyncExpectation = expectationWithDescription("update")
let asyncExpectation = expectation(description: "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.waitForExpectationsWithTimeout(1) { (error) -> Void in
self.waitForExpectations(timeout: 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 = expectationWithDescription("update")
let asyncExpectation = expectation(description: "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.waitForExpectationsWithTimeout(1) { (error) -> Void in
self.waitForExpectations(timeout: 1) { (error) -> Void in
XCTAssertTrue(fakeDataSource.wasRequestedForPrevious)
}
}
func testThat_WhenLoadsNextPage_ThenPreservesScrollPosition() {
let asyncExpectation = expectationWithDescription("update")
let asyncExpectation = expectation(description: "update")
let presenterBuilder = FakePresenterBuilder()
let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]])
let fakeDataSource = FakeDataSource()
@ -129,14 +129,14 @@ class ChatViewControllerTests: XCTestCase {
completion()
}
self.waitForExpectationsWithTimeout(1) { (error) -> Void in
self.waitForExpectations(timeout: 1) { (error) -> Void in
XCTAssertEqual(3000, controller.collectionView(controller.collectionView, numberOfItemsInSection: 0))
XCTAssertEqual(contentOffset, controller.collectionView.contentOffset)
}
}
func testThat_WhenManyMessagesAreLoaded_ThenRequestForMessageCountContention() {
let asyncExpectation = expectationWithDescription("update")
let asyncExpectation = expectation(description: "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.waitForExpectationsWithTimeout(1) { (error) -> Void in
self.waitForExpectations(timeout: 1) { (error) -> Void in
XCTAssertTrue(fakeDataSource.wasRequestedForMessageCountContention)
}
}
func testThat_WhenUpdatesFinish_ControllerIsNotRetained() {
let asyncExpectation = expectationWithDescription("update")
let asyncExpectation = expectation(description: "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.waitForExpectationsWithTimeout(1) { (error) -> Void in
self.waitForExpectations(timeout: 1) { (error) -> Void in
controller = nil
XCTAssertNil(weakController)
}
@ -193,28 +193,28 @@ class ChatViewControllerTests: XCTestCase {
func testThat_LayoutAdaptsWhenKeyboardIsShown() {
let controller = TesteableChatViewController()
let notificationCenter = NSNotificationCenter()
let notificationCenter = NotificationCenter()
controller.notificationCenter = notificationCenter
let fakeDataSource = FakeDataSource()
fakeDataSource.chatItems = createFakeChatItems(count: 2)
controller.chatDataSource = fakeDataSource
self.fakeDidAppearAndLayout(controller: controller)
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)
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)
}
func testThat_LayoutAdaptsWhenKeyboardIsHidden() {
let controller = TesteableChatViewController()
let notificationCenter = NSNotificationCenter()
let notificationCenter = NotificationCenter()
controller.notificationCenter = notificationCenter
let fakeDataSource = FakeDataSource()
fakeDataSource.chatItems = createFakeChatItems(count: 2)
controller.chatDataSource = fakeDataSource
self.fakeDidAppearAndLayout(controller: controller)
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)
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)
}
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 controller: TesteableChatViewController) {
private func fakeDidAppearAndLayout(controller: TesteableChatViewController) {
controller.view.frame = CGRect(x: 0, y: 0, width: 400, height: 900)
controller.viewWillAppear(true)
controller.viewDidAppear(true)

View File

@ -33,7 +33,8 @@ class ChatCollectionViewLayoutModelTests: XCTestCase {
XCTAssertEqual(width, layoutModel.calculatedForWidth)
XCTAssertEqual(CGSize(width: 320, height: 0), layoutModel.contentSize)
XCTAssertEqual([], layoutModel.layoutAttributes)
XCTAssertEqual([[]], layoutModel.layoutAttributesBySectionAndItem)
XCTAssertEqual(1, layoutModel.layoutAttributesBySectionAndItem.count)
XCTAssertEqual([], layoutModel.layoutAttributesBySectionAndItem.first!)
}
func testThatLayoutIsCorrectlyCreated() {
@ -49,14 +50,14 @@ class ChatCollectionViewLayoutModelTests: XCTestCase {
XCTAssertEqual(width, layoutModel.calculatedForWidth)
XCTAssertEqual(CGSize(width: 320, height: 28), layoutModel.contentSize)
XCTAssertEqual(expectedLayoutAttributes, layoutModel.layoutAttributes)
XCTAssertEqual([expectedLayoutAttributes], layoutModel.layoutAttributesBySectionAndItem)
XCTAssertEqual(1, layoutModel.layoutAttributesBySectionAndItem.count)
XCTAssertEqual(expectedLayoutAttributes, layoutModel.layoutAttributesBySectionAndItem.first!)
}
}
private func Atttributes(item item: Int, frame: CGRect) -> UICollectionViewLayoutAttributes {
let indexPath = NSIndexPath(forItem: item, inSection: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
private func Atttributes(item: Int, frame: CGRect) -> UICollectionViewLayoutAttributes {
let indexPath = IndexPath(item: item, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = frame
return attributes
}

View File

@ -36,8 +36,8 @@ class CollectionChangesTests: XCTestCase {
func testThatDoesNotGenerateChangesForEqualCollections() {
let changes = generateChanges(
oldCollection: [Item("a"), Item("b")],
newCollection: [Item("a"), Item("b")]
oldCollection: [Item(uid: "a"), Item(uid: "b")],
newCollection: [Item(uid: "a"), Item(uid: "b")]
)
XCTAssertEqual(changes.insertedIndexPaths, [])
XCTAssertEqual(changes.deletedIndexPaths, [])
@ -47,27 +47,27 @@ class CollectionChangesTests: XCTestCase {
func testThatGeneratesInsertions() {
let changes = generateChanges(
oldCollection: [],
newCollection: [Item("a"), Item("b")]
newCollection: [Item(uid: "a"), Item(uid: "b")]
)
XCTAssertEqual(changes.deletedIndexPaths, [])
XCTAssertEqual(changes.movedIndexPaths, [])
XCTAssertEqual(Set(changes.insertedIndexPaths), Set([NSIndexPath(forItem: 0, inSection: 0), NSIndexPath(forItem: 1, inSection: 0)]))
XCTAssertEqual(Set(changes.insertedIndexPaths), Set([IndexPath(item: 0, section: 0), IndexPath(item: 1, section: 0)]))
}
func testThatGeneratesDeletions() {
let changes = generateChanges(
oldCollection: [Item("a"), Item("b")],
oldCollection: [Item(uid: "a"), Item(uid: "b")],
newCollection: []
)
XCTAssertEqual(changes.deletedIndexPaths, Set([NSIndexPath(forItem: 0, inSection: 0), NSIndexPath(forItem: 1, inSection: 0)]))
XCTAssertEqual(changes.deletedIndexPaths, Set([IndexPath(item: 0, section: 0), IndexPath(item: 1, section: 0)]))
XCTAssertEqual(changes.movedIndexPaths.count, 0)
XCTAssertEqual(changes.insertedIndexPaths.count, 0)
}
func testThatGeneratesMovements() {
let changes = generateChanges(
oldCollection: [Item("a"), Item("b"), Item("c")],
newCollection: [Item("a"), Item("c"), Item("b")]
oldCollection: [Item(uid: "a"), Item(uid: "b"), Item(uid: "c")],
newCollection: [Item(uid: "a"), Item(uid: "c"), Item(uid: "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("a"), Item("b"), Item("c")],
newCollection: [Item("d"), Item("c"), Item("a")]
oldCollection: [Item(uid: "a"), Item(uid: "b"), Item(uid: "c")],
newCollection: [Item(uid: "d"), Item(uid: "c"), Item(uid: "a")]
)
XCTAssertEqual(changes.deletedIndexPaths, [NSIndexPath(forItem: 1, inSection: 0)])
XCTAssertEqual(changes.insertedIndexPaths, [NSIndexPath(forItem: 0, inSection: 0)])
XCTAssertEqual(changes.deletedIndexPaths, [IndexPath(item: 1, section: 0)])
XCTAssertEqual(changes.insertedIndexPaths, [IndexPath(item: 0, section: 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 = 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 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 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: NSIndexPath(forItem: from, inSection: 0), indexPathNew: NSIndexPath(forItem: to, inSection: 0))
func Move(_ from: Int, to: Int) -> CollectionChangeMove {
return CollectionChangeMove(indexPathOld: IndexPath(item: from, section: 0), indexPathNew: IndexPath(item: to, section: 0))
}
struct UniqueIdentificableItem: UniqueIdentificable {

View File

@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>2.1.0</string>
<string>3.0.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>

View File

@ -29,6 +29,7 @@ class ReadOnlyOrderedDictionaryTests: XCTestCase {
var orderedDictionary: ReadOnlyOrderedDictionary<FakeChatItem>!
override func setUp() {
super.setUp()
let items = [
FakeChatItem(uid: "3", type: "type3"),
FakeChatItem(uid: "1", type: "type1"),
@ -38,7 +39,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() {

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "ChattoAdditions"
s.version = "2.1.0"
s.version = "3.0.1"
s.summary = "UI componentes for Chatto"
s.description = <<-DESC
Text and photo bubbles

View File

@ -701,7 +701,7 @@
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 2.3;
SWIFT_VERSION = 3.0;
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 = 2.3;
SWIFT_VERSION = 3.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";

View File

@ -23,13 +23,13 @@
*/
public extension CABasicAnimation {
class func bma_fadeInAnimationWithDuration(duration: CFTimeInterval) -> CABasicAnimation {
let animation = CABasicAnimation.init(keyPath: "opacity")
class func bma_fadeInAnimationWithDuration(_ duration: CFTimeInterval) -> CABasicAnimation {
let animation = CABasicAnimation(keyPath: "opacity")
animation.duration = duration
animation.fromValue = 0
animation.toValue = 1
animation.fillMode = kCAFillModeForwards
animation.additive = false
animation.isAdditive = false
return animation
}
}

View File

@ -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: NSDate { get }
var date: Date { get }
var status: MessageStatus { get }
}
@ -59,7 +59,7 @@ public extension DecoratedMessageModelProtocol {
return self.messageModel.isIncoming
}
var date: NSDate {
var date: Date {
return self.messageModel.date
}
@ -68,15 +68,15 @@ public extension DecoratedMessageModelProtocol {
}
}
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
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 init(uid: String, senderId: String, type: String, isIncoming: Bool, date: NSDate, status: MessageStatus) {
public init(uid: String, senderId: String, type: String, isIncoming: Bool, date: Date, status: MessageStatus) {
self.uid = uid
self.senderId = senderId
self.type = type

View File

@ -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 viewModel: ViewModelT, failIconView: UIView)
func userDidTapOnAvatar(viewModel viewModel: ViewModelT)
func userDidTapOnBubble(viewModel viewModel: ViewModelT)
func userDidBeginLongPressOnBubble(viewModel viewModel: ViewModelT)
func userDidEndLongPressOnBubble(viewModel viewModel: ViewModelT)
func userDidTapOnFailIcon(viewModel: ViewModelT, failIconView: UIView)
func userDidTapOnAvatar(viewModel: ViewModelT)
func userDidTapOnBubble(viewModel: ViewModelT)
func userDidBeginLongPressOnBubble(viewModel: ViewModelT)
func userDidEndLongPressOnBubble(viewModel: ViewModelT)
}
public class BaseMessagePresenter<BubbleViewT, ViewModelBuilderT, InteractionHandlerT where
open class BaseMessagePresenter<BubbleViewT, ViewModelBuilderT, InteractionHandlerT>: BaseChatItemPresenter<BaseMessageCollectionViewCell<BubbleViewT>> where
ViewModelBuilderT: ViewModelBuilderProtocol,
ViewModelBuilderT.ViewModelT: MessageViewModelProtocol,
InteractionHandlerT: BaseMessageInteractionHandlerProtocol,
InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT,
BubbleViewT: UIView, BubbleViewT:MaximumLayoutWidthSpecificable, BubbleViewT: BackgroundSizingQueryable>: BaseChatItemPresenter<BaseMessageCollectionViewCell<BubbleViewT>> {
BubbleViewT: UIView, BubbleViewT:MaximumLayoutWidthSpecificable, BubbleViewT: BackgroundSizingQueryable {
public typealias CellT = BaseMessageCollectionViewCell<BubbleViewT>
public typealias ModelT = ViewModelBuilderT.ModelT
public typealias ViewModelT = ViewModelBuilderT.ViewModelT
@ -74,12 +74,12 @@ public class BaseMessagePresenter<BubbleViewT, ViewModelBuilderT, InteractionHan
return self.createViewModel()
}()
public func createViewModel() -> ViewModelT {
open 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 @@ public class BaseMessagePresenter<BubbleViewT, ViewModelBuilderT, InteractionHan
}
public var decorationAttributes: ChatItemDecorationAttributes!
public func configureCell(cell: CellT, decorationAttributes: ChatItemDecorationAttributes, animated: Bool, additionalConfiguration: (() -> Void)?) {
open func configureCell(_ cell: CellT, decorationAttributes: ChatItemDecorationAttributes, animated: Bool, additionalConfiguration: (() -> Void)?) {
cell.performBatchUpdates({ () -> Void in
self.messageViewModel.showsTail = decorationAttributes.showsTail
cell.avatarView.hidden = !decorationAttributes.canShowAvatar
cell.bubbleView.userInteractionEnabled = true // just in case something went wrong while showing UIMenuController
cell.avatarView.isHidden = !decorationAttributes.canShowAvatar
cell.bubbleView.isUserInteractionEnabled = 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,74 +125,73 @@ public class BaseMessagePresenter<BubbleViewT, ViewModelBuilderT, InteractionHan
}, animated: animated, completion: nil)
}
public override func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat {
open 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: CGFloat.max)).height
return self.sizingCell.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude)).height
}
public override var canCalculateHeightInBackground: Bool {
open override var canCalculateHeightInBackground: Bool {
return self.sizingCell.canCalculateSizeInBackground
}
public override func cellWillBeShown() {
open override func cellWillBeShown() {
self.messageViewModel.willBeShown()
}
public override func cellWasHidden() {
open override func cellWasHidden() {
self.messageViewModel.wasHidden()
}
public override func shouldShowMenu() -> Bool {
open 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.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)
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)
return true
}
@objc
func willShowMenu(notification: NSNotification) {
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIMenuControllerWillShowMenuNotification, object: nil)
guard let cell = self.cell, menuController = notification.object as? UIMenuController else {
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 {
assert(false, "Investigate -> Fix or remove assert")
return
}
cell.bubbleView.userInteractionEnabled = true
cell.bubbleView.isUserInteractionEnabled = true
menuController.setMenuVisible(false, animated: false)
menuController.setTargetRect(cell.bubbleView.bounds, inView: cell.bubbleView)
menuController.setTargetRect(cell.bubbleView.bounds, in: cell.bubbleView)
menuController.setMenuVisible(true, animated: true)
}
public func canShowMenu() -> Bool {
open func canShowMenu() -> Bool {
// Override in subclass
return false
}
public func onCellBubbleTapped() {
open func onCellBubbleTapped() {
self.interactionHandler?.userDidTapOnBubble(viewModel: self.messageViewModel)
}
public func onCellBubbleLongPressBegan() {
open func onCellBubbleLongPressBegan() {
self.interactionHandler?.userDidBeginLongPressOnBubble(viewModel: self.messageViewModel)
}
public func onCellBubbleLongPressEnded() {
open func onCellBubbleLongPressEnded() {
self.interactionHandler?.userDidEndLongPressOnBubble(viewModel: self.messageViewModel)
}
public func onCellAvatarTapped() {
open func onCellAvatarTapped() {
self.interactionHandler?.userDidTapOnAvatar(viewModel: self.messageViewModel)
}
public func onCellFailedButtonTapped(failedButtonView: UIView) {
open func onCellFailedButtonTapped(_ failedButtonView: UIView) {
self.interactionHandler?.userDidTapOnFailIcon(viewModel: self.messageViewModel, failIconView: failedButtonView)
}
}

View File

@ -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 {
}
}
public class MessageViewModel: MessageViewModelProtocol {
public var isIncoming: Bool {
open class MessageViewModel: MessageViewModelProtocol {
open var isIncoming: Bool {
return self.messageModel.isIncoming
}
public var status: MessageViewModelStatus {
open var status: MessageViewModelStatus {
return self.messageModel.status.viewModelStatus()
}
public var showsTail: Bool
public lazy var date: String = {
return self.dateFormatter.stringFromDate(self.messageModel.date)
open var showsTail: Bool
open lazy var date: String = {
return self.dateFormatter.string(from: self.messageModel.date as Date)
}()
public let dateFormatter: NSDateFormatter
public let dateFormatter: DateFormatter
public private(set) var messageModel: MessageModelProtocol
public init(dateFormatter: NSDateFormatter, showsTail: Bool, messageModel: MessageModelProtocol, avatarImage: UIImage?) {
public init(dateFormatter: DateFormatter, showsTail: Bool, messageModel: MessageModelProtocol, avatarImage: UIImage?) {
self.dateFormatter = dateFormatter
self.showsTail = showsTail
self.messageModel = messageModel
self.avatarImage = Observable<UIImage?>(avatarImage)
}
public var showsFailedIcon: Bool {
return self.status == .Failed
open var showsFailedIcon: Bool {
return self.status == .failed
}
public var avatarImage: Observable<UIImage?>
@ -132,15 +132,15 @@ public class MessageViewModelDefaultBuilder {
public init() {}
static let dateFormatter: NSDateFormatter = {
let formatter = NSDateFormatter()
formatter.locale = NSLocale.currentLocale()
formatter.dateStyle = .NoStyle
formatter.timeStyle = .ShortStyle
static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale.current
formatter.dateStyle = .none
formatter.timeStyle = .short
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)
}

View File

@ -26,12 +26,12 @@ import UIKit
import Chatto
public protocol BaseMessageCollectionViewCellStyleProtocol {
func avatarSize(viewModel viewModel: MessageViewModelProtocol) -> CGSize // .zero => no avatar
func avatarVerticalAlignment(viewModel viewModel: MessageViewModelProtocol) -> VerticalAlignment
func avatarSize(viewModel: MessageViewModelProtocol) -> CGSize // .zero => no avatar
func avatarVerticalAlignment(viewModel: MessageViewModelProtocol) -> VerticalAlignment
var failedIcon: UIImage { get }
var failedIconHighlighted: UIImage { get }
func attributedStringForDate(date: String) -> NSAttributedString
func layoutConstants(viewModel viewModel: MessageViewModelProtocol) -> BaseMessageCollectionViewCellLayoutConstants
func attributedStringForDate(_ date: String) -> NSAttributedString
func layoutConstants(viewModel: MessageViewModelProtocol) -> BaseMessageCollectionViewCellLayoutConstants
}
public struct BaseMessageCollectionViewCellLayoutConstants {
@ -65,13 +65,13 @@ public struct BaseMessageCollectionViewCellLayoutConstants {
- Have a BubbleViewType that responds properly to sizeThatFits:
*/
public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:UIView, BubbleViewType:MaximumLayoutWidthSpecificable, BubbleViewType: BackgroundSizingQueryable>: UICollectionViewCell, BackgroundSizingQueryable, AccessoryViewRevealable, UIGestureRecognizerDelegate {
open class BaseMessageCollectionViewCell<BubbleViewType>: UICollectionViewCell, BackgroundSizingQueryable, AccessoryViewRevealable, UIGestureRecognizerDelegate where BubbleViewType:UIView, BubbleViewType:MaximumLayoutWidthSpecificable, BubbleViewType: BackgroundSizingQueryable {
public var animationDuration: CFTimeInterval = 0.33
public var viewContext: ViewContext = .Normal
open var viewContext: ViewContext = .normal
public private(set) var isUpdating: Bool = false
public func performBatchUpdates(updateClosure: () -> Void, animated: Bool, completion: (() ->())?) {
open func performBatchUpdates(_ updateClosure: @escaping () -> Void, animated: Bool, completion: (() ->())?) {
self.isUpdating = true
let updateAndRefreshViews = {
updateClosure()
@ -82,7 +82,7 @@ public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:U
}
}
if animated {
UIView.animateWithDuration(self.animationDuration, animations: updateAndRefreshViews, completion: { (finished) -> Void in
UIView.animate(withDuration: self.animationDuration, animations: updateAndRefreshViews, completion: { (finished) -> Void in
completion?()
})
} else {
@ -90,7 +90,7 @@ public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:U
}
}
public var messageViewModel: MessageViewModelProtocol! {
open var messageViewModel: MessageViewModelProtocol! {
didSet {
updateViews()
}
@ -106,20 +106,20 @@ public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:U
}
}
override public var selected: Bool {
override open var isSelected: Bool {
didSet {
if oldValue != self.selected {
if oldValue != self.isSelected {
self.updateViews()
}
}
}
public var canCalculateSizeInBackground: Bool {
open var canCalculateSizeInBackground: Bool {
return self.bubbleView.canCalculateSizeInBackground
}
public private(set) var bubbleView: BubbleViewType!
public func createBubbleView() -> BubbleViewType! {
open func createBubbleView() -> BubbleViewType! {
assert(false, "Override in subclass")
return nil
}
@ -127,7 +127,7 @@ public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:U
public private(set) var avatarView: UIImageView!
func createAvatarView() -> UIImageView! {
let avatarImageView = UIImageView(frame: CGRect.zero)
avatarImageView.userInteractionEnabled = true
avatarImageView.isUserInteractionEnabled = true
return avatarImageView
}
@ -161,44 +161,44 @@ public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:U
self.avatarView = self.createAvatarView()
self.avatarView.addGestureRecognizer(self.avatarTapGestureRecognizer)
self.bubbleView = self.createBubbleView()
self.bubbleView.exclusiveTouch = true
self.bubbleView.isExclusiveTouch = 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.exclusiveTouch = true
self.exclusiveTouch = true
self.contentView.isExclusiveTouch = true
self.isExclusiveTouch = true
}
public func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
return self.bubbleView.bounds.contains(touch.locationInView(self.bubbleView))
open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
return self.bubbleView.bounds.contains(touch.location(in: self.bubbleView))
}
public func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return gestureRecognizer === self.longPressGestureRecognizer
}
public override func prepareForReuse() {
open override func prepareForReuse() {
super.prepareForReuse()
self.removeAccessoryView()
}
public lazy var failedButton: UIButton = {
let button = UIButton(type: .Custom)
button.addTarget(self, action: #selector(BaseMessageCollectionViewCell.failedButtonTapped), forControlEvents: .TouchUpInside)
public private(set) lazy var failedButton: UIButton = {
let button = UIButton(type: .custom)
button.addTarget(self, action: #selector(BaseMessageCollectionViewCell.failedButtonTapped), for: .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, style = self.baseStyle else { return }
guard let viewModel = self.messageViewModel, let style = self.baseStyle else { return }
if viewModel.showsFailedIcon {
self.failedButton.setImage(self.failedIcon, forState: .Normal)
self.failedButton.setImage(self.failedIconHighlighted, forState: .Highlighted)
self.failedButton.setImage(self.failedIcon, for: .normal)
self.failedButton.setImage(self.failedIconHighlighted, for: .highlighted)
self.failedButton.alpha = 1
} else {
self.failedButton.alpha = 0
@ -212,7 +212,7 @@ public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:U
}
// MARK: layout
public override func layoutSubviews() {
open override func layoutSubviews() {
super.layoutSubviews()
let layoutModel = self.calculateLayout(availableWidth: self.contentView.bounds.width)
@ -225,7 +225,7 @@ public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:U
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 @@ public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:U
}
}
public override func sizeThatFits(size: CGSize) -> CGSize {
open override func sizeThatFits(_ size: CGSize) -> CGSize {
return self.calculateLayout(availableWidth: size.width).size
}
private func calculateLayout(availableWidth availableWidth: CGFloat) -> BaseMessageLayoutModel {
private func calculateLayout(availableWidth: CGFloat) -> BaseMessageLayoutModel {
let layoutConstants = baseStyle.layoutConstants(viewModel: messageViewModel)
let parameters = BaseMessageLayoutModelParameters(
containerWidth: availableWidth,
@ -263,7 +263,6 @@ public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:U
return layoutModel
}
// MARK: timestamp revealing
lazy var accessoryTimestampView = UILabel()
@ -276,13 +275,12 @@ public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:U
public var allowAccessoryViewRevealing: Bool = true
public func preferredOffsetToRevealAccessoryView() -> CGFloat? {
open func preferredOffsetToRevealAccessoryView() -> CGFloat? {
let layoutConstants = baseStyle.layoutConstants(viewModel: messageViewModel)
return self.accessoryTimestampView.intrinsicContentSize().width + layoutConstants.horizontalTimestampMargin
return self.accessoryTimestampView.intrinsicContentSize.width + layoutConstants.horizontalTimestampMargin
}
public func revealAccessoryView(withOffset offset: CGFloat, animated: Bool) {
open func revealAccessoryView(withOffset offset: CGFloat, animated: Bool) {
self.offsetToRevealAccessoryView = offset
if self.accessoryTimestampView.superview == nil {
if offset > 0 {
@ -291,13 +289,13 @@ public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:U
}
if animated {
UIView.animateWithDuration(self.animationDuration, animations: { () -> Void in
UIView.animate(withDuration: self.animationDuration, animations: { () -> Void in
self.layoutIfNeeded()
})
}
} else {
if animated {
UIView.animateWithDuration(self.animationDuration, animations: { () -> Void in
UIView.animate(withDuration: self.animationDuration, animations: { () -> Void in
self.layoutIfNeeded()
}, completion: { (finished) -> Void in
if offset == 0 {
@ -313,33 +311,33 @@ public class BaseMessageCollectionViewCell<BubbleViewType where BubbleViewType:U
}
// MARK: User interaction
public var onFailedButtonTapped: ((cell: BaseMessageCollectionViewCell) -> Void)?
public var onFailedButtonTapped: ((_ cell: BaseMessageCollectionViewCell) -> Void)?
@objc
func failedButtonTapped() {
self.onFailedButtonTapped?(cell: self)
self.onFailedButtonTapped?(self)
}
public var onAvatarTapped: ((cell: BaseMessageCollectionViewCell) -> Void)?
public var onAvatarTapped: ((_ cell: BaseMessageCollectionViewCell) -> Void)?
@objc
func avatarTapped(tapGestureRecognizer: UITapGestureRecognizer) {
self.onAvatarTapped?(cell: self)
func avatarTapped(_ tapGestureRecognizer: UITapGestureRecognizer) {
self.onAvatarTapped?(self)
}
public var onBubbleTapped: ((cell: BaseMessageCollectionViewCell) -> Void)?
public var onBubbleTapped: ((_ cell: BaseMessageCollectionViewCell) -> Void)?
@objc
func bubbleTapped(tapGestureRecognizer: UITapGestureRecognizer) {
self.onBubbleTapped?(cell: self)
func bubbleTapped(_ tapGestureRecognizer: UITapGestureRecognizer) {
self.onBubbleTapped?(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?(cell: self)
case .Ended, .Cancelled:
self.onBubbleLongPressEnded?(cell: self)
case .began:
self.onBubbleLongPressBegan?(self)
case .ended, .cancelled:
self.onBubbleLongPressEnded?(self)
default:
break
}
@ -353,8 +351,7 @@ struct BaseMessageLayoutModel {
private (set) var avatarViewFrame = CGRect.zero
private (set) var preferredMaxWidthForBubble: CGFloat = 0
mutating func calculateLayout(parameters parameters: BaseMessageLayoutModelParameters) {
mutating func calculateLayout(parameters: BaseMessageLayoutModelParameters) {
let containerWidth = parameters.containerWidth
let isIncoming = parameters.isIncoming
let isFailed = parameters.isFailed
@ -365,13 +362,12 @@ struct BaseMessageLayoutModel {
let avatarSize = parameters.avatarSize
let preferredWidthForBubble = (containerWidth * parameters.maxContainerWidthPercentageForBubbleView).bma_round()
let bubbleSize = bubbleView.sizeThatFits(CGSize(width: preferredWidthForBubble, height: .max))
let bubbleSize = bubbleView.sizeThatFits(CGSize(width: preferredWidthForBubble, height: .greatestFiniteMagnitude))
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

View File

@ -24,7 +24,7 @@
import UIKit
public class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionViewCellStyleProtocol {
open class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionViewCellStyleProtocol {
typealias Class = BaseMessageCollectionViewCellDefaultStyle
@ -32,8 +32,8 @@ public class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionVie
let incoming: () -> UIColor
let outgoing: () -> UIColor
public init(
@autoclosure(escaping) incoming: () -> UIColor,
@autoclosure(escaping) outgoing: () -> UIColor) {
incoming: @autoclosure @escaping () -> UIColor,
outgoing: @autoclosure @escaping () -> UIColor) {
self.incoming = incoming
self.outgoing = outgoing
}
@ -45,10 +45,10 @@ public class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionVie
public let borderOutgoingTail: () -> UIImage
public let borderOutgoingNoTail: () -> UIImage
public init(
@autoclosure(escaping) borderIncomingTail: () -> UIImage,
@autoclosure(escaping) borderIncomingNoTail: () -> UIImage,
@autoclosure(escaping) borderOutgoingTail: () -> UIImage,
@autoclosure(escaping) borderOutgoingNoTail: () -> UIImage) {
borderIncomingTail: @autoclosure @escaping () -> UIImage,
borderIncomingNoTail: @autoclosure @escaping () -> UIImage,
borderOutgoingTail: @autoclosure @escaping () -> UIImage,
borderOutgoingNoTail: @autoclosure @escaping () -> UIImage) {
self.borderIncomingTail = borderIncomingTail
self.borderIncomingNoTail = borderIncomingNoTail
self.borderOutgoingTail = borderOutgoingTail
@ -60,8 +60,8 @@ public class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionVie
let normal: () -> UIImage
let highlighted: () -> UIImage
public init(
@autoclosure(escaping) normal: () -> UIImage,
@autoclosure(escaping) highlighted: () -> UIImage) {
normal: @autoclosure @escaping () -> UIImage,
highlighted: @autoclosure @escaping () -> UIImage) {
self.normal = normal
self.highlighted = highlighted
}
@ -71,8 +71,8 @@ public class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionVie
let font: () -> UIFont
let color: () -> UIColor
public init(
@autoclosure(escaping) font: () -> UIFont,
@autoclosure(escaping) color: () -> UIColor) {
font: @autoclosure @escaping () -> UIFont,
color: @autoclosure @escaping () -> UIColor) {
self.font = font
self.color = color
}
@ -81,7 +81,7 @@ public class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionVie
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 @@ public class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionVie
]
}()
public func attributedStringForDate(date: String) -> NSAttributedString {
open func attributedStringForDate(_ date: String) -> NSAttributedString {
return NSAttributedString(string: date, attributes: self.dateStringAttributes)
}
public func borderImage(viewModel viewModel: MessageViewModelProtocol) -> UIImage? {
open func borderImage(viewModel: MessageViewModelProtocol) -> UIImage? {
switch (viewModel.isIncoming, viewModel.showsTail) {
case (true, true):
return self.borderIncomingTail
@ -145,15 +145,15 @@ public class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionVie
}
}
public func avatarSize(viewModel viewModel: MessageViewModelProtocol) -> CGSize {
open func avatarSize(viewModel: MessageViewModelProtocol) -> CGSize {
return self.avatarStyle.size
}
public func avatarVerticalAlignment(viewModel viewModel: MessageViewModelProtocol) -> VerticalAlignment {
open func avatarVerticalAlignment(viewModel: MessageViewModelProtocol) -> VerticalAlignment {
return self.avatarStyle.alignment
}
public func layoutConstants(viewModel viewModel: MessageViewModelProtocol) -> BaseMessageCollectionViewCellLayoutConstants {
open func layoutConstants(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", 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)!
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)!
)
}
static public func createDefaultFailedIconImages() -> FailedIconImages {
let normal = {
return UIImage(named: "base-message-failed-icon", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!
return UIImage(named: "base-message-failed-icon", in: Bundle(for: Class.self), compatibleWith: nil)!
}
return FailedIconImages(
normal: normal(),
highlighted: normal().bma_blendWithColor(UIColor.blackColor().colorWithAlphaComponent(0.10))
highlighted: normal().bma_blendWithColor(UIColor.black.withAlphaComponent(0.10))
)
}
static public func createDefaultDateTextStyle() -> DateTextStyle {
return DateTextStyle(font: UIFont.systemFontOfSize(12), color: UIColor.bma_color(rgb: 0x9aa3ab))
return DateTextStyle(font: UIFont.systemFont(ofSize: 12), color: UIColor.bma_color(rgb: 0x9aa3ab))
}
static public func createDefaultLayoutConstants() -> BaseMessageCollectionViewCellLayoutConstants {

View File

@ -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 {

View File

@ -29,7 +29,7 @@ public protocol PhotoMessageModelProtocol: DecoratedMessageModelProtocol {
var imageSize: CGSize { get }
}
public class PhotoMessageModel<MessageModelT: MessageModelProtocol>: PhotoMessageModelProtocol {
open class PhotoMessageModel<MessageModelT: MessageModelProtocol>: PhotoMessageModelProtocol {
public var messageModel: MessageModelProtocol {
return self._messageModel
}

View File

@ -24,12 +24,12 @@
import Foundation
public class PhotoMessagePresenter<ViewModelBuilderT, InteractionHandlerT where
open class PhotoMessagePresenter<ViewModelBuilderT, InteractionHandlerT>
: BaseMessagePresenter<PhotoBubbleView, ViewModelBuilderT, InteractionHandlerT> where
ViewModelBuilderT: ViewModelBuilderProtocol,
ViewModelBuilderT.ViewModelT: PhotoMessageViewModelProtocol,
InteractionHandlerT: BaseMessageInteractionHandlerProtocol,
InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT>
: BaseMessagePresenter<PhotoBubbleView, ViewModelBuilderT, InteractionHandlerT> {
InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT {
public typealias ModelT = ViewModelBuilderT.ModelT
public typealias ViewModelT = ViewModelBuilderT.ViewModelT
@ -52,15 +52,15 @@ public class PhotoMessagePresenter<ViewModelBuilderT, InteractionHandlerT where
)
}
public override class func registerCells(collectionView: UICollectionView) {
collectionView.registerClass(PhotoMessageCollectionViewCell.self, forCellWithReuseIdentifier: "photo-message")
public final override class func registerCells(_ collectionView: UICollectionView) {
collectionView.register(PhotoMessageCollectionViewCell.self, forCellWithReuseIdentifier: "photo-message")
}
public override func dequeueCell(collectionView collectionView: UICollectionView, indexPath: NSIndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCellWithReuseIdentifier("photo-message", forIndexPath: indexPath)
public final override func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCell(withReuseIdentifier: "photo-message", for: indexPath)
}
public override func createViewModel() -> ViewModelBuilderT.ViewModelT {
open 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 @@ public class PhotoMessagePresenter<ViewModelBuilderT, InteractionHandlerT where
return nil
}
public override func configureCell(cell: BaseMessageCollectionViewCell<PhotoBubbleView>, decorationAttributes: ChatItemDecorationAttributes, animated: Bool, additionalConfiguration: (() -> Void)?) {
open 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 @@ public class PhotoMessagePresenter<ViewModelBuilderT, InteractionHandlerT where
}
public func updateCurrentCell() {
if let cell = self.photoCell, decorationAttributes = self.decorationAttributes {
self.configureCell(cell, decorationAttributes: decorationAttributes, animated: self.itemVisibility != .Appearing, additionalConfiguration: nil)
if let cell = self.photoCell, let decorationAttributes = self.decorationAttributes {
self.configureCell(cell, decorationAttributes: decorationAttributes, animated: self.itemVisibility != .appearing, additionalConfiguration: nil)
}
}
}

View File

@ -25,12 +25,11 @@
import Foundation
import Chatto
public class PhotoMessagePresenterBuilder<ViewModelBuilderT, InteractionHandlerT where
open class PhotoMessagePresenterBuilder<ViewModelBuilderT, InteractionHandlerT>: ChatItemPresenterBuilderProtocol where
ViewModelBuilderT: ViewModelBuilderProtocol,
ViewModelBuilderT.ViewModelT: PhotoMessageViewModelProtocol,
InteractionHandlerT: BaseMessageInteractionHandlerProtocol,
InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT
>: ChatItemPresenterBuilderProtocol {
InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT {
public typealias ModelT = ViewModelBuilderT.ModelT
public typealias ViewModelT = ViewModelBuilderT.ViewModelT
@ -47,11 +46,11 @@ public class PhotoMessagePresenterBuilder<ViewModelBuilderT, InteractionHandlerT
public lazy var photoCellStyle: PhotoMessageCollectionViewCellStyleProtocol = PhotoMessageCollectionViewCellDefaultStyle()
public lazy var baseCellStyle: BaseMessageCollectionViewCellStyleProtocol = BaseMessageCollectionViewCellDefaultStyle()
public func canHandleChatItem(chatItem: ChatItemProtocol) -> Bool {
open func canHandleChatItem(_ chatItem: ChatItemProtocol) -> Bool {
return self.viewModelBuilder.canCreateViewModel(fromModel: chatItem)
}
public func createPresenterWithChatItem(chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol {
open func createPresenterWithChatItem(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol {
assert(self.canHandleChatItem(chatItem))
return PhotoMessagePresenter<ViewModelBuilderT, InteractionHandlerT>(
messageModel: chatItem as! ModelT,
@ -63,7 +62,7 @@ public class PhotoMessagePresenterBuilder<ViewModelBuilderT, InteractionHandlerT
)
}
public var presenterType: ChatItemPresenterProtocol.Type {
open var presenterType: ChatItemPresenterProtocol.Type {
return PhotoMessagePresenter<ViewModelBuilderT, InteractionHandlerT>.self
}
}

View File

@ -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 }
}
public class PhotoMessageViewModel<PhotoMessageModelT: PhotoMessageModelProtocol>: PhotoMessageViewModelProtocol {
open 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?>
public var imageSize: CGSize {
open var imageSize: CGSize {
return self.photoMessage.imageSize
}
public let messageViewModel: MessageViewModelProtocol
public var showsFailedIcon: Bool {
return self.messageViewModel.showsFailedIcon || self.transferStatus.value == .Failed
open var showsFailedIcon: Bool {
return self.messageViewModel.showsFailedIcon || self.transferStatus.value == .failed
}
public init(photoMessage: PhotoMessageModelT, messageViewModel: MessageViewModelProtocol) {
@ -67,27 +67,27 @@ public class PhotoMessageViewModel<PhotoMessageModelT: PhotoMessageModelProtocol
self.messageViewModel = messageViewModel
}
public func willBeShown() {
open func willBeShown() {
// Need to declare empty. Otherwise subclass code won't execute (as of Xcode 7.2)
}
public func wasHidden() {
open func wasHidden() {
// Need to declare empty. Otherwise subclass code won't execute (as of Xcode 7.2)
}
}
public class PhotoMessageViewModelDefaultBuilder<PhotoMessageModelT: PhotoMessageModelProtocol>: ViewModelBuilderProtocol {
public init() { }
open class PhotoMessageViewModelDefaultBuilder<PhotoMessageModelT: PhotoMessageModelProtocol>: ViewModelBuilderProtocol {
public init() {}
let messageViewModelBuilder = MessageViewModelDefaultBuilder()
public func createViewModel(model: PhotoMessageModelT) -> PhotoMessageViewModel<PhotoMessageModelT> {
open func createViewModel(_ model: PhotoMessageModelT) -> PhotoMessageViewModel<PhotoMessageModelT> {
let messageViewModel = self.messageViewModelBuilder.createMessageViewModel(model)
let photoMessageViewModel = PhotoMessageViewModel(photoMessage: model, messageViewModel: messageViewModel)
return photoMessageViewModel
}
public func canCreateViewModel(fromModel model: Any) -> Bool {
open func canCreateViewModel(fromModel model: Any) -> Bool {
return model is PhotoMessageModelT
}
}

View File

@ -25,19 +25,19 @@
import UIKit
public protocol PhotoBubbleViewStyleProtocol {
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?
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?
}
public class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, BackgroundSizingQueryable {
open 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 @@ public class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, Background
public private(set) lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.autoresizingMask = .None
imageView.autoresizingMask = UIViewAutoresizing()
imageView.clipsToBounds = true
imageView.autoresizesSubviews = false
imageView.autoresizingMask = .None
imageView.contentMode = .ScaleAspectFill
imageView.autoresizingMask = UIViewAutoresizing()
imageView.contentMode = .scaleAspectFill
imageView.addSubview(self.borderView)
return imageView
}()
@ -78,12 +78,12 @@ public class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, Background
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 = .None
imageView.autoresizingMask = UIViewAutoresizing()
return imageView
}()
@ -100,7 +100,7 @@ public class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, Background
}
public private(set) var isUpdating: Bool = false
public func performBatchUpdates(updateClosure: () -> Void, animated: Bool, completion: (() ->())?) {
public func performBatchUpdates(_ updateClosure: @escaping () -> Void, animated: Bool, completion: (() ->())?) {
self.isUpdating = true
let updateAndRefreshViews = {
updateClosure()
@ -111,7 +111,7 @@ public class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, Background
}
}
if animated {
UIView.animateWithDuration(self.animationDuration, animations: updateAndRefreshViews, completion: { (finished) -> Void in
UIView.animate(withDuration: self.animationDuration, animations: updateAndRefreshViews, completion: { (finished) -> Void in
completion?()
})
} else {
@ -119,10 +119,10 @@ public class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, Background
}
}
public func updateViews() {
if self.viewContext == .Sizing { return }
open func updateViews() {
if self.viewContext == .sizing { return }
if isUpdating { return }
guard let _ = self.photoMessageViewModel, _ = self.photoMessageStyle else { return }
guard let _ = self.photoMessageViewModel, let _ = self.photoMessageStyle else { return }
self.updateProgressIndicator()
self.updateImages()
@ -132,23 +132,23 @@ public class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, Background
private func updateProgressIndicator() {
let transferStatus = self.photoMessageViewModel.transferStatus.value
let transferProgress = self.photoMessageViewModel.transferProgress.value
self.progressIndicatorView.hidden = [TransferStatus.Idle, TransferStatus.Success, TransferStatus.Failed].contains(self.photoMessageViewModel.transferStatus.value)
self.progressIndicatorView.isHidden = [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 @@ public class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, Background
private func updateImages() {
if let image = self.photoMessageViewModel.image.value {
self.imageView.image = image
self.placeholderIconView.hidden = true
self.placeholderIconView.isHidden = 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.hidden = false
self.placeholderIconView.isHidden = false
}
if let overlayColor = self.photoMessageStyle.overlayColor(viewModel: self.photoMessageViewModel) {
@ -178,14 +178,13 @@ public class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, Background
self.imageView.layer.mask = UIImageView(image: self.photoMessageStyle.maskingImage(viewModel: self.photoMessageViewModel)).layer
}
// MARK: Layout
public override func sizeThatFits(size: CGSize) -> CGSize {
open override func sizeThatFits(_ size: CGSize) -> CGSize {
return self.calculateTextBubbleLayout(maximumWidth: size.width).size
}
public override func layoutSubviews() {
open override func layoutSubviews() {
super.layoutSubviews()
let layout = self.calculateTextBubbleLayout(maximumWidth: self.preferredMaxLayoutWidth)
self.progressIndicatorView.center = layout.visualCenter
@ -197,20 +196,19 @@ public class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, Background
self.borderView.bma_rect = self.imageView.bounds
}
private func calculateTextBubbleLayout(maximumWidth maximumWidth: CGFloat) -> PhotoBubbleLayoutModel {
private func calculateTextBubbleLayout(maximumWidth: CGFloat) -> PhotoBubbleLayoutModel {
let layoutContext = PhotoBubbleLayoutModel.LayoutContext(photoMessageViewModel: self.photoMessageViewModel, style: self.photoMessageStyle, containerWidth: maximumWidth)
let layoutModel = PhotoBubbleLayoutModel(layoutContext: layoutContext)
layoutModel.calculateLayout()
return layoutModel
}
public var canCalculateSizeInBackground: Bool {
open 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

View File

@ -30,14 +30,10 @@ 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()
}
@ -61,7 +57,7 @@ public final class PhotoMessageCollectionViewCell: BaseMessageCollectionViewCell
}
}
public override func performBatchUpdates(updateClosure: () -> Void, animated: Bool, completion: (() -> Void)?) {
public override func performBatchUpdates(_ updateClosure: @escaping () -> Void, animated: Bool, completion: (() -> Void)?) {
super.performBatchUpdates({ () -> Void in
self.bubbleView.performBatchUpdates(updateClosure, animated: false, completion: nil)
}, animated: animated, completion: completion)

View File

@ -24,7 +24,7 @@
import UIKit
public class PhotoMessageCollectionViewCellDefaultStyle: PhotoMessageCollectionViewCellStyleProtocol {
open class PhotoMessageCollectionViewCellDefaultStyle: PhotoMessageCollectionViewCellStyleProtocol {
typealias Class = PhotoMessageCollectionViewCellDefaultStyle
public struct BubbleMasks {
@ -34,10 +34,10 @@ public class PhotoMessageCollectionViewCellDefaultStyle: PhotoMessageCollectionV
public let outgoingNoTail: () -> UIImage
public let tailWidth: CGFloat
public init(
@autoclosure(escaping) incomingTail: () -> UIImage,
@autoclosure(escaping) incomingNoTail: () -> UIImage,
@autoclosure(escaping) outgoingTail: () -> UIImage,
@autoclosure(escaping) outgoingNoTail: () -> UIImage,
incomingTail: @autoclosure @escaping () -> UIImage,
incomingNoTail: @autoclosure @escaping () -> UIImage,
outgoingTail: @autoclosure @escaping () -> UIImage,
outgoingNoTail: @autoclosure @escaping () -> UIImage,
tailWidth: CGFloat) {
self.incomingTail = incomingTail
self.incomingNoTail = incomingNoTail
@ -48,12 +48,12 @@ public class PhotoMessageCollectionViewCellDefaultStyle: PhotoMessageCollectionV
}
public struct Sizes {
public let aspectRatioIntervalForSquaredSize: ClosedInterval<CGFloat>
public let aspectRatioIntervalForSquaredSize: ClosedRange<CGFloat>
public let photoSizeLandscape: CGSize
public let photoSizePortrait: CGSize
public let photoSizeSquare: CGSize
public init(
aspectRatioIntervalForSquaredSize: ClosedInterval<CGFloat>,
aspectRatioIntervalForSquaredSize: ClosedRange<CGFloat>,
photoSizeLandscape: CGSize,
photoSizePortrait: CGSize,
photoSizeSquare: CGSize) {
@ -113,10 +113,10 @@ public class PhotoMessageCollectionViewCellDefaultStyle: PhotoMessageCollectionV
}()
lazy private var placeholderIcon: UIImage = {
return UIImage(named: "photo-bubble-placeholder-icon", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!
return UIImage(named: "photo-bubble-placeholder-icon", in: Bundle(for: Class.self), compatibleWith: nil)!
}()
public func maskingImage(viewModel viewModel: PhotoMessageViewModelProtocol) -> UIImage {
open func maskingImage(viewModel: PhotoMessageViewModelProtocol) -> UIImage {
switch (viewModel.isIncoming, viewModel.showsTail) {
case (true, true):
return self.maskImageIncomingTail
@ -129,44 +129,44 @@ public class PhotoMessageCollectionViewCellDefaultStyle: PhotoMessageCollectionV
}
}
public func borderImage(viewModel viewModel: PhotoMessageViewModelProtocol) -> UIImage? {
open func borderImage(viewModel: PhotoMessageViewModelProtocol) -> UIImage? {
return self.baseStyle.borderImage(viewModel: viewModel)
}
public func placeholderBackgroundImage(viewModel viewModel: PhotoMessageViewModelProtocol) -> UIImage {
open func placeholderBackgroundImage(viewModel: PhotoMessageViewModelProtocol) -> UIImage {
return viewModel.isIncoming ? self.placeholderBackgroundIncoming : self.placeholderBackgroundOutgoing
}
public func placeholderIconImage(viewModel viewModel: PhotoMessageViewModelProtocol) -> (icon: UIImage?, tintColor: UIColor?) {
if viewModel.image.value == nil && viewModel.transferStatus.value == .Failed {
open func placeholderIconImage(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)
}
public func tailWidth(viewModel viewModel: PhotoMessageViewModelProtocol) -> CGFloat {
open func tailWidth(viewModel: PhotoMessageViewModelProtocol) -> CGFloat {
return self.bubbleMasks.tailWidth
}
public func bubbleSize(viewModel viewModel: PhotoMessageViewModelProtocol) -> CGSize {
open func bubbleSize(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.start {
} else if aspectRatio < self.sizes.aspectRatioIntervalForSquaredSize.lowerBound {
return self.sizes.photoSizePortrait
} else {
return self.sizes.photoSizeLandscape
}
}
public func progressIndicatorColor(viewModel viewModel: PhotoMessageViewModelProtocol) -> UIColor {
open func progressIndicatorColor(viewModel: PhotoMessageViewModelProtocol) -> UIColor {
return viewModel.isIncoming ? self.colors.progressIndicatorColorIncoming : self.colors.progressIndicatorColorOutgoing
}
public func overlayColor(viewModel viewModel: PhotoMessageViewModelProtocol) -> UIColor? {
let showsOverlay = viewModel.image.value != nil && (viewModel.transferStatus.value == .Transfering || viewModel.status != MessageViewModelStatus.Success)
open func overlayColor(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", 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)!,
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)!,
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.whiteColor(),
overlayColor: UIColor.blackColor().colorWithAlphaComponent(0.70)
progressIndicatorColorOutgoing: UIColor.white,
overlayColor: UIColor.black.withAlphaComponent(0.70)
)
}
}

View File

@ -28,7 +28,7 @@ public protocol TextMessageModelProtocol: DecoratedMessageModelProtocol {
var text: String { get }
}
public class TextMessageModel<MessageModelT: MessageModelProtocol>: TextMessageModelProtocol {
open class TextMessageModel<MessageModelT: MessageModelProtocol>: TextMessageModelProtocol {
public var messageModel: MessageModelProtocol {
return self._messageModel
}

View File

@ -24,12 +24,12 @@
import UIKit
public class TextMessagePresenter<ViewModelBuilderT, InteractionHandlerT where
open class TextMessagePresenter<ViewModelBuilderT, InteractionHandlerT>
: BaseMessagePresenter<TextBubbleView, ViewModelBuilderT, InteractionHandlerT> where
ViewModelBuilderT: ViewModelBuilderProtocol,
ViewModelBuilderT.ViewModelT: TextMessageViewModelProtocol,
InteractionHandlerT: BaseMessageInteractionHandlerProtocol,
InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT>
: BaseMessagePresenter<TextBubbleView, ViewModelBuilderT, InteractionHandlerT> {
InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT {
public typealias ModelT = ViewModelBuilderT.ModelT
public typealias ViewModelT = ViewModelBuilderT.ViewModelT
@ -40,7 +40,7 @@ public class TextMessagePresenter<ViewModelBuilderT, InteractionHandlerT where
sizingCell: TextMessageCollectionViewCell,
baseCellStyle: BaseMessageCollectionViewCellStyleProtocol,
textCellStyle: TextMessageCollectionViewCellStyleProtocol,
layoutCache: NSCache) {
layoutCache: NSCache<AnyObject, AnyObject>) {
self.layoutCache = layoutCache
self.textCellStyle = textCellStyle
super.init(
@ -52,20 +52,20 @@ public class TextMessagePresenter<ViewModelBuilderT, InteractionHandlerT where
)
}
let layoutCache: NSCache
let layoutCache: NSCache<AnyObject, AnyObject>
let textCellStyle: TextMessageCollectionViewCellStyleProtocol
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 class func registerCells(_ collectionView: UICollectionView) {
collectionView.register(TextMessageCollectionViewCell.self, forCellWithReuseIdentifier: "text-message-incoming")
collectionView.register(TextMessageCollectionViewCell.self, forCellWithReuseIdentifier: "text-message-outcoming")
}
public override func dequeueCell(collectionView collectionView: UICollectionView, indexPath: NSIndexPath) -> UICollectionViewCell {
public final override func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell {
let identifier = self.messageViewModel.isIncoming ? "text-message-incoming" : "text-message-outcoming"
return collectionView.dequeueReusableCellWithReuseIdentifier(identifier, forIndexPath: indexPath)
return collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath)
}
public override func createViewModel() -> ViewModelBuilderT.ViewModelT {
open 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 @@ public class TextMessagePresenter<ViewModelBuilderT, InteractionHandlerT where
return nil
}
public override func configureCell(cell: BaseMessageCollectionViewCell<TextBubbleView>, decorationAttributes: ChatItemDecorationAttributes, animated: Bool, additionalConfiguration: (() -> Void)?) {
open 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,32 +100,24 @@ public class TextMessagePresenter<ViewModelBuilderT, InteractionHandlerT where
}
public func updateCurrentCell() {
if let cell = self.textCell, decorationAttributes = self.decorationAttributes {
self.configureCell(cell, decorationAttributes: decorationAttributes, animated: self.itemVisibility != .Appearing, additionalConfiguration: nil)
if let cell = self.textCell, let decorationAttributes = self.decorationAttributes {
self.configureCell(cell, decorationAttributes: decorationAttributes, animated: self.itemVisibility != .appearing, additionalConfiguration: nil)
}
}
public override func canShowMenu() -> Bool {
open override func canShowMenu() -> Bool {
return true
}
public override func canPerformMenuControllerAction(action: Selector) -> Bool {
#if swift(>=2.3)
let selector = #selector(UIResponderStandardEditActions.copy(_:))
#else
let selector = #selector(NSObject.copy(_:))
#endif
open override func canPerformMenuControllerAction(_ action: Selector) -> Bool {
let selector = #selector(UIResponderStandardEditActions.copy(_:))
return action == selector
}
public override func performMenuControllerAction(action: Selector) {
#if swift(>=2.3)
let selector = #selector(UIResponderStandardEditActions.copy(_:))
#else
let selector = #selector(NSObject.copy(_:))
#endif
open override func performMenuControllerAction(_ action: Selector) {
let selector = #selector(UIResponderStandardEditActions.copy(_:))
if action == selector {
UIPasteboard.generalPasteboard().string = self.messageViewModel.text
UIPasteboard.general.string = self.messageViewModel.text
} else {
assert(false, "Unexpected action")
}

View File

@ -25,12 +25,12 @@
import Foundation
import Chatto
public class TextMessagePresenterBuilder<ViewModelBuilderT, InteractionHandlerT where
open class TextMessagePresenterBuilder<ViewModelBuilderT, InteractionHandlerT>
: ChatItemPresenterBuilderProtocol where
ViewModelBuilderT: ViewModelBuilderProtocol,
ViewModelBuilderT.ViewModelT: TextMessageViewModelProtocol,
InteractionHandlerT: BaseMessageInteractionHandlerProtocol,
InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT>
: ChatItemPresenterBuilderProtocol {
InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT {
typealias ViewModelT = ViewModelBuilderT.ViewModelT
typealias ModelT = ViewModelBuilderT.ModelT
@ -43,14 +43,14 @@ public class TextMessagePresenterBuilder<ViewModelBuilderT, InteractionHandlerT
let viewModelBuilder: ViewModelBuilderT
let interactionHandler: InteractionHandlerT?
let layoutCache = NSCache()
let layoutCache = NSCache<AnyObject, AnyObject>()
lazy var sizingCell: TextMessageCollectionViewCell = {
var cell: TextMessageCollectionViewCell? = nil
if NSThread.isMainThread() {
if Thread.isMainThread {
cell = TextMessageCollectionViewCell.sizingCell()
} else {
dispatch_sync(dispatch_get_main_queue(), {
DispatchQueue.main.sync(execute: {
cell = TextMessageCollectionViewCell.sizingCell()
})
}
@ -61,11 +61,11 @@ public class TextMessagePresenterBuilder<ViewModelBuilderT, InteractionHandlerT
public lazy var textCellStyle: TextMessageCollectionViewCellStyleProtocol = TextMessageCollectionViewCellDefaultStyle()
public lazy var baseMessageStyle: BaseMessageCollectionViewCellStyleProtocol = BaseMessageCollectionViewCellDefaultStyle()
public func canHandleChatItem(chatItem: ChatItemProtocol) -> Bool {
open func canHandleChatItem(_ chatItem: ChatItemProtocol) -> Bool {
return self.viewModelBuilder.canCreateViewModel(fromModel: chatItem)
}
public func createPresenterWithChatItem(chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol {
open func createPresenterWithChatItem(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol {
assert(self.canHandleChatItem(chatItem))
return TextMessagePresenter<ViewModelBuilderT, InteractionHandlerT>(
messageModel: chatItem as! ModelT,
@ -78,7 +78,7 @@ public class TextMessagePresenterBuilder<ViewModelBuilderT, InteractionHandlerT
)
}
public var presenterType: ChatItemPresenterProtocol.Type {
open var presenterType: ChatItemPresenterProtocol.Type {
return TextMessagePresenter<ViewModelBuilderT, InteractionHandlerT>.self
}
}

View File

@ -28,7 +28,7 @@ public protocol TextMessageViewModelProtocol: DecoratedMessageViewModelProtocol
var text: String { get }
}
public class TextMessageViewModel<TextMessageModelT: TextMessageModelProtocol>: TextMessageViewModelProtocol {
open class TextMessageViewModel<TextMessageModelT: TextMessageModelProtocol>: TextMessageViewModelProtocol {
public var text: String {
return self.textMessage.text
}
@ -40,27 +40,27 @@ public class TextMessageViewModel<TextMessageModelT: TextMessageModelProtocol>:
self.messageViewModel = messageViewModel
}
public func willBeShown() {
open func willBeShown() {
// Need to declare empty. Otherwise subclass code won't execute (as of Xcode 7.2)
}
public func wasHidden() {
open func wasHidden() {
// Need to declare empty. Otherwise subclass code won't execute (as of Xcode 7.2)
}
}
public class TextMessageViewModelDefaultBuilder<TextMessageModelT: TextMessageModelProtocol>: ViewModelBuilderProtocol {
public init() { }
open class TextMessageViewModelDefaultBuilder<TextMessageModelT: TextMessageModelProtocol>: ViewModelBuilderProtocol {
public init() {}
let messageViewModelBuilder = MessageViewModelDefaultBuilder()
public func createViewModel(textMessage: TextMessageModelT) -> TextMessageViewModel<TextMessageModelT> {
open func createViewModel(_ textMessage: TextMessageModelT) -> TextMessageViewModel<TextMessageModelT> {
let messageViewModel = self.messageViewModelBuilder.createMessageViewModel(textMessage)
let textMessageViewModel = TextMessageViewModel(textMessage: textMessage, messageViewModel: messageViewModel)
return textMessageViewModel
}
public func canCreateViewModel(fromModel model: Any) -> Bool {
open func canCreateViewModel(fromModel model: Any) -> Bool {
return model is TextMessageModelT
}
}

View File

@ -25,25 +25,25 @@
import UIKit
public protocol TextBubbleViewStyleProtocol {
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
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
}
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 = .None
self.textView.selectable = false
if self.viewContext == .sizing {
self.textView.dataDetectorTypes = UIDataDetectorTypes()
self.textView.isSelectable = false
} else {
self.textView.dataDetectorTypes = .All
self.textView.selectable = true
self.textView.dataDetectorTypes = .all
self.textView.isSelectable = 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.clearColor()
textView.backgroundColor = UIColor.clear
})
textView.editable = false
textView.selectable = true
textView.dataDetectorTypes = .All
textView.isEditable = false
textView.isSelectable = true
textView.dataDetectorTypes = .all
textView.scrollsToTop = false
textView.scrollEnabled = false
textView.isScrollEnabled = false
textView.bounces = false
textView.bouncesZoom = false
textView.showsHorizontalScrollIndicator = false
textView.showsVerticalScrollIndicator = false
textView.layoutManager.allowsNonContiguousLayout = true
textView.exclusiveTouch = true
textView.isExclusiveTouch = true
textView.textContainer.lineFragmentPadding = 0
return textView
}()
public private(set) var isUpdating: Bool = false
public func performBatchUpdates(updateClosure: () -> Void, animated: Bool, completion: (() -> Void)?) {
public func performBatchUpdates(_ updateClosure: @escaping () -> 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.animateWithDuration(self.animationDuration, animations: updateAndRefreshViews, completion: { (finished) -> Void in
UIView.animate(withDuration: 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, viewModel = self.textMessageViewModel else { return }
guard let style = self.style, let 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!
private func calculateTextBubbleLayout(preferredMaxLayoutWidth preferredMaxLayoutWidth: CGFloat) -> TextBubbleLayoutModel {
public var layoutCache: NSCache<AnyObject, AnyObject>!
private func calculateTextBubbleLayout(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.objectForKey(layoutContext.hashValue) as? TextBubbleLayoutModel where layoutModel.layoutContext == layoutContext {
if let layoutModel = self.layoutCache.object(forKey: layoutContext.hashValue as AnyObject) as? TextBubbleLayoutModel, layoutModel.layoutContext == layoutContext {
return layoutModel
}
let layoutModel = TextBubbleLayoutModel(layoutContext: layoutContext)
layoutModel.calculateLayout()
self.layoutCache.setObject(layoutModel, forKey: layoutContext.hashValue)
self.layoutCache.setObject(layoutModel, forKey: layoutContext.hashValue as AnyObject)
return layoutModel
}
@ -214,7 +214,6 @@ public final class TextBubbleView: UIView, MaximumLayoutWidthSpecificable, Backg
}
}
private final class TextBubbleLayoutModel {
let layoutContext: LayoutContext
var textFrame: CGRect = CGRect.zero
@ -236,6 +235,12 @@ 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() {
@ -248,9 +253,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: .max)
let size = CGSize(width: width, height: .greatestFiniteMagnitude)
let container = NSTextContainer(size: size)
container.lineFragmentPadding = 0
return container
@ -264,7 +269,7 @@ private final class TextBubbleLayoutModel {
return layoutManager
}()
let rect = layoutManager.usedRectForTextContainer(textContainer)
let rect = layoutManager.usedRect(for: textContainer)
return rect.size.bma_round()
}
@ -277,26 +282,20 @@ 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 func canBecomeFirstResponder() -> Bool {
override var canBecomeFirstResponder: Bool {
return false
}
override func addGestureRecognizer(gestureRecognizer: UIGestureRecognizer) {
if gestureRecognizer.dynamicType == UILongPressGestureRecognizer.self && gestureRecognizer.delaysTouchesEnded {
override func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
if type(of: gestureRecognizer) == UILongPressGestureRecognizer.self && gestureRecognizer.delaysTouchesEnded {
super.addGestureRecognizer(gestureRecognizer)
}
}
override func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool {
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return false
}

View File

@ -30,21 +30,17 @@ 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: () -> Void, animated: Bool, completion: (() -> Void)?) {
public override func performBatchUpdates(_ updateClosure: @escaping () -> Void, animated: Bool, completion: (() -> Void)?) {
super.performBatchUpdates({ () -> Void in
self.bubbleView.performBatchUpdates(updateClosure, animated: false, completion: nil)
}, animated: animated, completion: completion)
@ -71,13 +67,13 @@ public final class TextMessageCollectionViewCell: BaseMessageCollectionViewCell<
}
}
override public var selected: Bool {
override public var isSelected: Bool {
didSet {
self.bubbleView.selected = self.selected
self.bubbleView.selected = self.isSelected
}
}
public var layoutCache: NSCache! {
public var layoutCache: NSCache<AnyObject, AnyObject>! {
didSet {
self.bubbleView.layoutCache = self.layoutCache
}

View File

@ -24,7 +24,7 @@
import UIKit
public class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionViewCellStyleProtocol {
open class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionViewCellStyleProtocol {
typealias Class = TextMessageCollectionViewCellDefaultStyle
public struct BubbleImages {
@ -33,10 +33,10 @@ public class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionVie
let outgoingTail: () -> UIImage
let outgoingNoTail: () -> UIImage
public init(
@autoclosure(escaping) incomingTail: () -> UIImage,
@autoclosure(escaping) incomingNoTail: () -> UIImage,
@autoclosure(escaping) outgoingTail: () -> UIImage,
@autoclosure(escaping) outgoingNoTail: () -> UIImage) {
incomingTail: @autoclosure @escaping () -> UIImage,
incomingNoTail: @autoclosure @escaping () -> UIImage,
outgoingTail: @autoclosure @escaping () -> UIImage,
outgoingNoTail: @autoclosure @escaping () -> UIImage) {
self.incomingTail = incomingTail
self.incomingNoTail = incomingNoTail
self.outgoingTail = outgoingTail
@ -51,9 +51,9 @@ public class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionVie
let incomingInsets: UIEdgeInsets
let outgoingInsets: UIEdgeInsets
public init(
@autoclosure(escaping) font: () -> UIFont,
@autoclosure(escaping) incomingColor: () -> UIColor,
@autoclosure(escaping) outgoingColor: () -> UIColor,
font: @autoclosure @escaping () -> UIFont,
incomingColor: @autoclosure @escaping () -> UIColor,
outgoingColor: @autoclosure @escaping () -> UIColor,
incomingInsets: UIEdgeInsets,
outgoingInsets: UIEdgeInsets) {
self.font = font
@ -64,7 +64,6 @@ public class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionVie
}
}
public let bubbleImages: BubbleImages
public let textStyle: TextStyle
public let baseStyle: BaseMessageCollectionViewCellDefaultStyle
@ -90,23 +89,23 @@ public class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionVie
lazy var incomingColor: UIColor = self.textStyle.incomingColor()
lazy var outgoingColor: UIColor = self.textStyle.outgoingColor()
public func textFont(viewModel viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIFont {
open func textFont(viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIFont {
return self.font
}
public func textColor(viewModel viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIColor {
open func textColor(viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIColor {
return viewModel.isIncoming ? self.incomingColor : self.outgoingColor
}
public func textInsets(viewModel viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIEdgeInsets {
open func textInsets(viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIEdgeInsets {
return viewModel.isIncoming ? self.textStyle.incomingInsets : self.textStyle.outgoingInsets
}
public func bubbleImageBorder(viewModel viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIImage? {
open func bubbleImageBorder(viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIImage? {
return self.baseStyle.borderImage(viewModel: viewModel)
}
public func bubbleImage(viewModel viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIImage {
open func bubbleImage(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] {
@ -124,18 +123,18 @@ public class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionVie
return UIImage()
}
public func createImage(templateImage image: UIImage, isIncoming: Bool, status: MessageViewModelStatus, isSelected: Bool) -> UIImage {
open 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.whiteColor().colorWithAlphaComponent(0.70))
case .failed, .sending:
color = color.bma_blendWithColor(UIColor.white.withAlphaComponent(0.70))
}
if isSelected {
color = color.bma_blendWithColor(UIColor.blackColor().colorWithAlphaComponent(0.10))
color = color.bma_blendWithColor(UIColor.black.withAlphaComponent(0.10))
}
return image.bma_tintWithColor(color)
@ -155,19 +154,19 @@ public class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionVie
}
private func calculateHash(withHashValues hashes: [Int]) -> Int {
return hashes.reduce(0, combine: { 31 &* $0 &+ $1.hashValue })
return hashes.reduce(0, { 31 &* $0 &+ $1.hashValue })
}
}
}
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
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
}
}
}
}
@ -175,18 +174,18 @@ public extension TextMessageCollectionViewCellDefaultStyle { // Default values
static public func createDefaultBubbleImages() -> BubbleImages {
return BubbleImages(
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)!
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)!
)
}
static public func createDefaultTextStyle() -> TextStyle {
return TextStyle(
font: UIFont.systemFontOfSize(16),
incomingColor: UIColor.blackColor(),
outgoingColor: UIColor.whiteColor(),
font: UIFont.systemFont(ofSize: 16),
incomingColor: UIColor.black,
outgoingColor: UIColor.white,
incomingInsets: UIEdgeInsets(top: 10, left: 19, bottom: 10, right: 15),
outgoingInsets: UIEdgeInsets(top: 10, left: 15, bottom: 10, right: 19)
)

View File

@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.1.0</string>
<string>3.0.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>

View File

@ -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
public class ChatInputBar: ReusableXibView {
open class ChatInputBar: ReusableXibView {
public weak var delegate: ChatInputBarDelegate?
weak var presenter: ChatInputBarPresenter?
@ -56,8 +56,8 @@ public class ChatInputBar: ReusableXibView {
@IBOutlet var constraintsForHiddenSendButton: [NSLayoutConstraint]!
@IBOutlet var tabBarContainerHeightConstraint: NSLayoutConstraint!
class public func loadNib() -> ChatInputBar {
let view = NSBundle(forClass: self).loadNibNamed(self.nibName(), owner: nil, options: nil)!.first as! ChatInputBar
class open func loadNib() -> ChatInputBar {
let view = Bundle(for: self).loadNibNamed(self.nibName(), owner: nil, options: nil)!.first as! ChatInputBar
view.translatesAutoresizingMaskIntoConstraints = false
view.frame = CGRect.zero
return view
@ -67,34 +67,34 @@ public class ChatInputBar: ReusableXibView {
return "ChatInputBar"
}
public override func awakeFromNib() {
open override func awakeFromNib() {
super.awakeFromNib()
self.topBorderHeightConstraint.constant = 1 / UIScreen.mainScreen().scale
self.topBorderHeightConstraint.constant = 1 / UIScreen.main.scale
self.textView.scrollsToTop = false
self.textView.delegate = self
self.scrollView.scrollsToTop = false
self.sendButton.enabled = false
self.sendButton.isEnabled = false
}
public override func updateConstraints() {
open override func updateConstraints() {
if self.showsTextView {
NSLayoutConstraint.activateConstraints(self.constraintsForVisibleTextView)
NSLayoutConstraint.deactivateConstraints(self.constraintsForHiddenTextView)
NSLayoutConstraint.activate(self.constraintsForVisibleTextView)
NSLayoutConstraint.deactivate(self.constraintsForHiddenTextView)
} else {
NSLayoutConstraint.deactivateConstraints(self.constraintsForVisibleTextView)
NSLayoutConstraint.activateConstraints(self.constraintsForHiddenTextView)
NSLayoutConstraint.deactivate(self.constraintsForVisibleTextView)
NSLayoutConstraint.activate(self.constraintsForHiddenTextView)
}
if self.showsSendButton {
NSLayoutConstraint.deactivateConstraints(self.constraintsForHiddenSendButton)
NSLayoutConstraint.activateConstraints(self.constraintsForVisibleSendButton)
NSLayoutConstraint.deactivate(self.constraintsForHiddenSendButton)
NSLayoutConstraint.activate(self.constraintsForVisibleSendButton)
} else {
NSLayoutConstraint.deactivateConstraints(self.constraintsForVisibleSendButton)
NSLayoutConstraint.activateConstraints(self.constraintsForHiddenSendButton)
NSLayoutConstraint.deactivate(self.constraintsForVisibleSendButton)
NSLayoutConstraint.activate(self.constraintsForHiddenSendButton)
}
super.updateConstraints()
}
public var showsTextView: Bool = true {
open var showsTextView: Bool = true {
didSet {
self.setNeedsUpdateConstraints()
self.setNeedsLayout()
@ -102,7 +102,7 @@ public class ChatInputBar: ReusableXibView {
}
}
public var showsSendButton: Bool = true {
open var showsSendButton: Bool = true {
didSet {
self.setNeedsUpdateConstraints()
self.setNeedsLayout()
@ -113,15 +113,15 @@ public class ChatInputBar: ReusableXibView {
public var maxCharactersCount: UInt? // nil -> unlimited
private func updateIntrinsicContentSizeAnimated() {
let options: UIViewAnimationOptions = [.BeginFromCurrentState, .AllowUserInteraction, .CurveEaseInOut]
UIView.animateWithDuration(0.25, delay: 0, options: options, animations: { () -> Void in
let options: UIViewAnimationOptions = [.beginFromCurrentState, .allowUserInteraction]
UIView.animate(withDuration: 0.25, delay: 0, options: options, animations: { () -> Void in
self.invalidateIntrinsicContentSize()
self.layoutIfNeeded()
self.superview?.layoutIfNeeded()
}, completion: nil)
}
public override func layoutSubviews() {
open 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 @@ public class ChatInputBar: ReusableXibView {
}
}
public func becomeFirstResponderWithInputView(inputView: UIView?) {
open 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 @@ public class ChatInputBar: ReusableXibView {
}
}
private func updateSendButton() {
self.sendButton.enabled = self.shouldEnableSendButton(self)
fileprivate func updateSendButton() {
self.sendButton.isEnabled = 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, forState: .Normal)
self.sendButton.setTitle(appearance.sendButtonAppearance.title, for: .normal)
appearance.sendButtonAppearance.titleColors.forEach { (state, color) in
self.sendButton.setTitleColor(color, forState: state.controlState)
self.sendButton.setTitleColor(color, for: 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, shouldChangeTextInRange nsRange: NSRange, replacementText text: String) -> Bool {
public func textView(_ textView: UITextView, shouldChangeTextIn 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.substringWithRange(range).characters.count
let rangeLength = textView.text.substring(with: range).characters.count
let nextCount = currentCount - rangeLength + text.characters.count
return UInt(nextCount) <= maxCharactersCount
}
@ -262,12 +262,13 @@ extension ChatInputBar: UITextViewDelegate {
}
private extension String {
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
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
}
}

View File

@ -24,13 +24,13 @@
public struct ChatInputBarAppearance {
public struct SendButtonAppearance {
public var font = UIFont.systemFontOfSize(16)
public var font = UIFont.systemFont(ofSize: 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.whiteColor().colorWithAlphaComponent(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.white.withAlphaComponent(0.4))
]
}
@ -41,10 +41,10 @@ public struct ChatInputBarAppearance {
}
public struct TextInputAppearance {
public var font = UIFont.systemFontOfSize(12)
public var textColor = UIColor.blackColor()
public var placeholderFont = UIFont.systemFontOfSize(12)
public var placeholderColor = UIColor.grayColor()
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 placeholderText = ""
public var textInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
}
@ -56,7 +56,6 @@ public struct ChatInputBarAppearance {
public init() {}
}
// Workaround for SR-2223
public struct UIControlStateWrapper: Hashable {

View File

@ -29,18 +29,19 @@ 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: NSNotificationCenter
let notificationCenter: NotificationCenter
public init(chatInputBar: ChatInputBar,
chatInputItems: [ChatInputItemProtocol],
chatInputBarAppearance: ChatInputBarAppearance,
notificationCenter: NSNotificationCenter = NSNotificationCenter.defaultCenter()) {
notificationCenter: NotificationCenter = NotificationCenter.default) {
self.chatInputBar = chatInputBar
self.chatInputItems = chatInputItems
self.chatInputBar.setAppearance(chatInputBarAppearance)
@ -49,16 +50,16 @@ protocol ChatInputBarPresenter: class {
self.chatInputBar.presenter = self
self.chatInputBar.inputItems = self.chatInputItems
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)
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)
}
deinit {
self.notificationCenter.removeObserver(self)
}
private(set) var focusedItem: ChatInputItemProtocol? {
fileprivate(set) var focusedItem: ChatInputItemProtocol? {
willSet {
self.focusedItem?.selected = false
}
@ -67,11 +68,11 @@ protocol ChatInputBarPresenter: class {
}
}
private func updateFirstResponderWithInputItem(inputItem: ChatInputItemProtocol) {
let responder = self.chatInputBar.textView
fileprivate 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 {
@ -79,10 +80,10 @@ protocol ChatInputBarPresenter: class {
}
}
private func firstKeyboardInputItem() -> ChatInputItemProtocol? {
fileprivate func firstKeyboardInputItem() -> ChatInputItemProtocol? {
var firstKeyboardInputItem: ChatInputItemProtocol? = nil
for inputItem in self.chatInputItems {
if inputItem.presentationMode == .Keyboard {
if inputItem.presentationMode == .keyboard {
firstKeyboardInputItem = inputItem
break
}
@ -97,13 +98,13 @@ protocol ChatInputBarPresenter: class {
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
@ -113,19 +114,19 @@ protocol ChatInputBarPresenter: class {
private var allowListenToChangeFrameEvents = true
@objc
private func keyboardDidChangeFrame(notification: NSNotification) {
private func keyboardDidChangeFrame(_ notification: Notification) {
guard self.allowListenToChangeFrameEvents else { return }
guard let value = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue else { return }
self.lastKnownKeyboardHeight = value.CGRectValue().height
guard let value = (notification as NSNotification).userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue else { return }
self.lastKnownKeyboardHeight = value.cgRectValue.height
}
@objc
private func keyboardWillHide(notification: NSNotification) {
private func keyboardWillHide(_ notification: Notification) {
self.allowListenToChangeFrameEvents = false
}
@objc
private func keyboardWillShow(notification: NSNotification) {
private func keyboardWillShow(_ notification: Notification) {
self.allowListenToChangeFrameEvents = true
}
}
@ -147,20 +148,20 @@ extension BasicChatInputBarPresenter {
func onSendButtonPressed() {
if let focusedItem = self.focusedItem {
focusedItem.handleInput(self.chatInputBar.inputText)
focusedItem.handleInput(self.chatInputBar.inputText as AnyObject)
} else if let keyboardItem = self.firstKeyboardInputItem() {
keyboardItem.handleInput(self.chatInputBar.inputText)
keyboardItem.handleInput(self.chatInputBar.inputText as AnyObject)
}
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)
}
}

View File

@ -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)
}

View File

@ -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 func intrinsicContentSize() -> CGSize {
return self.inputItem.tabView.intrinsicContentSize()
override var intrinsicContentSize: CGSize {
return self.inputItem.tabView.intrinsicContentSize
}
}

View File

@ -24,7 +24,7 @@
import UIKit
public class ExpandableTextView: UITextView {
open class ExpandableTextView: UITextView {
private let placeholder: UITextView = UITextView()
@ -38,7 +38,7 @@ public class ExpandableTextView: UITextView {
self.commonInit()
}
override public var contentSize: CGSize {
override open var contentSize: CGSize {
didSet {
self.invalidateIntrinsicContentSize()
self.layoutIfNeeded() // needed?
@ -46,56 +46,55 @@ public class ExpandableTextView: UITextView {
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
NotificationCenter.default.removeObserver(self)
}
private func commonInit() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ExpandableTextView.textDidChange), name: UITextViewTextDidChangeNotification, object: self)
NotificationCenter.default.addObserver(self, selector: #selector(ExpandableTextView.textDidChange), name: NSNotification.Name.UITextViewTextDidChange, object: self)
self.configurePlaceholder()
self.updatePlaceholderVisibility()
}
override public func layoutSubviews() {
override open func layoutSubviews() {
super.layoutSubviews()
self.placeholder.frame = self.bounds
}
override public func intrinsicContentSize() -> CGSize {
override open var intrinsicContentSize: CGSize {
return self.contentSize
}
override public var text: String! {
override open var text: String! {
didSet {
self.textDidChange()
}
}
override public var textContainerInset: UIEdgeInsets {
override open var textContainerInset: UIEdgeInsets {
didSet {
self.configurePlaceholder()
}
}
override public var textAlignment: NSTextAlignment {
override open var textAlignment: NSTextAlignment {
didSet {
self.configurePlaceholder()
}
}
public func setTextPlaceholder(textPlaceholder: String) {
open func setTextPlaceholder(_ textPlaceholder: String) {
self.placeholder.text = textPlaceholder
}
public func setTextPlaceholderColor(color: UIColor) {
open func setTextPlaceholderColor(_ color: UIColor) {
self.placeholder.textColor = color
}
public func setTextPlaceholderFont(font: UIFont) {
open func setTextPlaceholderFont(_ font: UIFont) {
self.placeholder.font = font
}
public func setTextPlaceholderAccessibilityIdentifier(accessibilityIdentifier: String) {
open func setTextPlaceholderAccessibilityIdentifier(_ accessibilityIdentifier: String) {
self.placeholder.accessibilityIdentifier = accessibilityIdentifier
}
@ -109,14 +108,14 @@ public 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.scrollEnabled = false
self.scrollEnabled = true
self.isScrollEnabled = false
self.isScrollEnabled = true
}
}
private func scrollToCaret() {
if let textRange = self.selectedTextRange {
var rect = caretRectForPosition(textRange.end)
var rect = caretRect(for: textRange.end)
rect = CGRect(origin: rect.origin, size: CGSize(width: rect.width, height: rect.height + textContainerInset.bottom))
self.scrollRectToVisible(rect, animated: false)
@ -141,11 +140,11 @@ public class ExpandableTextView: UITextView {
private func configurePlaceholder() {
self.placeholder.translatesAutoresizingMaskIntoConstraints = false
self.placeholder.editable = false
self.placeholder.selectable = false
self.placeholder.userInteractionEnabled = false
self.placeholder.isEditable = false
self.placeholder.isSelectable = false
self.placeholder.isUserInteractionEnabled = false
self.placeholder.textAlignment = self.textAlignment
self.placeholder.textContainerInset = self.textContainerInset
self.placeholder.backgroundColor = UIColor.clearColor()
self.placeholder.backgroundColor = UIColor.clear
}
}

View File

@ -24,7 +24,7 @@
import UIKit
public class HorizontalStackScrollView: UIScrollView {
open class HorizontalStackScrollView: UIScrollView {
private var arrangedViews: [UIView] = []
private var arrangedViewContraints: [NSLayoutConstraint] = []
@ -34,16 +34,16 @@ public class HorizontalStackScrollView: UIScrollView {
}
}
func addArrangedViews(views: [UIView]) {
func addArrangedViews(_ views: [UIView]) {
for view in views {
view.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(view)
}
self.arrangedViews.appendContentsOf(views)
self.arrangedViews.append(contentsOf: views)
self.setNeedsUpdateConstraints()
}
override public func updateConstraints() {
override open func updateConstraints() {
super.updateConstraints()
self.removeConstraintsForArrangedViews()
self.addConstraintsForArrengedViews()
@ -57,23 +57,23 @@ public class HorizontalStackScrollView: UIScrollView {
}
private func addConstraintsForArrengedViews() {
for (index, view) in arrangedViews.enumerate() {
for (index, view) in arrangedViews.enumerated() {
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))
}
}
}

View File

@ -29,8 +29,8 @@ protocol LiveCameraCaptureSessionProtocol {
var captureLayer: AVCaptureVideoPreviewLayer? { get }
var isInitialized: Bool { get }
var isCapturing: Bool { get }
func startCapturing(completion: () -> Void)
func stopCapturing(completion: () -> Void)
func startCapturing(_ completion: @escaping () -> Void)
func stopCapturing(_ completion: @escaping () -> Void)
}
class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
@ -38,39 +38,39 @@ class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
var isInitialized: Bool = false
var isCapturing: Bool {
return self.isInitialized && self.captureSession?.running ?? false
return self.isInitialized && self.captureSession?.isRunning ?? false
}
deinit {
var layer = self.captureLayer
layer?.removeFromSuperlayer()
var session: AVCaptureSession? = self.isInitialized ? self.captureSession : nil
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
DispatchQueue.global(qos: .default).async {
// 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: () -> Void) {
let operation = NSBlockOperation()
func startCapturing(_ completion: @escaping () -> Void) {
let operation = BlockOperation()
operation.addExecutionBlock { [weak operation, weak self] in
guard let sSelf = self, strongOperation = operation where !strongOperation.cancelled else { return }
guard let sSelf = self, let strongOperation = operation, !strongOperation.isCancelled else { return }
sSelf.addInputDevicesIfNeeded()
sSelf.captureSession?.startRunning()
dispatch_async(dispatch_get_main_queue(), completion)
DispatchQueue.main.async(execute: completion)
}
self.queue.cancelAllOperations()
self.queue.addOperation(operation)
}
func stopCapturing(completion: () -> Void) {
let operation = NSBlockOperation()
func stopCapturing(_ completion: @escaping () -> Void) {
let operation = BlockOperation()
operation.addExecutionBlock { [weak operation, weak self] in
guard let sSelf = self, strongOperation = operation where !strongOperation.cancelled else { return }
guard let sSelf = self, let strongOperation = operation, !strongOperation.isCancelled else { return }
sSelf.captureSession?.stopRunning()
sSelf.removeInputDevices()
dispatch_async(dispatch_get_main_queue(), completion)
DispatchQueue.main.async(execute: completion)
}
self.queue.cancelAllOperations()
self.queue.addOperation(operation)
@ -78,17 +78,17 @@ class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
private (set) var captureLayer: AVCaptureVideoPreviewLayer?
private lazy var queue: NSOperationQueue = {
let queue = NSOperationQueue()
queue.qualityOfService = .UserInitiated
private lazy var queue: OperationQueue = {
let queue = OperationQueue()
queue.qualityOfService = .userInitiated
queue.maxConcurrentOperationCount = 1
return queue
}()
private lazy var captureSession: AVCaptureSession? = {
assert(!NSThread.isMainThread(), "This can be very slow, make sure it happens in a background thread")
assert(!Thread.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(!NSThread.isMainThread(), "This can be very slow, make sure it happens in a background thread")
assert(!Thread.isMainThread, "This can be very slow, make sure it happens in a background thread")
if self.captureSession?.inputs?.count == 0 {
let device = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
let device = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)
do {
let input = try AVCaptureDeviceInput(device: device)
self.captureSession?.addInput(input)
@ -113,7 +113,7 @@ class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
}
private func removeInputDevices() {
assert(!NSThread.isMainThread(), "This can be very slow, make sure it happens in a background thread")
assert(!Thread.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)
}

View File

@ -32,8 +32,8 @@ public struct LiveCameraCellAppearance {
public var cameraLockImageProvider: () -> UIImage?
public init(backgroundColor: UIColor,
@autoclosure(escaping) cameraImage: () -> UIImage?,
@autoclosure(escaping) cameraLockImage: () -> UIImage?) {
cameraImage: @autoclosure @escaping () -> UIImage?,
cameraLockImage: @autoclosure @escaping () -> 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", inBundle: NSBundle(forClass: LiveCameraCell.self), compatibleWithTraitCollection: nil),
cameraLockImage: UIImage(named: "camera_lock", inBundle: NSBundle(forClass: LiveCameraCell.self), compatibleWithTraitCollection: nil)
cameraImage: UIImage(named: "camera", in: Bundle(for: LiveCameraCell.self), compatibleWith: nil),
cameraLockImage: UIImage(named: "camera_lock", in: Bundle(for: LiveCameraCell.self), compatibleWith: 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.removeAnimationForKey(animationKey)
captureLayer.addAnimation(animation, forKey: animationKey)
captureLayer.removeAnimation(forKey: animationKey)
captureLayer.add(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?(cell: self)
self.onWasAddedToWindow?(self)
} else {
self.onWasRemovedFromWindow?(cell: self)
self.onWasRemovedFromWindow?(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()

View File

@ -29,19 +29,13 @@ public final class LiveCameraCellPresenter {
private typealias Class = LiveCameraCellPresenter
public typealias AVAuthorizationStatusProvider = () -> AVAuthorizationStatus
private let cellAppearance: LiveCameraCellAppearance
private let authorizationStatusProvider: () -> AVAuthorizationStatus
public init(cellAppearance: LiveCameraCellAppearance, authorizationStatusProvider: AVAuthorizationStatusProvider = LiveCameraCellPresenter.createDefaultCameraAuthorizationStatusProvider()) {
public init(cellAppearance: LiveCameraCellAppearance = LiveCameraCellAppearance.createDefaultAppearance(), authorizationStatusProvider: @escaping 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()
}
@ -49,21 +43,21 @@ public final class LiveCameraCellPresenter {
private static let reuseIdentifier = "LiveCameraCell"
private static func createDefaultCameraAuthorizationStatusProvider() -> AVAuthorizationStatusProvider {
return {
return AVCaptureDevice.authorizationStatusForMediaType(AVMediaTypeVideo)
return AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeVideo)
}
}
public static func registerCells(collectionView collectionView: UICollectionView) {
collectionView.registerClass(LiveCameraCell.self, forCellWithReuseIdentifier: Class.reuseIdentifier)
public static func registerCells(collectionView: UICollectionView) {
collectionView.register(LiveCameraCell.self, forCellWithReuseIdentifier: Class.reuseIdentifier)
}
public func dequeueCell(collectionView collectionView: UICollectionView, indexPath: NSIndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCellWithReuseIdentifier(Class.reuseIdentifier, forIndexPath: indexPath)
public func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCell(withReuseIdentifier: Class.reuseIdentifier, for: 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
@ -73,7 +67,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
@ -100,14 +94,14 @@ public final class LiveCameraCellPresenter {
}
cameraCell.onWasAddedToWindow = { [weak self] (cell) in
guard let sSelf = self where sSelf.cell === cell else { return }
guard let sSelf = self, sSelf.cell === cell else { return }
if !sSelf.cameraPickerIsVisible {
sSelf.startCapturing()
}
}
cameraCell.onWasRemovedFromWindow = { [weak self] (cell) in
guard let sSelf = self where sSelf.cell === cell else { return }
guard let sSelf = self, sSelf.cell === cell else { return }
if !sSelf.cameraPickerIsVisible {
sSelf.stopCapturing()
}
@ -115,11 +109,11 @@ public final class LiveCameraCellPresenter {
}
// MARK: - App Notifications
lazy var notificationCenter = NSNotificationCenter.defaultCenter()
lazy var notificationCenter = NotificationCenter.default
private func subscribeToAppNotifications() {
self.notificationCenter.addObserver(self, selector: #selector(LiveCameraCellPresenter.handleWillResignActiveNotification), name: UIApplicationWillResignActiveNotification, object: nil)
self.notificationCenter.addObserver(self, selector: #selector(LiveCameraCellPresenter.handleDidBecomeActiveNotification), name: UIApplicationDidBecomeActiveNotification, object: nil)
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)
}
private func unsubscribeFromAppNotifications() {
@ -130,7 +124,7 @@ public final class LiveCameraCellPresenter {
@objc
private func handleWillResignActiveNotification() {
if self.captureSession.isCapturing ?? false {
if self.captureSession.isCapturing {
self.needsRestoreCaptureSession = true
self.stopCapturing()
}
@ -173,16 +167,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()

View File

@ -24,7 +24,7 @@
import Foundation
public class PhotosChatInputItem: ChatInputItemProtocol {
open class PhotosChatInputItem: ChatInputItemProtocol {
typealias Class = PhotosChatInputItem
public var photoInputHandler: ((UIImage) -> Void)?
@ -42,16 +42,16 @@ public class PhotosChatInputItem: ChatInputItemProtocol {
self.inputViewAppearance = inputViewAppearance
}
public class func createDefaultButtonAppearance() -> TabInputButtonAppearance {
public static func createDefaultButtonAppearance() -> TabInputButtonAppearance {
let images: [UIControlStateWrapper: UIImage] = [
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)!
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)!
]
return TabInputButtonAppearance(images: images, size: nil)
}
public class func createDefaultInputViewAppearance() -> PhotosInputViewAppearance {
public static func createDefaultInputViewAppearance() -> PhotosInputViewAppearance {
return PhotosInputViewAppearance(liveCameraCellAppearence: LiveCameraCellAppearance.createDefaultAppearance())
}
@ -65,31 +65,31 @@ public class PhotosChatInputItem: ChatInputItemProtocol {
return photosInputView
}()
public var selected = false {
open var selected = false {
didSet {
self.internalTabView.selected = self.selected
self.internalTabView.isSelected = self.selected
}
}
// MARK: - ChatInputItemProtocol
public var presentationMode: ChatInputItemPresentationMode {
return .CustomView
open var presentationMode: ChatInputItemPresentationMode {
return .customView
}
public var showsSendButton: Bool {
open var showsSendButton: Bool {
return false
}
public var inputView: UIView? {
open var inputView: UIView? {
return self.photosInputView as? UIView
}
public var tabView: UIView {
open var tabView: UIView {
return self.internalTabView
}
public func handleInput(input: AnyObject) {
open func handleInput(_ input: AnyObject) {
if let image = input as? UIImage {
self.photoInputHandler?(image)
}
@ -98,15 +98,15 @@ public 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?()
}
}

View File

@ -32,8 +32,8 @@ class PhotosInputCameraPicker: NSObject {
private var completionBlocks: (onImageTaken: ((UIImage?) -> Void)?, onCameraPickerDismissed: (() -> Void)?)?
func presentCameraPicker(onImageTaken onImageTaken: (UIImage?) -> Void, onCameraPickerDismissed: () -> Void) {
guard UIImagePickerController.isSourceTypeAvailable(.Camera) else {
func presentCameraPicker(onImageTaken: @escaping (UIImage?) -> Void, onCameraPickerDismissed: @escaping () -> 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.presentViewController(controller, animated: true, completion:nil)
controller.sourceType = .camera
presentingController.present(controller, animated: true, completion:nil)
}
private func finishPickingImage(image: UIImage?, fromPicker picker: UIImagePickerController) {
fileprivate func finishPickingImage(_ image: UIImage?, fromPicker picker: UIImagePickerController) {
let (onImageTaken, onCameraPickerDismissed) = self.completionBlocks ?? (nil, nil)
picker.dismissViewControllerAnimated(true, completion: onCameraPickerDismissed)
picker.dismiss(animated: 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)
}
}

View File

@ -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, inBundle: NSBundle(forClass: PhotosInputPlaceholderCell.self), compatibleWithTraitCollection: nil)
self.imageView.contentMode = .center
self.imageView.image = UIImage(named: Constants.imageName, in: Bundle(for: PhotosInputPlaceholderCell.self), compatibleWith: 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
}

View File

@ -25,7 +25,7 @@
import UIKit
protocol PhotosInputCellProviderProtocol {
func cellForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewCell
func cellForItemAtIndexPath(_ indexPath: IndexPath) -> UICollectionViewCell
}
class PhotosInputPlaceholderCellProvider: PhotosInputCellProviderProtocol {
@ -33,11 +33,11 @@ class PhotosInputPlaceholderCellProvider: PhotosInputCellProviderProtocol {
private let collectionView: UICollectionView
init(collectionView: UICollectionView) {
self.collectionView = collectionView
self.collectionView.registerClass(PhotosInputPlaceholderCell.self, forCellWithReuseIdentifier: self.reuseIdentifier)
self.collectionView.register(PhotosInputPlaceholderCell.self, forCellWithReuseIdentifier: self.reuseIdentifier)
}
func cellForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewCell {
return self.collectionView.dequeueReusableCellWithReuseIdentifier(self.reuseIdentifier, forIndexPath: indexPath)
func cellForItemAtIndexPath(_ indexPath: IndexPath) -> UICollectionViewCell {
return self.collectionView.dequeueReusableCell(withReuseIdentifier: self.reuseIdentifier, for: indexPath)
}
}
@ -48,20 +48,20 @@ class PhotosInputCellProvider: PhotosInputCellProviderProtocol {
init(collectionView: UICollectionView, dataProvider: PhotosInputDataProviderProtocol) {
self.dataProvider = dataProvider
self.collectionView = collectionView
self.collectionView.registerClass(PhotosInputCell.self, forCellWithReuseIdentifier: self.reuseIdentifier)
self.collectionView.register(PhotosInputCell.self, forCellWithReuseIdentifier: self.reuseIdentifier)
}
func cellForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = self.collectionView.dequeueReusableCellWithReuseIdentifier(self.reuseIdentifier, forIndexPath: indexPath) as! PhotosInputCell
func cellForItemAtIndexPath(_ indexPath: IndexPath) -> UICollectionViewCell {
let cell = self.collectionView.dequeueReusableCell(withReuseIdentifier: self.reuseIdentifier, for: indexPath) as! PhotosInputCell
self.configureCell(cell, atIndexPath: indexPath)
return cell
}
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)
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)
}
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, sCell = cell else { return }
guard let sSelf = self, let 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.objectForKey(sCell) as? NSNumber)?.intValue == requestID
let imageIsForThisCell = imageProvidedSynchronously || sSelf.previewRequests.object(forKey: sCell)?.int32Value == requestID
if imageIsForThisCell {
sCell.image = image
}
}
imageProvidedSynchronously = false
self.previewRequests.setObject(NSNumber(int: requestID), forKey:cell)
self.previewRequests.setObject(NSNumber(value: requestID), forKey:cell)
}
}

View File

@ -25,15 +25,15 @@
import PhotosUI
protocol PhotosInputDataProviderDelegate: class {
func handlePhotosInpudDataProviderUpdate(dataProvider: PhotosInputDataProviderProtocol, updateBlock: () -> Void)
func handlePhotosInpudDataProviderUpdate(_ dataProvider: PhotosInputDataProviderProtocol, updateBlock: @escaping () -> Void)
}
protocol PhotosInputDataProviderProtocol {
weak var delegate: PhotosInputDataProviderDelegate? { get set }
var count: Int { get }
func requestPreviewImageAtIndex(index: Int, targetSize: CGSize, completion: (UIImage) -> Void) -> Int32
func requestFullImageAtIndex(index: Int, completion: (UIImage) -> Void)
func cancelPreviewImageRequest(requestID: Int32)
func requestPreviewImageAtIndex(_ index: Int, targetSize: CGSize, completion: @escaping (UIImage) -> Void) -> Int32
func requestFullImageAtIndex(_ index: Int, completion: @escaping (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: (UIImage) -> Void) -> Int32 {
func requestPreviewImageAtIndex(_ index: Int, targetSize: CGSize, completion: @escaping (UIImage) -> Void) -> Int32 {
return 0
}
func requestFullImageAtIndex(index: Int, completion: (UIImage) -> Void) {
func requestFullImageAtIndex(_ index: Int, completion: @escaping (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!
private var fetchResult: PHFetchResult<PHAsset>!
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.fetchAssetCollectionsWithType(.SmartAlbum, subtype: .SmartAlbumUserLibrary, options: nil).firstObject as? PHAssetCollection {
self.fetchResult = PHAsset.fetchAssetsInAssetCollection(userLibraryCollection, options: fetchOptions(NSPredicate(format: "mediaType = \(PHAssetMediaType.Image.rawValue)")))
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)")))
} else {
self.fetchResult = PHAsset.fetchAssetsWithMediaType(.Image, options: fetchOptions(nil))
self.fetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions(nil))
}
super.init()
PHPhotoLibrary.sharedPhotoLibrary().registerChangeObserver(self)
PHPhotoLibrary.shared().register(self)
}
deinit {
PHPhotoLibrary.sharedPhotoLibrary().unregisterChangeObserver(self)
PHPhotoLibrary.shared().unregisterChangeObserver(self)
}
var count: Int {
return self.fetchResult.count
}
func requestPreviewImageAtIndex(index: Int, targetSize: CGSize, completion: (UIImage) -> Void) -> Int32 {
func requestPreviewImageAtIndex(_ index: Int, targetSize: CGSize, completion: @escaping (UIImage) -> Void) -> Int32 {
assert(index >= 0 && index < self.fetchResult.count, "Index out of bounds")
let asset = self.fetchResult[index] as! PHAsset
let asset = self.fetchResult[index]
let options = PHImageRequestOptions()
options.deliveryMode = .HighQualityFormat
return self.imageManager.requestImageForAsset(asset, targetSize: targetSize, contentMode: .AspectFill, options: options) { (image, info) in
options.deliveryMode = .highQualityFormat
return self.imageManager.requestImage(for: 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: (UIImage) -> Void) {
func requestFullImageAtIndex(_ index: Int, completion: @escaping (UIImage) -> Void) {
assert(index >= 0 && index < self.fetchResult.count, "Index out of bounds")
let asset = self.fetchResult[index] as! PHAsset
self.imageManager.requestImageDataForAsset(asset, options: .None) { (data, dataUTI, orientation, info) -> Void in
let asset = self.fetchResult[index]
self.imageManager.requestImageData(for: 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.
dispatch_async(dispatch_get_main_queue()) { [weak self] in
DispatchQueue.main.async { [weak self] in
guard let sSelf = self else { return }
if let changeDetails = changeInstance.changeDetailsForFetchResult(sSelf.fetchResult) {
if let changeDetails = changeInstance.changeDetails(for: sSelf.fetchResult as! PHFetchResult<PHObject>) {
let updateBlock = { () -> Void in
self?.fetchResult = changeDetails.fetchResultAfterChanges
self?.fetchResult = changeDetails.fetchResultAfterChanges as! PHFetchResult<PHAsset>
}
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: (UIImage) -> Void) -> Int32 {
func requestPreviewImageAtIndex(_ index: Int, targetSize: CGSize, completion: @escaping (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: (UIImage) -> Void) {
func requestFullImageAtIndex(_ index: Int, completion: @escaping (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: () -> Void) {
func handlePhotosInpudDataProviderUpdate(_ dataProvider: PhotosInputDataProviderProtocol, updateBlock: @escaping () -> Void) {
self.delegate?.handlePhotosInpudDataProviderUpdate(self, updateBlock: updateBlock)
}
}

View File

@ -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 {
private struct Constants {
fileprivate struct Constants {
static let liveCameraItemIndex = 0
}
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!
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!
var cameraAuthorizationStatus: AVAuthorizationStatus {
return AVCaptureDevice.authorizationStatusForMediaType(AVMediaTypeVideo)
return AVCaptureDevice.authorizationStatus(forMediaType: 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.requestAccessForMediaType(AVMediaTypeVideo) { (success) -> Void in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
AVCaptureDevice.requestAccess(forMediaType: AVMediaTypeVideo) { (success) -> Void in
DispatchQueue.main.async(execute: { () -> Void in
self.reloadVideoItem()
})
}
@ -122,22 +122,22 @@ class PhotosInputView: UIView, PhotosInputViewProtocol {
guard let sSelf = self else { return }
sSelf.collectionView.performBatchUpdates({
sSelf.collectionView.reloadItemsAtIndexPaths([NSIndexPath(forItem: Constants.liveCameraItemIndex, inSection: 0)])
sSelf.collectionView.reloadItems(at: [IndexPath(item: Constants.liveCameraItemIndex, section: 0)])
}, completion: { (finished) in
dispatch_async(dispatch_get_main_queue(), completion)
DispatchQueue.main.async(execute: 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 {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
if status == PHAuthorizationStatus.authorized {
DispatchQueue.main.async(execute: { () -> 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()
dispatch_async(dispatch_get_main_queue(), completion)
DispatchQueue.main.async(execute: completion)
}
}
func reload() {
self.collectionViewQueue.addTask { [weak self] (completion) in
self?.collectionView.reloadData()
dispatch_async(dispatch_get_main_queue(), completion)
DispatchQueue.main.async(execute: completion)
}
}
private lazy var cameraPicker: PhotosInputCameraPicker = {
fileprivate lazy var cameraPicker: PhotosInputCameraPicker = {
return PhotosInputCameraPicker(presentingController: self.presentingController)
}()
private lazy var liveCameraPresenter: LiveCameraCellPresenter = {
return LiveCameraCellPresenter(cellAppearance: self.appearance?.liveCameraCellAppearence)
fileprivate lazy var liveCameraPresenter: LiveCameraCellPresenter = {
return LiveCameraCellPresenter(cellAppearance: self.appearance?.liveCameraCellAppearence ?? LiveCameraCellAppearance.createDefaultAppearance())
}()
}
@ -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.whiteColor()
self.collectionView.backgroundColor = UIColor.white
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, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> 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, didSelectItemAtIndexPath indexPath: NSIndexPath) {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
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, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return self.itemSizeCalculator.itemSizeForWidth(collectionView.bounds.width, atIndex: indexPath.item)
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return self.itemSizeCalculator.interitemSpace
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return self.itemSizeCalculator.interitemSpace
}
func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.item == Constants.liveCameraItemIndex {
self.liveCameraPresenter.cellWillBeShown(cell)
}
}
func collectionView(collectionView: UICollectionView, didEndDisplayingCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.item == Constants.liveCameraItemIndex {
self.liveCameraPresenter.cellWasHidden(cell)
}
@ -261,20 +261,20 @@ extension PhotosInputView: UICollectionViewDelegateFlowLayout {
}
extension PhotosInputView: PhotosInputDataProviderDelegate {
func handlePhotosInpudDataProviderUpdate(dataProvider: PhotosInputDataProviderProtocol, updateBlock: () -> Void) {
func handlePhotosInpudDataProviderUpdate(_ dataProvider: PhotosInputDataProviderProtocol, updateBlock: @escaping () -> Void) {
self.collectionViewQueue.addTask { [weak self] (completion) in
guard let sSelf = self else { return }
updateBlock()
sSelf.collectionView.reloadData()
dispatch_async(dispatch_get_main_queue(), completion)
DispatchQueue.main.async(execute: completion)
}
}
}
private class PhotosInputCollectionViewLayout: UICollectionViewFlowLayout {
private override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return newBounds.width != self.collectionView?.bounds.width
}
}

View File

@ -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

View File

@ -24,22 +24,22 @@
import UIKit
@objc public class ReusableXibView: UIView {
@objc open class ReusableXibView: UIView {
func loadViewFromNib() -> 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
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
return view
}
override public func awakeAfterUsingCoder(aDecoder: NSCoder) -> AnyObject? {
override open func awakeAfter(using aDecoder: NSCoder) -> Any? {
if self.subviews.count > 0 {
return self
}
let bundle = NSBundle(forClass: self.dynamicType)
if let loadedView = bundle.loadNibNamed(self.dynamicType.nibName(), owner: nil, options: nil)?.first as? UIView {
let bundle = Bundle(for: type(of: self))
if let loadedView = bundle.loadNibNamed(type(of: self).nibName(), owner: nil, options: nil)?.first as? UIView {
loadedView.frame = frame
loadedView.autoresizingMask = autoresizingMask
loadedView.translatesAutoresizingMaskIntoConstraints = translatesAutoresizingMaskIntoConstraints

View File

@ -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.exclusiveTouch = true
let button = TabInputButton(type: .custom)
button.isExclusiveTouch = true
images.forEach { (state, image) in
button.setImage(image, forState: state.controlState)
button.setImage(image, for: state.controlState)
}
if let accessibilityIdentifier = accessibilityID {
button.accessibilityIdentifier = accessibilityIdentifier
@ -52,10 +52,10 @@ public class TabInputButton: UIButton {
private var size: CGSize?
public override func intrinsicContentSize() -> CGSize {
public override var intrinsicContentSize: CGSize {
if let size = self.size {
return size
}
return super.intrinsicContentSize()
return super.intrinsicContentSize
}
}

View File

@ -24,7 +24,7 @@
import Foundation
public class TextChatInputItem {
open class TextChatInputItem {
typealias Class = TextChatInputItem
public var textInputHandler: ((String) -> Void)?
@ -33,22 +33,22 @@ public class TextChatInputItem {
self.buttonAppearance = tabInputButtonAppearance
}
public class func createDefaultButtonAppearance() -> TabInputButtonAppearance {
public static func createDefaultButtonAppearance() -> TabInputButtonAppearance {
let images: [UIControlStateWrapper: UIImage] = [
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)!
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)!
]
return TabInputButtonAppearance(images: images, size: nil)
}
lazy private var internalTabView: TabInputButton = {
lazy fileprivate var internalTabView: TabInputButton = {
return TabInputButton.makeInputButton(withAppearance: self.buttonAppearance, accessibilityID: "text.chat.input.view")
}()
public var selected = false {
open var selected = false {
didSet {
self.internalTabView.selected = self.selected
self.internalTabView.isSelected = self.selected
}
}
}
@ -56,7 +56,7 @@ public 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)
}

View File

@ -36,12 +36,12 @@ public class Observable<T> {
didSet {
self.cleanDeadObservers()
for observer in self.observers {
observer.closure(old: oldValue, new: self.value)
observer.closure(oldValue, self.value)
}
}
}
public func observe(observer: AnyObject, closure: (old: T, new: T) -> ()) {
public func observe(_ observer: AnyObject, closure: @escaping (_ 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: (old: T, new: T) -> ()) {
let closure: (_ old: T, _ new: T) -> ()
init (owner: AnyObject, closure: @escaping (_ old: T, _ new: T) -> ()) {
self.owner = owner
self.closure = closure
}

View File

@ -101,6 +101,7 @@
rotationAnimation.duration = 1;
rotationAnimation.cumulative = YES;
rotationAnimation.repeatCount = HUGE_VALF;
rotationAnimation.removedOnCompletion = NO;
[_bgLayer addAnimation:rotationAnimation forKey:@"rotationAnimation"];
}

View File

@ -25,26 +25,26 @@
import Foundation
import CoreGraphics
private let scale = UIScreen.mainScreen().scale
private let scale = UIScreen.main.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 dx: CGFloat, dy: CGFloat) -> CGSize {
func bma_insetBy(dx: CGFloat, dy: CGFloat) -> CGSize {
return CGSize(width: self.width - dx, height: self.height - dy)
}
func bma_outsetBy(dx dx: CGFloat, dy: CGFloat) -> CGSize {
func bma_outsetBy(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,9 +105,8 @@ public extension CGRect {
}
}
public extension CGPoint {
func bma_offsetBy(dx dx: CGFloat, dy: CGFloat) -> CGPoint {
func bma_offsetBy(dx: CGFloat, dy: CGFloat) -> CGPoint {
return CGPoint(x: self.x + dx, y: self.y + dy)
}
}
@ -147,39 +146,38 @@ 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()
CGContextFillRect(context, rect)
self.drawInRect(rect, blendMode: .DestinationIn, alpha: 1)
context.fill(rect)
self.draw(in: rect, blendMode: .destinationIn, alpha: 1)
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image.resizableImageWithCapInsets(self.capInsets)
return image.resizableImage(withCapInsets: 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.mainScreen().scale)
UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.main.scale)
let context = UIGraphicsGetCurrentContext()!
CGContextTranslateCTM(context, 0, rect.height)
CGContextScaleCTM(context, 1.0, -1.0)
CGContextSetBlendMode(context, .Normal)
CGContextDrawImage(context, rect, self.CGImage!)
CGContextClipToMask(context, rect, self.CGImage!)
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!)
color.setFill()
CGContextAddRect(context, rect)
CGContextDrawPath(context, .Fill)
context.addRect(rect)
context.drawPath(using: .fill)
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image.resizableImageWithCapInsets(self.capInsets)
return image.resizableImage(withCapInsets: 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()
@ -191,11 +189,11 @@ public extension UIImage {
}
public extension UIColor {
static func bma_color(rgb rgb: Int) -> UIColor {
static func bma_color(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

View File

@ -33,11 +33,12 @@ 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: NSDate(), status: .Success)
let messageModel = MessageModel(uid: "uid", senderId: "senderId", type: "photo-message", isIncoming: true, date: Date(), 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)
@ -60,14 +61,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)
cell.onBubbleLongPressBegan?(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)
cell.onBubbleLongPressEnded?(cell)
XCTAssertTrue(self.interactionHandler.didHandleEndLongPressOnBubble)
}
}

View File

@ -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: NSDate(), status: .Success)
let messageModel = MessageModel(uid: "uid", senderId: "senderId", type: "photo-message", isIncoming: true, date: Date(), 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))

View File

@ -31,11 +31,12 @@ 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(), status: .Success)
let messageModel = MessageModel(uid: "uid", senderId: "senderId", type: "photo-message", isIncoming: true, date: NSDate() as Date, 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)
}
@ -60,18 +61,18 @@ class PhotoMessagePresenterTests: XCTestCase, UICollectionViewDataSource {
PhotoMessagePresenter<PhotoMessageViewModelDefaultBuilder<PhotoMessageModel<MessageModel>>, PhotoMessageTestHandler>.registerCells(collectionView)
collectionView.dataSource = self
collectionView.reloadData()
XCTAssertNotNil(self.presenter.dequeueCell(collectionView: collectionView, indexPath: NSIndexPath(forItem: 0, inSection: 0)))
XCTAssertNotNil(self.presenter.dequeueCell(collectionView: collectionView, indexPath: IndexPath(item: 0, section: 0)))
collectionView.dataSource = nil
}
// MARK: Helpers
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
return self.presenter.dequeueCell(collectionView: collectionView, indexPath: indexPath)
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
return self.presenter.dequeueCell(collectionView: collectionView, indexPath: indexPath as IndexPath)
}
}
@ -79,27 +80,27 @@ class PhotoMessageTestHandler: BaseMessageInteractionHandlerProtocol {
typealias ViewModelT = PhotoMessageViewModel<PhotoMessageModel<MessageModel>>
var didHandleTapOnFailIcon = false
func userDidTapOnFailIcon(viewModel viewModel: ViewModelT, failIconView: UIView) {
func userDidTapOnFailIcon(viewModel: ViewModelT, failIconView: UIView) {
self.didHandleTapOnFailIcon = true
}
var didHandleTapOnAvatar = false
func userDidTapOnAvatar(viewModel viewModel: ViewModelT) {
func userDidTapOnAvatar(viewModel: ViewModelT) {
self.didHandleTapOnAvatar = true
}
var didHandleTapOnBubble = false
func userDidTapOnBubble(viewModel viewModel: ViewModelT) {
func userDidTapOnBubble(viewModel: ViewModelT) {
self.didHandleTapOnBubble = true
}
var didHandleBeginLongPressOnBubble = false
func userDidBeginLongPressOnBubble(viewModel viewModel: ViewModelT) {
func userDidBeginLongPressOnBubble(viewModel: ViewModelT) {
self.didHandleBeginLongPressOnBubble = true
}
var didHandleEndLongPressOnBubble = false
func userDidEndLongPressOnBubble(viewModel viewModel: ViewModelT) {
func userDidEndLongPressOnBubble(viewModel: ViewModelT) {
self.didHandleEndLongPressOnBubble = true
}
}

View File

@ -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: NSDate(), status: .Success)
let messageModel = MessageModel(uid: "uid", senderId: "senderId", type: "text-message", isIncoming: true, date: Date(), status: .success)
let textMessageModel = TextMessageModel(messageModel: messageModel, text: "Some text")
let builder = TextMessagePresenterBuilder(viewModelBuilder: TextMessageViewModelDefaultBuilder<TextMessageModel<MessageModel>>(), interactionHandler: TextMessageTestHandler())
XCTAssertNotNil(builder.createPresenterWithChatItem(textMessageModel))

View File

@ -31,11 +31,12 @@ 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(), status: .Success)
let messageModel = MessageModel(uid: "uid", senderId: "senderId", type: "text-message", isIncoming: true, date: NSDate() as Date, 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())
}
@ -46,7 +47,7 @@ class TextMessagePresenterTests: XCTestCase, UICollectionViewDataSource {
TextMessagePresenter<TextMessageViewModelDefaultBuilder<TextMessageModel<MessageModel>>, TextMessageTestHandler>.registerCells(collectionView)
collectionView.dataSource = self
collectionView.reloadData()
XCTAssertNotNil(self.presenter.dequeueCell(collectionView: collectionView, indexPath: NSIndexPath(forItem: 0, inSection: 0)))
XCTAssertNotNil(self.presenter.dequeueCell(collectionView: collectionView, indexPath: IndexPath(item: 0, section: 0)))
collectionView.dataSource = nil
}
@ -81,35 +82,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
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
return self.presenter.dequeueCell(collectionView: collectionView, indexPath: indexPath)
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
return self.presenter.dequeueCell(collectionView: collectionView, indexPath: indexPath as IndexPath)
}
}
class TextMessageTestHandler: BaseMessageInteractionHandlerProtocol {
typealias ViewModelT = TextMessageViewModel<TextMessageModel<MessageModel>>
func userDidTapOnFailIcon(viewModel viewModel: ViewModelT, failIconView: UIView) {
func userDidTapOnFailIcon(viewModel: ViewModelT, failIconView: UIView) {
}
func userDidTapOnAvatar(viewModel viewModel: ViewModelT) {
func userDidTapOnAvatar(viewModel: ViewModelT) {
}
func userDidTapOnBubble(viewModel viewModel: ViewModelT) {
func userDidTapOnBubble(viewModel: ViewModelT) {
}
func userDidBeginLongPressOnBubble(viewModel viewModel: ViewModelT) {
func userDidBeginLongPressOnBubble(viewModel: ViewModelT) {
}
func userDidEndLongPressOnBubble(viewModel viewModel: ViewModelT) {
func userDidEndLongPressOnBubble(viewModel: ViewModelT) {
}
}

View File

@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>2.1.0</string>
<string>3.0.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>

View File

@ -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.enabled = false
self.bar.sendButton.isEnabled = false
self.bar.inputText = "!"
XCTAssertTrue(self.bar.sendButton.enabled)
XCTAssertTrue(self.bar.sendButton.isEnabled)
}
func testThat_WhenInputTextBecomesEmpty_BarDisablesSendButton() {
self.bar.sendButton.enabled = true
self.bar.sendButton.isEnabled = true
self.bar.inputText = ""
XCTAssertFalse(self.bar.sendButton.enabled)
XCTAssertFalse(self.bar.sendButton.isEnabled)
}
// MARK: - Presenter tests
func testThat_WhenItemViewTapped_ItNotifiesPresenterThatNewItemReceivedFocus() {
self.setupPresenter()
let item = MockInputItem()
self.bar.inputItemViewTapped(createItemView(item))
self.bar.inputItemViewTapped(createItemView(inputItem: 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.enabled = true
self.bar.sendButton.isEnabled = true
self.bar.textView.text = ""
self.bar.textViewDidChange(self.bar.textView)
XCTAssertFalse(self.bar.sendButton.enabled)
XCTAssertFalse(self.bar.sendButton.isEnabled)
}
func testThat_WhenTextViewDidChange_ItEnablesSendButton() {
self.bar.sendButton.enabled = false
self.bar.sendButton.isEnabled = false
self.bar.textView.text = "!"
self.bar.textViewDidChange(self.bar.textView)
XCTAssertTrue(self.bar.sendButton.enabled)
XCTAssertTrue(self.bar.sendButton.isEnabled)
}
func testThat_WhenSendButtonTapped_ItNotifiesPresenter() {
@ -118,7 +118,7 @@ class ChatInputBarTests: XCTestCase {
func testThat_WhenItemViewTapped_ItNotifiesDelegateThatNewItemReceivedFocus() {
self.setupDelegate()
let item = MockInputItem()
self.bar.inputItemViewTapped(createItemView(item))
self.bar.inputItemViewTapped(createItemView(inputItem: 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.enabled)
XCTAssertFalse(self.bar.sendButton.isEnabled)
}
func testThat_WhenItemViewTapped_ItReceivesFocuesByDefault() {
self.setupPresenter()
let item = MockInputItem()
self.bar.inputItemViewTapped(createItemView(item))
self.bar.inputItemViewTapped(createItemView(inputItem: 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(item))
self.bar.inputItemViewTapped(createItemView(inputItem: 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(item))
self.bar.inputItemViewTapped(createItemView(inputItem: 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
}

View File

@ -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
}
}

View File

@ -45,7 +45,7 @@ class ChatInputItemViewTests: XCTestCase {
class MockInputItemViewDelegate: ChatInputItemViewDelegate {
var itemViewTapped = false
func inputItemViewTapped(view: ChatInputItemView) {
func inputItemViewTapped(_ view: ChatInputItemView) {
self.itemViewTapped = true
}
}

View File

@ -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)

View File

@ -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(cellAppearance: nil, authorizationStatusProvider: self.cameraAuthorizationStatusProvider)
self.presenter = LiveCameraCellPresenter(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.postNotificationName(UIApplicationWillResignActiveNotification, object: nil)
self.presenter.notificationCenter.post(name: NSNotification.Name.UIApplicationWillResignActive, 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.postNotificationName(UIApplicationWillResignActiveNotification, object: nil)
self.presenter.notificationCenter.postNotificationName(UIApplicationDidBecomeActiveNotification, object: nil)
self.presenter.notificationCenter.post(name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
self.presenter.notificationCenter.post(name: NSNotification.Name.UIApplicationDidBecomeActive, 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.postNotificationName(UIApplicationWillResignActiveNotification, object: nil)
self.presenter.notificationCenter.postNotificationName(UIApplicationDidBecomeActiveNotification, object: nil)
self.presenter.notificationCenter.post(name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
self.presenter.notificationCenter.post(name: NSNotification.Name.UIApplicationDidBecomeActive, 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.willMoveToWindow(UIWindow())
firstCell.willMove(toWindow: UIWindow())
XCTAssertFalse(mockCaptureSession.isCapturing)
}
}
@ -209,7 +209,7 @@ private class MockLiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
var isInitialized: Bool = false
var isCapturing: Bool = false
func startCapturing(completion: () -> Void) {
func startCapturing(_ completion: @escaping () -> Void) {
guard !self.isCapturing else { return }
self.isInitialized = true
@ -217,7 +217,7 @@ private class MockLiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
completion()
}
func stopCapturing(completion: () -> Void) {
func stopCapturing(_ completion: @escaping () -> Void) {
guard self.isCapturing else { return }
self.isInitialized = true

View File

@ -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)
self.inputItem.handleInput(5 as AnyObject)
XCTAssertFalse(handled)
}

View File

@ -45,7 +45,7 @@ class TextChatInputItemTests: XCTestCase {
self.inputItem.textInputHandler = { text in
handled = true
}
self.inputItem.handleInput("text")
self.inputItem.handleInput("text" as AnyObject)
XCTAssertTrue(handled)
}
@ -54,7 +54,7 @@ class TextChatInputItemTests: XCTestCase {
self.inputItem.textInputHandler = { text in
handled = true
}
self.inputItem.handleInput(5)
self.inputItem.handleInput(5 as AnyObject)
XCTAssertFalse(handled)
}
}

View File

@ -10,7 +10,6 @@
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 */; };
@ -45,13 +44,6 @@
remoteGlobalIDString = C33FBFA41BDE441C008E3545;
remoteInfo = ChattoApp;
};
C33FBFC51BDE441C008E3545 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = C33FBF9D1BDE441C008E3545 /* Project object */;
proxyType = 1;
remoteGlobalIDString = C33FBFA41BDE441C008E3545;
remoteInfo = ChattoApp;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
@ -79,9 +71,6 @@
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>"; };
@ -123,13 +112,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
C33FBFC11BDE441C008E3545 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@ -156,7 +138,6 @@
children = (
C33FBFA71BDE441C008E3545 /* ChattoApp */,
C33FBFBC1BDE441C008E3545 /* ChattoAppTests */,
C33FBFC71BDE441C008E3545 /* ChattoAppUITests */,
C33FBFA61BDE441C008E3545 /* Products */,
0852C8B139C7CFA0A1C22090 /* Frameworks */,
B616EDF620454A787C7E7D84 /* Pods */,
@ -168,7 +149,6 @@
children = (
C33FBFA51BDE441C008E3545 /* ChattoApp.app */,
C33FBFB91BDE441C008E3545 /* ChattoAppTests.xctest */,
C33FBFC41BDE441C008E3545 /* ChattoAppUITests.xctest */,
);
name = Products;
sourceTree = "<group>";
@ -194,15 +174,6 @@
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 = (
@ -307,24 +278,6 @@
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 */
@ -337,13 +290,11 @@
TargetAttributes = {
C33FBFA41BDE441C008E3545 = {
CreatedOnToolsVersion = 7.1;
LastSwiftMigration = 0800;
};
C33FBFB81BDE441C008E3545 = {
CreatedOnToolsVersion = 7.1;
TestTargetID = C33FBFA41BDE441C008E3545;
};
C33FBFC31BDE441C008E3545 = {
CreatedOnToolsVersion = 7.1;
LastSwiftMigration = 0800;
TestTargetID = C33FBFA41BDE441C008E3545;
};
};
@ -363,7 +314,6 @@
targets = (
C33FBFA41BDE441C008E3545 /* ChattoApp */,
C33FBFB81BDE441C008E3545 /* ChattoAppTests */,
C33FBFC31BDE441C008E3545 /* ChattoAppUITests */,
);
};
/* End PBXProject section */
@ -387,13 +337,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
C33FBFC21BDE441C008E3545 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@ -437,7 +380,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
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";
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";
showEnvVarsInLog = 0;
};
F8D7533B1E7B2E137B143EBD /* [CP] Copy Pods Resources */ = {
@ -494,14 +437,6 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
C33FBFC01BDE441C008E3545 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C33FBFC91BDE441C008E3545 /* ChattoAppUITests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
@ -510,11 +445,6 @@
target = C33FBFA41BDE441C008E3545 /* ChattoApp */;
targetProxy = C33FBFBA1BDE441C008E3545 /* PBXContainerItemProxy */;
};
C33FBFC61BDE441C008E3545 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = C33FBFA41BDE441C008E3545 /* ChattoApp */;
targetProxy = C33FBFC51BDE441C008E3545 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@ -540,6 +470,7 @@
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++";
@ -580,7 +511,7 @@
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 2.3;
SWIFT_VERSION = 3.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@ -588,6 +519,7 @@
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++";
@ -621,7 +553,7 @@
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 2.3;
SWIFT_VERSION = 3.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
@ -631,7 +563,6 @@
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;
@ -645,7 +576,6 @@
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;
@ -679,30 +609,6 @@
};
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 */
@ -733,15 +639,6 @@
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 */;

View File

@ -39,16 +39,6 @@
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

View File

@ -29,33 +29,31 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> 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:.
}
}

View File

@ -26,7 +26,7 @@ import Foundation
import ChattoAdditions
class BaseMessageCollectionViewCellAvatarStyle: BaseMessageCollectionViewCellDefaultStyle {
override func avatarSize(viewModel viewModel: MessageViewModelProtocol) -> CGSize {
override func avatarSize(viewModel: MessageViewModelProtocol) -> CGSize {
// Display avatar for both incoming and outgoing messages for demo purpose
return CGSize(width: 35, height: 35)
}

View File

@ -36,24 +36,24 @@ class BaseMessageHandler {
init (messageSender: FakeMessageSender) {
self.messageSender = messageSender
}
func userDidTapOnFailIcon(viewModel viewModel: DemoMessageViewModelProtocol) {
func userDidTapOnFailIcon(viewModel: DemoMessageViewModelProtocol) {
print("userDidTapOnFailIcon")
self.messageSender.sendMessage(viewModel.messageModel)
}
func userDidTapOnAvatar(viewModel viewModel: MessageViewModelProtocol) {
func userDidTapOnAvatar(viewModel: MessageViewModelProtocol) {
print("userDidTapOnAvatar")
}
func userDidTapOnBubble(viewModel viewModel: DemoMessageViewModelProtocol) {
func userDidTapOnBubble(viewModel: DemoMessageViewModelProtocol) {
print("userDidTapOnBubble")
}
func userDidBeginLongPressOnBubble(viewModel viewModel: DemoMessageViewModelProtocol) {
func userDidBeginLongPressOnBubble(viewModel: DemoMessageViewModelProtocol) {
print("userDidBeginLongPressOnBubble")
}
func userDidEndLongPressOnBubble(viewModel viewModel: DemoMessageViewModelProtocol) {
func userDidEndLongPressOnBubble(viewModel: DemoMessageViewModelProtocol) {
print("userDidEndLongPressOnBubble")
}
}

View File

@ -30,14 +30,14 @@ final class ChatItemsDemoDecorator: ChatItemsDecoratorProtocol {
struct Constants {
static let shortSeparation: CGFloat = 3
static let normalSeparation: CGFloat = 10
static let timeIntervalThresholdToIncreaseSeparation: NSTimeInterval = 120
static let timeIntervalThresholdToIncreaseSeparation: TimeInterval = 120
}
func decorateItems(chatItems: [ChatItemProtocol]) -> [DecoratedChatItem] {
func decorateItems(_ chatItems: [ChatItemProtocol]) -> [DecoratedChatItem] {
var decoratedChatItems = [DecoratedChatItem]()
let calendar = NSCalendar.currentCalendar()
let calendar = Calendar.current
for (index, chatItem) in chatItems.enumerate() {
for (index, chatItem) in chatItems.enumerated() {
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.compareDate(currentMessage.date, toDate: previousMessage.date, toUnitGranularity: NSCalendarUnit.Day) != NSComparisonResult.OrderedSame
addTimeSeparator = !calendar.isDate(currentMessage.date, inSameDayAs: previousMessage.date)
} else {
addTimeSeparator = true
}
@ -77,13 +77,13 @@ final class ChatItemsDemoDecorator: ChatItemsDecoratorProtocol {
chatItem: chatItem,
decorationAttributes: ChatItemDecorationAttributes(bottomMargin: bottomMargin, showsTail: showsTail, canShowAvatar: showsTail))
)
decoratedChatItems.appendContentsOf(additionalItems)
decoratedChatItems.append(contentsOf: 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.timeIntervalSinceDate(currentMessage.date) > Constants.timeIntervalThresholdToIncreaseSeparation {
} else if nextMessage.date.timeIntervalSince(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
}
}

View File

@ -26,7 +26,7 @@ import UIKit
class ConversationsViewController: UITableViewController {
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
var initialCount = 0
let pageSize = 50
@ -39,18 +39,17 @@ class ConversationsViewController: UITableViewController {
} else if segue.identifier == "10000 messages" {
initialCount = 10000
} else if segue.identifier == "overview" {
dataSource = FakeDataSource(messages: TutorialMessageFactory.createMessages().map { $0 }, pageSize: pageSize)
dataSource = FakeDataSource(messages: TutorialMessageFactory.createMessages(), pageSize: pageSize)
} else {
assert(false, "segue not handled!")
}
let chatController = { () -> DemoChatViewController? in
if let controller = segue.destinationViewController as? DemoChatViewController {
if let controller = segue.destination as? DemoChatViewController {
return controller
}
if let tabController = segue.destinationViewController as? UITabBarController,
controller = tabController.viewControllers?.first as? DemoChatViewController {
if let tabController = segue.destination as? UITabBarController,
let controller = tabController.viewControllers?.first as? DemoChatViewController {
return controller
}
return nil

View File

@ -42,9 +42,9 @@ class DemoChatViewController: BaseChatViewController {
override func viewDidLoad() {
super.viewDidLoad()
let image = UIImage(named: "bubble-incoming-tail-border", inBundle: NSBundle(forClass: DemoChatViewController.self), compatibleWithTraitCollection: nil)?.bma_tintWithColor(UIColor.blueColor())
let image = UIImage(named: "bubble-incoming-tail-border", in: Bundle(for: DemoChatViewController.self), compatibleWith: nil)?.bma_tintWithColor(.blue)
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