Compare commits

...

69 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
Diego Sanchez 1fb8acb292 Bumps version to 2.1.0 2016-09-17 22:19:46 +02:00
Diego Sanchez bdf753f4e8 Updates changelog for 2.1 release 2016-09-17 22:19:46 +02:00
geegaset 7f46182366 accessibility identifier for ChatInputBar placeholder (#218) 2016-09-12 15:37:29 +01:00
geegaset 7029891b06 use camera roll as default pictures source (#215)
* use camera roll as default pictures source

* creationDate restored
2016-09-02 14:16:45 +01:00
Anton Schukin 1bab617b8d Merge pull request #213 from badoo/IOS-9716
sort assets by creation date
2016-09-01 14:18:34 +01:00
Peter Kolpashchikov 9a8dd758e0 sort assets by creation date 2016-09-01 14:07:31 +01:00
Diego Sánchez 585f0dbfc7 Makes some functions and properties public in TextMessageCollectionViewCellDefaultStyle (#207) 2016-08-23 14:49:24 +01:00
geegaset b98c487643 TabInputButton accessibilityId introduced (#206)
* TabInputButton accessibilityId introduced

* TabInputButton accessibilityId introduced
2016-08-23 11:48:13 +01:00
Diego Sánchez 22a3833761 Makes LiveCameraCellPresenter public and reusable out of ChattoAdditions (#205)
* Makes LiveCameraCellPresenter public and reusable out of ChattoAdditions

* Fixes tests
2016-08-22 22:14:24 +01:00
Daniel Burgess ab286307c3 Fix a bug causing keyboard view offset to be incorrect (#204)
* Fix a bug causing keyboard view offset to be incorrect

In some rare cases, if the height of the view is a fractional point
(i.e., not a whole number), it would cause the views to not be offset
despite the keyboard being shown on top of them. This does not happen
with every fractional height. Different devices also behave a little
differently in seemingly identical layouts, due to their pixel density
being different.

The base issue is that, due to floating point rounding errors, two
values that _should_ be identical and pass the guard fail to do so,
because the lack of precision results in them not being equal. By
flooring the values, we can ignore really minor differences and ensure
rounding errors don't cause this issue.

* Unify bma_round methods to use correct calculation

Thanks to @diegosanchezr for the suggested improvement.

* Revert ChattoAdditions bma_round change

Unfortunately, removing this in favor of the Chatto version broke size
calculations, so putting it back...

* Switch to using infix operator to check float comparison

* Add utils to Chatto project
2016-08-22 15:46:32 +01:00
Diego Sánchez e881ae93aa Merge pull request #202 from andris-zalitis/fix-for-issue-197
Fixes issue with hidesBottomBarWhenPushed and a tabbar
2016-08-19 15:37:39 +01:00
Andris 94dd99f148 Fixes issue with hidesBottomBarWhenPushed and a tabbar 2016-08-15 10:18:52 +03:00
Anton 3781a41d58 Merge pull request #200 from TerekhovAnton/master
So generated initialisers for structs are internal and not visible from another module..
2016-08-12 15:15:33 +01:00
Anton 57cede17d9 Merge branch 'master' into master 2016-08-12 15:06:17 +01:00
Anton Terehov 4bfc854a0b So generated initialisers for structs are internal and not visible from another module.. 2016-08-12 15:03:16 +01:00
Diego Sánchez cd1765d692 Removes references to dev branch in readme 2016-08-12 13:05:17 +01:00
Diego Sánchez 0184efc4e3 Merge pull request #199 from TerekhovAnton/master
Adds possibility to configure colour of LiveCameraCell, also providing default option
2016-08-12 13:02:27 +01:00
Anton Terehov ed76fe6336 Code review comments 2016-08-12 12:00:32 +01:00
Anton Terehov 9c3b40df1d Refactors method name 2016-08-11 17:03:44 +01:00
Anton Terehov 6307c3119f Adds possibility to configure colour of LiveCameraCell, also providing default option. 2016-08-11 16:58:43 +01:00
Diego Sánchez fbb95b0c60 Merge pull request #198 from badoo/xCode8Beta5Compatibility
Xcode 8 Beta 5 compatibility.
2016-08-10 17:14:57 +01:00
Bohdan Orlov 3608e23120 xCode 8 Beta 5 compatibility. 2016-08-10 16:31:53 +01:00
Diego Sánchez f2beddc095 Updates Chatto version in readme 2016-08-10 12:41:17 +01:00
Diego Sanchez 0f55395762 Bumps version to 2.0.1 2016-08-08 14:46:05 +01:00
Diego Sánchez eec82d04c9 Merge pull request #196 from badoo/fix-chattoadditions-compilation-in-optimized-mode
Fix ChattoAdditions compilation in optimized mode
2016-08-08 14:18:04 +01:00
Diego Sanchez d92a312bf0 Updates Readme 2016-08-08 14:08:11 +01:00
Diego Sanchez 953659eb01 Runs Pod update 2016-08-08 14:07:37 +01:00
Diego Sanchez a7cbdddaf3 Updates Podfile 2016-08-08 14:06:55 +01:00
Diego Sanchez 311831d4af Allows compiling ChattoAdditions with -O instead of just debug and whole-module. This is needed to pass podspec validation 2016-08-08 14:05:58 +01:00
Diego Sanchez 75c6ccd32d Compatibility for Xcode 8 beta 2016-08-08 13:00:58 +01:00
Diego Sanchez dd34d2ebd0 Removes warnings 2016-08-08 12:58:08 +01:00
Diego Sanchez 5656c43bb8 Updates Changelog and Readme 2016-08-08 12:46:33 +01:00
Diego Sánchez 8dd95ad13b Merge pull request #195 from badoo/dev
Merge Dev into Master
2016-08-08 12:35:02 +01:00
Diego Sanchez 1c1e5d21de Merge remote-tracking branch 'origin/master' into dev 2016-08-08 11:54:06 +01:00
Diego Sánchez 8b003c64b8 Prepare release 2.0 (#194)
* Adds changelog

* Bumps version to 2.0.0

* Updates readme

* Updates readme with workaround for SR-2223

* Fix for readme
2016-08-08 11:33:12 +01:00
Zhao Wang 9f344f39a6 Add public initializer with frame and textContainer for ExpandableTextView (#193)
* Expose public initializer with frame and textContainer for ExpandableTextView

* add override keyword
2016-08-07 21:36:48 +01:00
Diego Sánchez a541e5b9f3 Makes MessageViewModelProtocol callbacks willBeShown and wasHidden optional (#190) 2016-08-01 21:09:47 +01:00
Max Konovalov 1ef46a0e35 Add updating logic to text message (#178)
* Add updating logic to text message

* Change Observable to class

* Fix avatar hiding

* Move willBeShown/wasHidden declarations to common protocol
2016-07-31 22:53:52 +01:00
Diego Sánchez 7541a5ab64 Workaround for issue 187 (#188)
* Sets -Owholemodule in ChattoApp's ChattoAdditions so it compiles in release config (workaround for SR-2223)

* Updates pods
2016-07-30 14:48:34 +01:00
Diego Sanchez 3be87bb3ef Fixes travis script 2016-04-21 12:15:35 +01:00
135 changed files with 2341 additions and 2228 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 - file_length
- force_cast - force_cast
- function_body_length - function_body_length

View File

@ -1,12 +1,10 @@
language: objective-c language: objective-c
osx_image: xcode7.3 osx_image: xcode8.1
script: script:
- set -o pipefail - 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 - xcodebuild clean build test -workspace ./Chatto.xcworkspace -scheme Chatto -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 7' -configuration Debug | xcpretty
- rm -rf ~/Library/Developer/Xcode/DerivedData - bash <(curl -s https://codecov.io/bash) -J 'Chatto'
- xcodebuild clean build test -workspace ./Chatto.xcworkspace -scheme Chatto -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 6,OS=9.3' -configuration Debug | xcpretty - xcodebuild clean build test -workspace ./Chatto.xcworkspace -scheme ChattoAdditions -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 7' -configuration Debug | xcpretty
- (curl -s https://codecov.io/bash) | bash - bash <(curl -s https://codecov.io/bash) -J 'ChattoAdditions'
- rm -rf ~/Library/Developer/Xcode/DerivedData - xcodebuild clean build test -workspace ./ChattoApp/ChattoApp.xcworkspace -scheme ChattoApp -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 7' -configuration Debug | xcpretty
- xcodebuild clean build test -workspace ./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

92
CHANGELOG.md Normal file
View File

@ -0,0 +1,92 @@
### 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)
* Fixes collection view insets when keyboard is shown [#204](https://github.com/badoo/Chatto/pull/204) - [@dbburgess](https://github.com/dbburgess)
* LiveCameraCellPresenter made public [#205](https://github.com/badoo/Chatto/pull/205) - [@diegosanchezr](https://github.com/diegosanchezr)
* Fixes order of photos on iOS 10 [#215](https://github.com/badoo/Chatto/pull/215) - [@geegaset](https://github.com/geegaset)
* Adds accessibility identifiers in ChatInputBar [#218](https://github.com/badoo/Chatto/pull/218), [#206](https://github.com/badoo/Chatto/pull/206) - [@geegaset](https://github.com/geegaset)
* Xcode 8 - Swift 2.3 support
### 2.0 (Aug 8, 2016)
* Renames `ChatViewController` to `BaseChatViewController`. [#31](https://github.com/badoo/Chatto/pull/31) - [@diegosanchezr](https://github.com/diegosanchezr)
* Makes presenters easier to reuse by relaxing generic constraints [#35](https://github.com/badoo/Chatto/pull/35) - [@diegosanchezr](https://github.com/diegosanchezr)
* Fixes issues when the dataSource updates with a different instance for a previously existing chatItem. [#36](https://github.com/badoo/Chatto/pull/36) - [@diegosanchezr](https://github.com/diegosanchezr)
* `BaseChatViewController` exposes `chatItemCompanionCollection`. [#39](https://github.com/badoo/Chatto/pull/39) - [@diegosanchezr](https://github.com/diegosanchezr)
* This gives access to the presenter and decorationAttributes of a chatItem.
* `ChatDataSourceDelegateProtocol` gets `chatDataSourceDidUpdate(:context)`. [#39](https://github.com/badoo/Chatto/pull/39) - [@diegosanchezr](https://github.com/diegosanchezr)
* This allows to customize the update of the UICollectionView (reloadData vs performBatchUpdates)
* `MessageViewModelProtocol` loses `status` setter. `messageModel` property is removed. [#44](https://github.com/badoo/Chatto/pull/44) - [@diegosanchezr](https://github.com/diegosanchezr)
* `ChatDataSourceProtocol` loses the completion blocks in `loadNext()` and `loadPrevious()`. [#45](https://github.com/badoo/Chatto/pull/45) - [@diegosanchezr](https://github.com/diegosanchezr)
* It's now the dataSource's responsability to notify when pagination finishes (by triggering `chatDataSourceDidUpdate(:context)`)
* `BaseChatViewController` is no longer retained until a running update finishes. [#47](https://github.com/badoo/Chatto/pull/47) - [@diegosanchezr](https://github.com/diegosanchezr)
* `BaseMessageCollectionViewCell` can now be subclassed out of `ChattoAdditions`. [#48](https://github.com/badoo/Chatto/pull/48) - [@bcamur](https://github.com/bcamur)
* `ChatInputBarDelegate` made public. [#50](https://github.com/badoo/Chatto/pull/50) - [@AntonPalich](https://github.com/AntonPalich)
* `PhotoMessagePresenter` exposes `viewModelBuilder` and `interactionHandler`. [#52](https://github.com/badoo/Chatto/pull/52) - [@AntonPalich](https://github.com/AntonPalich)
* Avatars in cells. [#55](https://github.com/badoo/Chatto/pull/55) - [@zwang](https://github.com/zwang), [#176](https://github.com/badoo/Chatto/pull/176) - [@maxkonovalov](https://github.com/maxkonovalov)
* `MessageViewModelProtocol` gets `avatarImage` property
* `BaseMessageCollectionViewCellStyleProtocol` gets methods to configure the layout of the avatar
* `BaseMessageInteractionHandlerProtocol` gets `userDidTapOnAvatar(viewModel:)`
* `ChatItemDecorationAttributes` gets `canShowAvatar`
* `BaseMessagePresenter` exposes user events (so subclasses can complement or bypass the interactionHandler). [#62](https://github.com/badoo/Chatto/pull/62) - [@AntonPalich](https://github.com/AntonPalich)
* `BaseMessagePresenter.onCellBubbleTapped()`
* `BaseMessagePresenter.onCellBubbleLongPressed()`
* `BaseMessagePresenter.onCellFailedButtonTapped()`
* `BaseMessagePresenter` exposes `messageModel`, `sizingCell`, `viewModelBuilder`, `interactionHandler` and `cellStyle`. [#63](https://github.com/badoo/Chatto/pull/63) - [@AntonPalich](https://github.com/AntonPalich)
* `PhotosChatInputItem` gets new callbacks `cameraPermissionHandler`, `photosPermissionHandler`. [#65](https://github.com/badoo/Chatto/pull/65) - [@Viacheslav-Radchenko](https://github.com/Viacheslav-Radchenko)
* Enhanced customization for cells and the input component. [#67](https://github.com/badoo/Chatto/pull/67) - [@diegosanchezr](https://github.com/diegosanchezr), [#73](https://github.com/badoo/Chatto/pull/73) [@AntonPalich](https://github.com/AntonPalich)
* `BaseChatViewController` exposes `referenceIndexPathsToRestoreScrollPositionOnUpdate`. [#75](https://github.com/badoo/Chatto/pull/75) - [@diegosanchezr](https://github.com/diegosanchezr)
* It can be overriden to customize how the scroll position is preserved after a update.
* Fixes blinking when sending text messages on iOS 8
* Adds placeholders when there are very few photos in the camera roll. [#85](https://github.com/badoo/Chatto/pull/85) - [@Viacheslav-Radchenko](https://github.com/Viacheslav-Radchenko)
* `BaseChatViewController` exposes `createPresenterFactory()`. It can be overriden to provide a factory of presenters with custom logic. [#89](https://github.com/badoo/Chatto/pull/89) - [@weyg](https://github.com/weyg)
* `BaseChatViewController` exposes `inputContainer`. [#90](https://github.com/badoo/Chatto/pull/90) - [@diegosanchezr](https://github.com/diegosanchezr)
* Fixes insets issues. [#91](https://github.com/badoo/Chatto/pull/91), [#110](https://github.com/badoo/Chatto/pull/110) - [@diegosanchezr](https://github.com/diegosanchezr)
* Fixes memory leak when screen is left with the keyboard opened. [#93](https://github.com/badoo/Chatto/pull/93) - [@diegosanchezr](https://github.com/diegosanchezr)
* PhotosChatInputItem listens to changes in the camera roll and updates accordingly. [#94](https://github.com/badoo/Chatto/pull/94) - [@AntonPalich](https://github.com/AntonPalich)
* Fixes issues with the keyboard [#96](https://github.com/badoo/Chatto/pull/96), [#115](https://github.com/badoo/Chatto/pull/115) - [@diegosanchezr](https://github.com/diegosanchezr), [#108](https://github.com/badoo/Chatto/pull/108) - [@AntonPalich](https://github.com/AntonPalich)
* `BaseChatViewController` gets `setChatDataSource(_:triggeringUpdateType)`. [#98](https://github.com/badoo/Chatto/pull/98) - [@diegosanchezr](https://github.com/diegosanchezr)
* This allows to set a dataSource and not trigger an update of the collection view immediately.
* This can be useful on the first load of the conversation when the dataSource doesn't have any data yet.
* `ChatInputBar` gets `shouldEnableSendButton` closure to customize when the send button is enabled [#103](https://github.com/badoo/Chatto/pull/103) - [@ikashkuta](https://github.com/ikashkuta)
* Fixes UIMenuController not going away on the first tap outside when the keyboard is dismissed. [#104](https://github.com/badoo/Chatto/pull/104) - [@diegosanchezr](https://github.com/diegosanchezr)
* `ChatInputBarDelegate` gets `inputBarShouldBeginTextEditing(_:)` and `inputBar(_: shouldFocusOnItem)` [#105](https://github.com/badoo/Chatto/pull/105) - [@ikashkuta](https://github.com/ikashkuta)
* `BaseChatViewController` gets `accessoryViewRevealerConfig`. [#114](https://github.com/badoo/Chatto/pull/114) - [@diegosanchezr](https://github.com/diegosanchezr)
* Allows setting an angle threshold that triggers the revealing
* Allows applying a transform to the finger's translation (to mimic a resistance effect)
* Fixes sizing of text cells: [#122](https://github.com/badoo/Chatto/pull/122), [#123](https://github.com/badoo/Chatto/pull/123) - [@AntonPalich](https://github.com/AntonPalich), [#127](https://github.com/badoo/Chatto/pull/127), [#161](https://github.com/badoo/Chatto/pull/161)- [@diegosanchezr](https://github.com/diegosanchezr)
* `ChatInputBar` gets `focusOnInputItem(_:)` so that an input item can be focused programmatically. [#124](https://github.com/badoo/Chatto/pull/124) - [@ikashkuta](https://github.com/ikashkuta)
* `BaseMessagePresenter` gets user events `onCellBubbleLongPressBegan()` and `onCellBubbleLongPressEnded`. [#125](https://github.com/badoo/Chatto/pull/125) - [@AntonPalich](https://github.com/AntonPalich)
* `PhotoBubbleView` can be subclassed. [#130](https://github.com/badoo/Chatto/pull/130) - [@AntonPalich](https://github.com/AntonPalich)
* Configurable margins for revealable timestamps. [#135](https://github.com/badoo/Chatto/pull/135) - [@AntonPalich](https://github.com/AntonPalich)
* `BaseChatViewController` gets `endsEditingWhenTappingOnChatBackground` to dismiss the keyboard automatically. [#138](https://github.com/badoo/Chatto/pull/138) - [@diegosanchezr](https://github.com/diegosanchezr)
* `BaseMessageCollectionViewCellDefaultStyle` gets optional `bubbleBorderImages` [#139](https://github.com/badoo/Chatto/pull/139) - [@diegosanchezr](https://github.com/diegosanchezr)
* `ChatItemsDecorator` can now have a last word about the `uid` used by the update engine. [#143](https://github.com/badoo/Chatto/pull/143) - [@diegosanchezr](https://github.com/diegosanchezr)
* `BaseChatViewController` gets `updatesConfig` property. [#145](https://github.com/badoo/Chatto/pull/145) - [@diegosanchezr](https://github.com/diegosanchezr)
* `coalesceUpdates` controls whether updates are combined (if dataSource notifies about changes while there is a running update)
* `fastUpdates` controls whether a UICollectionView update can be performed before the previous one has finished.
* Exposes `BaseChatViewController.updateQueue`. [#150](https://github.com/badoo/Chatto/pull/150) - [@ikashkuta](https://github.com/ikashkuta), [#169](https://github.com/badoo/Chatto/pull/169) - [@diegosanchezr](https://github.com/diegosanchezr)
* Allows clients to pause updates in the UICollectionView.
* Performance optimizations for text cells. [#144](https://github.com/badoo/Chatto/pull/144), [#166](https://github.com/badoo/Chatto/pull/166) - [@diegosanchezr](https://github.com/diegosanchezr)
* `ChatInputBar` gets `maxCharactersCount` to limit text input size
* Allows `ChatInputBar` to be instantiated from an own nib. [#153](https://github.com/badoo/Chatto/pull/153) - [@makoni](https://github.com/makoni)
* Enhanced customization for buttons in `ChatInputBar`. [#154](https://github.com/badoo/Chatto/pull/154) - [@diegosanchezr](https://github.com/diegosanchezr)
* Fixes memory leak. [#165](https://github.com/badoo/Chatto/pull/165) - [@AntonPalich](https://github.com/AntonPalich)
* Improves responsiveness of the camera. [#168](https://github.com/badoo/Chatto/pull/168), [#173](https://github.com/badoo/Chatto/pull/173) - [@diegosanchezr](https://github.com/diegosanchezr)
* Preserves height of the input view when switching between input items. [#170](https://github.com/badoo/Chatto/pull/170), [#174](https://github.com/badoo/Chatto/pull/174) - [@ikashkuta](https://github.com/ikashkuta)
* `AccessoryViewRevealable` gets `allowAccessoryViewRevealing`. [#175](https://github.com/badoo/Chatto/pull/175) - [@ikashkuta](https://github.com/ikashkuta)
* Allows to control whether timestamp can be revealed on a per cell basis.
### 1.0.1 (Jan 14, 2016)
* Support for Carthage
### 1.0 (Nov 27, 2015)
* First version

View File

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

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "0720" LastUpgradeVersion = "0800"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "0720" LastUpgradeVersion = "0800"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@ -7,6 +7,7 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
B3B1B0FF1D6B40DF00D1183D /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B1B0FE1D6B40DF00D1183D /* Utils.swift */; };
C31E919A1BFF4CA300339585 /* BaseChatViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31E91991BFF4CA300339585 /* BaseChatViewControllerTests.swift */; }; C31E919A1BFF4CA300339585 /* BaseChatViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31E91991BFF4CA300339585 /* BaseChatViewControllerTests.swift */; };
C321C3961BE78835009803D1 /* CollectionChangesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C321C3951BE78835009803D1 /* CollectionChangesTests.swift */; }; C321C3961BE78835009803D1 /* CollectionChangesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C321C3951BE78835009803D1 /* CollectionChangesTests.swift */; };
C321DDA91BE9649F00DE88CC /* BaseChatItemPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C321DD941BE9649F00DE88CC /* BaseChatItemPresenter.swift */; }; C321DDA91BE9649F00DE88CC /* BaseChatItemPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C321DD941BE9649F00DE88CC /* BaseChatItemPresenter.swift */; };
@ -48,6 +49,7 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
55E85D821BE390BE001885AD /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 55E85D821BE390BE001885AD /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B3B1B0FE1D6B40DF00D1183D /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; };
C31E91991BFF4CA300339585 /* BaseChatViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseChatViewControllerTests.swift; sourceTree = "<group>"; }; C31E91991BFF4CA300339585 /* BaseChatViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseChatViewControllerTests.swift; sourceTree = "<group>"; };
C321C3951BE78835009803D1 /* CollectionChangesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionChangesTests.swift; sourceTree = "<group>"; }; C321C3951BE78835009803D1 /* CollectionChangesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionChangesTests.swift; sourceTree = "<group>"; };
C321DD941BE9649F00DE88CC /* BaseChatItemPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseChatItemPresenter.swift; sourceTree = "<group>"; }; C321DD941BE9649F00DE88CC /* BaseChatItemPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseChatItemPresenter.swift; sourceTree = "<group>"; };
@ -188,6 +190,7 @@
C3E905041BE0521700C662A2 /* Info.plist */, C3E905041BE0521700C662A2 /* Info.plist */,
C342D0C01C638A2C008A4605 /* ReadOnlyOrderedDictionary.swift */, C342D0C01C638A2C008A4605 /* ReadOnlyOrderedDictionary.swift */,
C36281EC1BF10086004D6BCE /* SerialTaskQueue.swift */, C36281EC1BF10086004D6BCE /* SerialTaskQueue.swift */,
B3B1B0FE1D6B40DF00D1183D /* Utils.swift */,
C321DD931BE9649F00DE88CC /* Chat Items */, C321DD931BE9649F00DE88CC /* Chat Items */,
C3E904AE1BE0509E00C662A2 /* ChatController */, C3E904AE1BE0509E00C662A2 /* ChatController */,
); );
@ -265,7 +268,7 @@
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastSwiftUpdateCheck = 0710; LastSwiftUpdateCheck = 0710;
LastUpgradeCheck = 0710; LastUpgradeCheck = 0800;
ORGANIZATIONNAME = Badoo; ORGANIZATIONNAME = Badoo;
TargetAttributes = { TargetAttributes = {
C32BB71F1BE0504D0069EC50 = { C32BB71F1BE0504D0069EC50 = {
@ -349,6 +352,7 @@
C36281EB1BF0F62F004D6BCE /* DummyChatItemPresenter.swift in Sources */, C36281EB1BF0F62F004D6BCE /* DummyChatItemPresenter.swift in Sources */,
C3C7C3981CAC4BAC00A49929 /* ChatCollectionViewLayout.swift in Sources */, C3C7C3981CAC4BAC00A49929 /* ChatCollectionViewLayout.swift in Sources */,
C3C7C39B1CAC4BAC00A49929 /* KeyboardTracker.swift in Sources */, C3C7C39B1CAC4BAC00A49929 /* KeyboardTracker.swift in Sources */,
B3B1B0FF1D6B40DF00D1183D /* Utils.swift in Sources */,
C342D0C11C638A2C008A4605 /* ReadOnlyOrderedDictionary.swift in Sources */, C342D0C11C638A2C008A4605 /* ReadOnlyOrderedDictionary.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -391,8 +395,10 @@
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
@ -420,7 +426,7 @@
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 2.3; SWIFT_VERSION = 3.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = ""; VERSION_INFO_PREFIX = "";
@ -440,8 +446,10 @@
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
@ -462,7 +470,7 @@
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 2.3; SWIFT_VERSION = 3.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES; VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";

View File

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

View File

@ -25,7 +25,7 @@ THE SOFTWARE.
import Foundation import Foundation
public protocol ChatItemsDecoratorProtocol { public protocol ChatItemsDecoratorProtocol {
func decorateItems(chatItems: [ChatItemProtocol]) -> [DecoratedChatItem] func decorateItems(_ chatItems: [ChatItemProtocol]) -> [DecoratedChatItem]
} }
public struct DecoratedChatItem: UniqueIdentificable { public struct DecoratedChatItem: UniqueIdentificable {

View File

@ -35,29 +35,29 @@ public protocol ChatItemDecorationAttributesProtocol {
} }
public protocol ChatItemPresenterProtocol: class { public protocol ChatItemPresenterProtocol: class {
static func registerCells(collectionView: UICollectionView) static func registerCells(_ collectionView: UICollectionView)
var canCalculateHeightInBackground: Bool { get } // Default is false var canCalculateHeightInBackground: Bool { get } // Default is false
func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat
func dequeueCell(collectionView collectionView: UICollectionView, indexPath: NSIndexPath) -> UICollectionViewCell func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell
func configureCell(cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) func configureCell(_ cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?)
func cellWillBeShown(cell: UICollectionViewCell) // optional func cellWillBeShown(_ cell: UICollectionViewCell) // optional
func cellWasHidden(cell: UICollectionViewCell) // optional func cellWasHidden(_ cell: UICollectionViewCell) // optional
func shouldShowMenu() -> Bool // optional. Default is false func shouldShowMenu() -> Bool // optional. Default is false
func canPerformMenuControllerAction(action: Selector) -> Bool // optional. Default is false func canPerformMenuControllerAction(_ action: Selector) -> Bool // optional. Default is false
func performMenuControllerAction(action: Selector) // optional func performMenuControllerAction(_ action: Selector) // optional
} }
public extension ChatItemPresenterProtocol { // Optionals public extension ChatItemPresenterProtocol { // Optionals
var canCalculateHeightInBackground: Bool { return false } var canCalculateHeightInBackground: Bool { return false }
func cellWillBeShown(cell: UICollectionViewCell) {} func cellWillBeShown(_ cell: UICollectionViewCell) {}
func cellWasHidden(cell: UICollectionViewCell) {} func cellWasHidden(_ cell: UICollectionViewCell) {}
func shouldShowMenu() -> Bool { return false } func shouldShowMenu() -> Bool { return false }
func canPerformMenuControllerAction(action: Selector) -> Bool { return false } func canPerformMenuControllerAction(_ action: Selector) -> Bool { return false }
func performMenuControllerAction(action: Selector) {} func performMenuControllerAction(_ action: Selector) {}
} }
public protocol ChatItemPresenterBuilderProtocol { public protocol ChatItemPresenterBuilderProtocol {
func canHandleChatItem(chatItem: ChatItemProtocol) -> Bool func canHandleChatItem(_ chatItem: ChatItemProtocol) -> Bool
func createPresenterWithChatItem(chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol func createPresenterWithChatItem(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol
var presenterType: ChatItemPresenterProtocol.Type { get } var presenterType: ChatItemPresenterProtocol.Type { get }
} }

View File

@ -27,8 +27,8 @@ import Foundation
// Handles messages that aren't supported so they appear as invisible // Handles messages that aren't supported so they appear as invisible
class DummyChatItemPresenter: ChatItemPresenterProtocol { class DummyChatItemPresenter: ChatItemPresenterProtocol {
class func registerCells(collectionView: UICollectionView) { class func registerCells(_ collectionView: UICollectionView) {
collectionView.registerClass(DummyCollectionViewCell.self, forCellWithReuseIdentifier: "cell-id-unhandled-message") collectionView.register(DummyCollectionViewCell.self, forCellWithReuseIdentifier: "cell-id-unhandled-message")
} }
var canCalculateHeightInBackground: Bool { var canCalculateHeightInBackground: Bool {
@ -39,14 +39,13 @@ class DummyChatItemPresenter: ChatItemPresenterProtocol {
return 0 return 0
} }
func dequeueCell(collectionView collectionView: UICollectionView, indexPath: NSIndexPath) -> UICollectionViewCell { func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCellWithReuseIdentifier("cell-id-unhandled-message", forIndexPath: indexPath) return collectionView.dequeueReusableCell(withReuseIdentifier: "cell-id-unhandled-message", for: indexPath)
} }
func configureCell(cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) { func configureCell(_ cell: UICollectionViewCell, decorationAttributes: ChatItemDecorationAttributesProtocol?) {
cell.hidden = true cell.isHidden = true
} }
} }
class DummyCollectionViewCell: UICollectionViewCell {} class DummyCollectionViewCell: UICollectionViewCell {}

View File

@ -26,15 +26,15 @@ import Foundation
extension BaseChatViewController: ChatDataSourceDelegateProtocol { extension BaseChatViewController: ChatDataSourceDelegateProtocol {
public func chatDataSourceDidUpdate(chatDataSource: ChatDataSourceProtocol, updateType: UpdateType) { public func chatDataSourceDidUpdate(_ chatDataSource: ChatDataSourceProtocol, updateType: UpdateType) {
self.enqueueModelUpdate(updateType: updateType) self.enqueueModelUpdate(updateType: updateType)
} }
public func chatDataSourceDidUpdate(chatDataSource: ChatDataSourceProtocol) { public func chatDataSourceDidUpdate(_ chatDataSource: ChatDataSourceProtocol) {
self.enqueueModelUpdate(updateType: .Normal) self.enqueueModelUpdate(updateType: .normal)
} }
public func enqueueModelUpdate(updateType updateType: UpdateType) { public func enqueueModelUpdate(updateType: UpdateType) {
let newItems = self.chatDataSource?.chatItems ?? [] let newItems = self.chatDataSource?.chatItems ?? []
if self.updatesConfig.coalesceUpdates { if self.updatesConfig.coalesceUpdates {
@ -56,7 +56,7 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
} }
public func enqueueMessageCountReductionIfNeeded() { 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 self.updateQueue.addTask { [weak self] (completion) -> () in
guard let sSelf = self else { return } guard let sSelf = self else { return }
sSelf.chatDataSource?.adjustNumberOfMessages(preferredMaxCount: sSelf.constants.preferredMaxMessageCountAdjustment, focusPosition: sSelf.focusPosition, completion: { (didAdjust) -> Void in 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 newItems = sSelf.chatDataSource?.chatItems ?? []
let oldItems = sSelf.chatItemCompanionCollection 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) return min(max(0, Double(midContentOffset / contentHeight)), 1.0)
} }
func updateVisibleCells(changes: CollectionChanges) { func updateVisibleCells(_ changes: CollectionChanges) {
// Datasource should be already updated! // Datasource should be already updated!
assert(self.visibleCellsAreValid(changes: changes), "Invalid visible cells. Don't call me") assert(self.visibleCellsAreValid(changes: changes), "Invalid visible cells. Don't call me")
@ -104,17 +104,17 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
} }
} }
private func visibleCellsFromCollectionViewApi() -> [NSIndexPath: UICollectionViewCell] { private func visibleCellsFromCollectionViewApi() -> [IndexPath: UICollectionViewCell] {
var visibleCells: [NSIndexPath: UICollectionViewCell] = [:] var visibleCells: [IndexPath: UICollectionViewCell] = [:]
self.collectionView.indexPathsForVisibleItems().forEach({ (indexPath) in self.collectionView.indexPathsForVisibleItems.forEach({ (indexPath) in
if let cell = self.collectionView.cellForItemAtIndexPath(indexPath) { if let cell = self.collectionView.cellForItem(at: indexPath) {
visibleCells[indexPath] = cell visibleCells[indexPath] = cell
} }
}) })
return visibleCells 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 // 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. // if self.updatesConfig.fastUpdates is enabled, very fast updates could result in `updateVisibleCells` updating wrong cells.
// See more: https://github.com/diegosanchezr/UICollectionViewStressing // See more: https://github.com/diegosanchezr/UICollectionViewStressing
@ -128,18 +128,19 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
private enum ScrollAction { private enum ScrollAction {
case scrollToBottom 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, changes: CollectionChanges,
updateType: UpdateType, updateType: UpdateType,
completion: () -> Void) { completion: @escaping () -> Void) {
let usesBatchUpdates: Bool let usesBatchUpdates: Bool
let animateBatchUpdates: Bool
do { // Recover from too fast updates... do { // Recover from too fast updates...
let visibleCellsAreValid = self.visibleCellsAreValid(changes: changes) 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 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 // 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 // ... 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 let mustDoReloadData = !visibleCellsAreValid // Only way to recover from this inconsistent state
usesBatchUpdates = !wantsReloadData && !mustDoReloadData usesBatchUpdates = !wantsReloadData && !mustDoReloadData
animateBatchUpdates = ![UpdateType.pagination, UpdateType.firstLoad].contains(updateType)
} }
let scrollAction: ScrollAction let scrollAction: ScrollAction
do { // Scroll action do { // Scroll action
if updateType != .Pagination && self.isScrolledAtBottom() { if updateType != .pagination && self.isScrolledAtBottom() {
scrollAction = .scrollToBottom scrollAction = .scrollToBottom
} else { } else {
let (oldReferenceIndexPath, newReferenceIndexPath) = self.referenceIndexPathsToRestoreScrollPositionOnUpdate(itemsBeforeUpdate: self.chatItemCompanionCollection, changes: changes) let (oldReferenceIndexPath, newReferenceIndexPath) = self.referenceIndexPathsToRestoreScrollPositionOnUpdate(itemsBeforeUpdate: self.chatItemCompanionCollection, changes: changes)
@ -178,7 +180,7 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
if myCompletionExecuted { return } if myCompletionExecuted { return }
myCompletionExecuted = true 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 // Reduces inconsistencies before next update: https://github.com/diegosanchezr/UICollectionViewStressing
completion() completion()
}) })
@ -186,36 +188,46 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
} }
if usesBatchUpdates { if usesBatchUpdates {
UIView.animateWithDuration(self.constants.updatesAnimationDuration, animations: { () -> Void in let batchUpdates = {
self.unfinishedBatchUpdatesCount += 1 self.unfinishedBatchUpdatesCount += 1
self.collectionView.performBatchUpdates({ () -> Void in self.collectionView.performBatchUpdates({ () -> Void in
updateModelClosure() updateModelClosure()
self.updateVisibleCells(changes) // For instace, to support removal of tails self.updateVisibleCells(changes) // For instace, to support removal of tails
self.collectionView.deleteItemsAtIndexPaths(Array(changes.deletedIndexPaths)) self.collectionView.deleteItems(at: Array(changes.deletedIndexPaths))
self.collectionView.insertItemsAtIndexPaths(Array(changes.insertedIndexPaths)) self.collectionView.insertItems(at: Array(changes.insertedIndexPaths))
for move in changes.movedIndexPaths { 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 }) { [weak self] (finished) -> Void in
defer { myCompletion() } defer { myCompletion() }
guard let sSelf = self else { return } guard let sSelf = self else { return }
sSelf.unfinishedBatchUpdatesCount -= 1 sSelf.unfinishedBatchUpdatesCount -= 1
if sSelf.unfinishedBatchUpdatesCount == 0, let onAllBatchUpdatesFinished = self?.onAllBatchUpdatesFinished { 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 { } else {
self.visibleCells = [:] self.visibleCells = [:]
updateModelClosure() updateModelClosure()
self.collectionView.reloadData() self.collectionView.reloadData()
self.collectionView.collectionViewLayout.prepareLayout() self.collectionView.collectionViewLayout.prepare()
} }
switch scrollAction { switch scrollAction {
case .scrollToBottom: case .scrollToBottom:
self.scrollToBottom(animated: updateType == .Normal) self.scrollToBottom(animated: updateType == .normal)
case .preservePosition(rectForReferenceIndexPathBeforeUpdate: let oldRect, referenceIndexPathAfterUpdate: let indexPath): case .preservePosition(rectForReferenceIndexPathBeforeUpdate: let oldRect, referenceIndexPathAfterUpdate: let indexPath):
let newRect = self.rectAtIndexPath(indexPath) let newRect = self.rectAtIndexPath(indexPath)
self.scrollToPreservePosition(oldRefRect: oldRect, newRefRect: newRect) 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 collectionViewWidth = self.collectionView.bounds.width
let updateType = self.isFirstLayout ? .FirstLoad : updateType let updateType = self.isFirstLayout ? .firstLoad : updateType
let performInBackground = updateType != .FirstLoad let performInBackground = updateType != .firstLoad
self.autoLoadingEnabled = false 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( self?.performBatchUpdates(
updateModelClosure: modelUpdate.updateModelClosure, updateModelClosure: updateModelClosure,
changes: modelUpdate.changes, changes: changes,
updateType: updateType, updateType: updateType,
completion: { () -> Void in completion: { () -> Void in
self?.autoLoadingEnabled = true self?.autoLoadingEnabled = true
@ -251,19 +263,19 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
} }
if performInBackground { 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() let modelUpdate = createModelUpdate()
dispatch_async(dispatch_get_main_queue(), { () -> Void in DispatchQueue.main.async(execute: { () -> Void in
perfomBatchUpdates(changes: modelUpdate.changes, updateModelClosure: modelUpdate.updateModelClosure) perfomBatchUpdates(modelUpdate.changes, modelUpdate.updateModelClosure)
}) })
} }
} else { } else {
let modelUpdate = createModelUpdate() 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 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 changes = Chatto.generateChanges(oldCollection: oldItems.lazy.map { $0 }, newCollection: newDecoratedItems.lazy.map { $0 })
let itemCompanionCollection = self.createCompanionCollection(fromChatItems: newDecoratedItems, previousCompanionCollection: oldItems) 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 // 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. // 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 presenter = oldChatItemCompanion.presenter
} else { } else {
presenter = self.createPresenterForChatItem(decoratedChatItem.chatItem) 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 IntermediateItemLayoutData = (height: CGFloat?, bottomMargin: CGFloat)
typealias ItemLayoutData = (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 let layoutData = intermediateLayoutData.map { (intermediateLayoutData: IntermediateItemLayoutData) -> ItemLayoutData in
return (height: intermediateLayoutData.height!, bottomMargin: intermediateLayoutData.bottomMargin) return (height: intermediateLayoutData.height!, bottomMargin: intermediateLayoutData.bottomMargin)
} }
return ChatCollectionViewLayoutModel.createModel(self.collectionView.bounds.width, itemsLayoutData: layoutData) return ChatCollectionViewLayoutModel.createModel(self.collectionView.bounds.width, itemsLayoutData: layoutData)
} }
let isInbackground = !NSThread.isMainThread() let isInbackground = !Thread.isMainThread
var intermediateLayoutData = [IntermediateItemLayoutData]() var intermediateLayoutData = [IntermediateItemLayoutData]()
var itemsForMainThread = [(index: Int, itemCompanion: ChatItemCompanion)]() var itemsForMainThread = [(index: Int, itemCompanion: ChatItemCompanion)]()
for (index, itemCompanion) in items.enumerate() { for (index, itemCompanion) in items.enumerated() {
var height: CGFloat? var height: CGFloat?
let bottomMargin: CGFloat = itemCompanion.decorationAttributes?.bottomMargin ?? 0 let bottomMargin: CGFloat = itemCompanion.decorationAttributes?.bottomMargin ?? 0
if !isInbackground || itemCompanion.presenter.canCalculateHeightInBackground { if !isInbackground || itemCompanion.presenter.canCalculateHeightInBackground {
@ -320,7 +332,7 @@ extension BaseChatViewController: ChatDataSourceDelegateProtocol {
} }
if itemsForMainThread.count > 0 { if itemsForMainThread.count > 0 {
dispatch_sync(dispatch_get_main_queue(), { () -> Void in DispatchQueue.main.sync(execute: { () -> Void in
for (index, itemCompanion) in itemsForMainThread { for (index, itemCompanion) in itemsForMainThread {
let height = itemCompanion.presenter.heightForCell(maximumWidth: collectionViewWidth, decorationAttributes: itemCompanion.decorationAttributes) let height = itemCompanion.presenter.heightForCell(maximumWidth: collectionViewWidth, decorationAttributes: itemCompanion.decorationAttributes)
intermediateLayoutData[index].height = height intermediateLayoutData[index].height = height

View File

@ -26,11 +26,12 @@ import Foundation
extension BaseChatViewController: ChatCollectionViewLayoutDelegate { 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 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 presenter = self.presenterForIndexPath(indexPath)
let cell = presenter.dequeueCell(collectionView: collectionView, indexPath: indexPath) let cell = presenter.dequeueCell(collectionView: collectionView, indexPath: indexPath)
let decorationAttributes = self.decorationAttributesForIndexPath(indexPath) let decorationAttributes = self.decorationAttributesForIndexPath(indexPath)
@ -38,16 +39,17 @@ extension BaseChatViewController: ChatCollectionViewLayoutDelegate {
return cell 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 // 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 // Instead let's use a mapping presenter <--> cell
if let oldPresenterForCell = self.presentersByCell.objectForKey(cell) as? ChatItemPresenterProtocol { if let oldPresenterForCell = self.presentersByCell.object(forKey: cell) as? ChatItemPresenterProtocol {
self.presentersByCell.removeObjectForKey(cell) self.presentersByCell.removeObject(forKey: cell)
oldPresenterForCell.cellWasHidden(cell) oldPresenterForCell.cellWasHidden(cell)
} }
if self.updatesConfig.fastUpdates { 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 self.visibleCells[indexPath] = nil
} else { } else {
self.visibleCells.forEach({ (indexPath, storedCell) in 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. // Here indexPath should always referer to updated data source.
let presenter = self.presenterForIndexPath(indexPath) let presenter = self.presenterForIndexPath(indexPath)
@ -80,23 +83,29 @@ extension BaseChatViewController: ChatCollectionViewLayoutDelegate {
} }
} }
public func collectionView(collectionView: UICollectionView, shouldShowMenuForItemAtIndexPath indexPath: NSIndexPath) -> Bool { @objc(collectionView:shouldShowMenuForItemAtIndexPath:)
return self.presenterForIndexPath(indexPath).shouldShowMenu() ?? false 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 { @objc(collectionView:canPerformAction:forItemAtIndexPath:withSender:)
return self.presenterForIndexPath(indexPath).canPerformMenuControllerAction(action) ?? false 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) self.presenterForIndexPath(indexPath).performMenuControllerAction(action)
} }
func presenterForIndexPath(indexPath: NSIndexPath) -> ChatItemPresenterProtocol { func presenterForIndexPath(_ indexPath: IndexPath) -> ChatItemPresenterProtocol {
return self.presenterForIndex(indexPath.item, chatItemCompanionCollection: self.chatItemCompanionCollection) 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 { guard index < items.count else {
// This can happen from didEndDisplayingCell if we reloaded with less messages // This can happen from didEndDisplayingCell if we reloaded with less messages
return DummyChatItemPresenter() return DummyChatItemPresenter()
@ -104,11 +113,11 @@ extension BaseChatViewController: ChatCollectionViewLayoutDelegate {
return items[index].presenter return items[index].presenter
} }
public func createPresenterForChatItem(chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol { public func createPresenterForChatItem(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol {
return self.presenterFactory.createChatItemPresenter(chatItem) return self.presenterFactory.createChatItemPresenter(chatItem)
} }
public func decorationAttributesForIndexPath(indexPath: NSIndexPath) -> ChatItemDecorationAttributesProtocol? { public func decorationAttributesForIndexPath(_ indexPath: IndexPath) -> ChatItemDecorationAttributesProtocol? {
return self.chatItemCompanionCollection[indexPath.item].decorationAttributes return self.chatItemCompanionCollection[indexPath.item].decorationAttributes
} }
} }

View File

@ -25,8 +25,8 @@
import Foundation import Foundation
public enum CellVerticalEdge { public enum CellVerticalEdge {
case Top case top
case Bottom case bottom
} }
extension CGFloat { extension CGFloat {
@ -36,17 +36,17 @@ extension CGFloat {
extension BaseChatViewController { extension BaseChatViewController {
public func isScrolledAtBottom() -> Bool { public func isScrolledAtBottom() -> Bool {
guard self.collectionView.numberOfSections() > 0 && self.collectionView.numberOfItemsInSection(0) > 0 else { return true } guard self.collectionView.numberOfSections > 0 && self.collectionView.numberOfItems(inSection: 0) > 0 else { return true }
let sectionIndex = self.collectionView.numberOfSections() - 1 let sectionIndex = self.collectionView.numberOfSections - 1
let itemIndex = self.collectionView.numberOfItemsInSection(sectionIndex) - 1 let itemIndex = self.collectionView.numberOfItems(inSection: sectionIndex) - 1
let lastIndexPath = NSIndexPath(forItem: itemIndex, inSection: sectionIndex) let lastIndexPath = IndexPath(item: itemIndex, section: sectionIndex)
return self.isIndexPathVisible(lastIndexPath, atEdge: .Bottom) return self.isIndexPathVisible(lastIndexPath, atEdge: .bottom)
} }
public func isScrolledAtTop() -> Bool { public func isScrolledAtTop() -> Bool {
guard self.collectionView.numberOfSections() > 0 && self.collectionView.numberOfItemsInSection(0) > 0 else { return true } guard self.collectionView.numberOfSections > 0 && self.collectionView.numberOfItems(inSection: 0) > 0 else { return true }
let firstIndexPath = NSIndexPath(forItem: 0, inSection: 0) let firstIndexPath = IndexPath(item: 0, section: 0)
return self.isIndexPathVisible(firstIndexPath, atEdge: .Top) return self.isIndexPathVisible(firstIndexPath, atEdge: .top)
} }
public func isCloseToBottom() -> Bool { public func isCloseToBottom() -> Bool {
@ -59,14 +59,14 @@ extension BaseChatViewController {
return (self.visibleRect().minY / self.collectionView.contentSize.height) < self.constants.autoloadingFractionalThreshold return (self.visibleRect().minY / self.collectionView.contentSize.height) < self.constants.autoloadingFractionalThreshold
} }
public func isIndexPathVisible(indexPath: NSIndexPath, atEdge edge: CellVerticalEdge) -> Bool { public func isIndexPathVisible(_ indexPath: IndexPath, atEdge edge: CellVerticalEdge) -> Bool {
if let attributes = self.collectionView.collectionViewLayout.layoutAttributesForItemAtIndexPath(indexPath) { if let attributes = self.collectionView.collectionViewLayout.layoutAttributesForItem(at: indexPath) {
let visibleRect = self.visibleRect() let visibleRect = self.visibleRect()
let intersection = visibleRect.intersect(attributes.frame) let intersection = visibleRect.intersection(attributes.frame)
if edge == .Top { if edge == .top {
return CGFloat.abs(intersection.minY - attributes.frame.minY) < CGFloat.bma_epsilon return abs(intersection.minY - attributes.frame.minY) < CGFloat.bma_epsilon
} else { } else {
return CGFloat.abs(intersection.maxY - attributes.frame.maxY) < CGFloat.bma_epsilon return abs(intersection.maxY - attributes.frame.maxY) < CGFloat.bma_epsilon
} }
} }
return false return false
@ -75,22 +75,22 @@ extension BaseChatViewController {
public func visibleRect() -> CGRect { public func visibleRect() -> CGRect {
let contentInset = self.collectionView.contentInset let contentInset = self.collectionView.contentInset
let collectionViewBounds = self.collectionView.bounds 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)) 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 // Cancel current scrolling
self.collectionView.setContentOffset(self.collectionView.contentOffset, animated: false) 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 // 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 // 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 // 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 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 { 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) self.collectionView.contentOffset = CGPoint(x: 0, y: offsetY)
}) })
} else { } else {
@ -98,21 +98,21 @@ extension BaseChatViewController {
} }
} }
public func scrollToPreservePosition(oldRefRect oldRefRect: CGRect?, newRefRect: CGRect?) { public func scrollToPreservePosition(oldRefRect: CGRect?, newRefRect: CGRect?) {
guard let oldRefRect = oldRefRect, newRefRect = newRefRect else { guard let oldRefRect = oldRefRect, let newRefRect = newRefRect else {
return return
} }
let diffY = newRefRect.minY - oldRefRect.minY let diffY = newRefRect.minY - oldRefRect.minY
self.collectionView.contentOffset = CGPoint(x: 0, y: self.collectionView.contentOffset.y + diffY) self.collectionView.contentOffset = CGPoint(x: 0, y: self.collectionView.contentOffset.y + diffY)
} }
public func scrollViewDidScroll(scrollView: UIScrollView) { public func scrollViewDidScroll(_ scrollView: UIScrollView) {
if self.collectionView.dragging { if self.collectionView.isDragging {
self.autoLoadMoreContentIfNeeded() self.autoLoadMoreContentIfNeeded()
} }
} }
public func scrollViewDidScrollToTop(scrollView: UIScrollView) { public func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
self.autoLoadMoreContentIfNeeded() self.autoLoadMoreContentIfNeeded()
} }

View File

@ -24,14 +24,14 @@
import UIKit import UIKit
public class BaseChatViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate { open class BaseChatViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
public typealias ChatItemCompanionCollection = ReadOnlyOrderedDictionary<ChatItemCompanion> public typealias ChatItemCompanionCollection = ReadOnlyOrderedDictionary<ChatItemCompanion>
public struct Constants { 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 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 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 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] public var autoloadingFractionalThreshold: CGFloat = 0.05 // in [0, 1]
@ -54,12 +54,12 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
return _chatDataSource return _chatDataSource
} }
set { 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) // 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 = dataSource
self._chatDataSource?.delegate = self self._chatDataSource?.delegate = self
if let updateType = updateType { if let updateType = updateType {
@ -72,12 +72,12 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
self.collectionView?.dataSource = nil 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 = 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() super.viewDidLoad()
self.addCollectionView() self.addCollectionView()
self.addInputViews() self.addInputViews()
@ -91,39 +91,39 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
public var endsEditingWhenTappingOnChatBackground = true public var endsEditingWhenTappingOnChatBackground = true
@objc @objc
public func userDidTapOnCollectionView() { open func userDidTapOnCollectionView() {
if self.endsEditingWhenTappingOnChatBackground { if self.endsEditingWhenTappingOnChatBackground {
self.view.endEditing(true) self.view.endEditing(true)
} }
} }
public override func viewWillAppear(animated: Bool) { open override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
self.keyboardTracker.startTracking() self.keyboardTracker.startTracking()
} }
public override func viewWillDisappear(animated: Bool) { open override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated) super.viewWillDisappear(animated)
self.keyboardTracker.stopTracking() self.keyboardTracker.stopTracking()
} }
private func addCollectionView() { 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.contentInset = self.constants.defaultContentInsets
self.collectionView.scrollIndicatorInsets = self.constants.defaultScrollIndicatorInsets self.collectionView.scrollIndicatorInsets = self.constants.defaultScrollIndicatorInsets
self.collectionView.alwaysBounceVertical = true self.collectionView.alwaysBounceVertical = true
self.collectionView.backgroundColor = UIColor.clearColor() self.collectionView.backgroundColor = UIColor.clear
self.collectionView.keyboardDismissMode = .Interactive self.collectionView.keyboardDismissMode = .interactive
self.collectionView.showsVerticalScrollIndicator = true self.collectionView.showsVerticalScrollIndicator = true
self.collectionView.showsHorizontalScrollIndicator = false self.collectionView.showsHorizontalScrollIndicator = false
self.collectionView.allowsSelection = false self.collectionView.allowsSelection = false
self.collectionView.translatesAutoresizingMaskIntoConstraints = false self.collectionView.translatesAutoresizingMaskIntoConstraints = false
self.collectionView.autoresizingMask = .None self.collectionView.autoresizingMask = UIViewAutoresizing()
self.view.addSubview(self.collectionView) 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: .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: .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: .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: .trailing, relatedBy: .equal, toItem: self.collectionView, attribute: .trailing, multiplier: 1, constant: 0))
self.collectionView.dataSource = self self.collectionView.dataSource = self
self.collectionView.delegate = self self.collectionView.delegate = self
self.accessoryViewRevealer = AccessoryViewRevealer(collectionView: self.collectionView) self.accessoryViewRevealer = AccessoryViewRevealer(collectionView: self.collectionView)
@ -140,25 +140,25 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
private var inputContainerBottomConstraint: NSLayoutConstraint! private var inputContainerBottomConstraint: NSLayoutConstraint!
private func addInputViews() { private func addInputViews() {
self.inputContainer = UIView(frame: CGRect.zero) self.inputContainer = UIView(frame: CGRect.zero)
self.inputContainer.autoresizingMask = .None self.inputContainer.autoresizingMask = UIViewAutoresizing()
self.inputContainer.translatesAutoresizingMaskIntoConstraints = false self.inputContainer.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(self.inputContainer) 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.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: .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.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.inputContainerBottomConstraint = NSLayoutConstraint(item: self.view, attribute: .bottom, relatedBy: .equal, toItem: self.inputContainer, attribute: .bottom, multiplier: 1, constant: 0)
self.view.addConstraint(self.inputContainerBottomConstraint) self.view.addConstraint(self.inputContainerBottomConstraint)
let inputView = self.createChatInputView() let inputView = self.createChatInputView()
self.inputContainer.addSubview(inputView) 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: .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: .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: .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: .trailing, relatedBy: .equal, toItem: inputView, attribute: .trailing, multiplier: 1, constant: 0))
} }
var isAdjustingInputContainer: Bool = false var isAdjustingInputContainer: Bool = false
public func setupKeyboardTracker() { open func setupKeyboardTracker() {
let layoutBlock = { [weak self] (bottomMargin: CGFloat) in let layoutBlock = { [weak self] (bottomMargin: CGFloat) in
guard let sSelf = self else { return } guard let sSelf = self else { return }
sSelf.isAdjustingInputContainer = true sSelf.isAdjustingInputContainer = true
@ -170,11 +170,11 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
(self.view as? BaseChatViewControllerView)?.bmaInputAccessoryView = self.keyboardTracker?.trackingView (self.view as? BaseChatViewControllerView)?.bmaInputAccessoryView = self.keyboardTracker?.trackingView
} }
var notificationCenter = NSNotificationCenter.defaultCenter() var notificationCenter = NotificationCenter.default
var keyboardTracker: KeyboardTracker! var keyboardTracker: KeyboardTracker!
public var isFirstLayout: Bool = true public private(set) var isFirstLayout: Bool = true
override public func viewDidLayoutSubviews() { override open func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews() super.viewDidLayoutSubviews()
self.adjustCollectionViewInsets() self.adjustCollectionViewInsets()
@ -183,12 +183,19 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
if self.isFirstLayout { if self.isFirstLayout {
self.updateQueue.start() self.updateQueue.start()
self.isFirstLayout = false self.isFirstLayout = false
// If we have been pushed on nav controller and hidesBottomBarWhenPushed = true, then ignore bottomLayoutMargin
// because it has incorrect value when we actually have a bottom bar (tabbar)
if self.hidesBottomBarWhenPushed && (navigationController?.viewControllers.count ?? 0) > 1 && navigationController?.viewControllers.last == self {
self.inputContainerBottomConstraint.constant = 0
} else {
self.inputContainerBottomConstraint.constant = self.bottomLayoutGuide.length self.inputContainerBottomConstraint.constant = self.bottomLayoutGuide.length
} }
} }
}
private func adjustCollectionViewInsets() { 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 let isBouncingAtTop = isInteracting && self.collectionView.contentOffset.y < -self.collectionView.contentInset.top
if isBouncingAtTop { return } if isBouncingAtTop { return }
@ -197,7 +204,7 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
let insetBottomDiff = newInsetBottom - self.collectionView.contentInset.bottom let insetBottomDiff = newInsetBottom - self.collectionView.contentInset.bottom
let newInsetTop = self.topLayoutGuide.length + self.constants.defaultContentInsets.top 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 allContentFits: Bool = {
let availableHeight = self.collectionView.bounds.height - (newInsetTop + newInsetBottom) let availableHeight = self.collectionView.bounds.height - (newInsetTop + newInsetBottom)
return availableHeight >= contentSize.height return availableHeight >= contentSize.height
@ -233,9 +240,9 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
} }
} }
func rectAtIndexPath(indexPath: NSIndexPath?) -> CGRect? { func rectAtIndexPath(_ indexPath: IndexPath?) -> CGRect? {
if let indexPath = indexPath { if let indexPath = indexPath {
return self.collectionView.collectionViewLayout.layoutAttributesForItemAtIndexPath(indexPath)?.frame return self.collectionView.collectionViewLayout.layoutAttributesForItem(at: indexPath)?.frame
} }
return nil return nil
} }
@ -244,8 +251,8 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
var accessoryViewRevealer: AccessoryViewRevealer! var accessoryViewRevealer: AccessoryViewRevealer!
public private(set) var inputContainer: UIView! public private(set) var inputContainer: UIView!
var presenterFactory: ChatItemPresenterFactoryProtocol! var presenterFactory: ChatItemPresenterFactoryProtocol!
let presentersByCell = NSMapTable(keyOptions: .WeakMemory, valueOptions: .WeakMemory) let presentersByCell = NSMapTable<UICollectionViewCell, AnyObject>(keyOptions: .weakMemory, valueOptions: .weakMemory)
var visibleCells: [NSIndexPath: UICollectionViewCell] = [:] // @see visibleCellsAreValid(changes:) var visibleCells: [IndexPath: UICollectionViewCell] = [:] // @see visibleCellsAreValid(changes:)
public internal(set) var updateQueue: SerialTaskQueueProtocol = SerialTaskQueue() public internal(set) var updateQueue: SerialTaskQueueProtocol = SerialTaskQueue()
@ -257,7 +264,7 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
*/ */
public var chatItemsDecorator: ChatItemsDecoratorProtocol? public var chatItemsDecorator: ChatItemsDecoratorProtocol?
public var createCollectionViewLayout: UICollectionViewLayout { open func createCollectionViewLayout() -> UICollectionViewLayout {
let layout = ChatCollectionViewLayout() let layout = ChatCollectionViewLayout()
layout.delegate = self layout.delegate = self
return layout return layout
@ -267,17 +274,17 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
// MARK: Subclass overrides // MARK: Subclass overrides
public func createPresenterFactory() -> ChatItemPresenterFactoryProtocol { open func createPresenterFactory() -> ChatItemPresenterFactoryProtocol {
// Default implementation // Default implementation
return ChatItemPresenterFactory(presenterBuildersByType: self.createPresenterBuilders()) return ChatItemPresenterFactory(presenterBuildersByType: self.createPresenterBuilders())
} }
public func createPresenterBuilders() -> [ChatItemType: [ChatItemPresenterBuilderProtocol]] { open func createPresenterBuilders() -> [ChatItemType: [ChatItemPresenterBuilderProtocol]] {
assert(false, "Override in subclass") assert(false, "Override in subclass")
return [ChatItemType: [ChatItemPresenterBuilderProtocol]]() return [ChatItemType: [ChatItemPresenterBuilderProtocol]]()
} }
public func createChatInputView() -> UIView { open func createChatInputView() -> UIView {
assert(false, "Override in subclass") assert(false, "Override in subclass")
return UIView() return UIView()
} }
@ -286,20 +293,20 @@ public class BaseChatViewController: UIViewController, UICollectionViewDataSourc
When paginating up we need to change the scroll position as the content is pushed down. 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 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 let firstItemMoved = changes.movedIndexPaths.first
return (firstItemMoved?.indexPathOld, firstItemMoved?.indexPathNew) return (firstItemMoved?.indexPathOld as IndexPath?, firstItemMoved?.indexPathNew as IndexPath?)
} }
} }
extension BaseChatViewController { // Rotation extension BaseChatViewController { // Rotation
public override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) { open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator) super.viewWillTransition(to: size, with: coordinator)
let shouldScrollToBottom = self.isScrolledAtBottom() let shouldScrollToBottom = self.isScrolledAtBottom()
let referenceIndexPath = self.collectionView.indexPathsForVisibleItems().first let referenceIndexPath = self.collectionView.indexPathsForVisibleItems.first
let oldRect = self.rectAtIndexPath(referenceIndexPath) let oldRect = self.rectAtIndexPath(referenceIndexPath)
coordinator.animateAlongsideTransition({ (context) -> Void in coordinator.animate(alongsideTransition: { (context) -> Void in
if shouldScrollToBottom { if shouldScrollToBottom {
self.scrollToBottom(animated: false) self.scrollToBottom(animated: false)
} else { } else {

View File

@ -32,8 +32,8 @@ public protocol AccessoryViewRevealable {
public struct AccessoryViewRevealerConfig { public struct AccessoryViewRevealerConfig {
public let angleThresholdInRads: CGFloat public let angleThresholdInRads: CGFloat
public let translationTransform: (rawTranslation: CGFloat) -> CGFloat public let translationTransform: (_ rawTranslation: CGFloat) -> CGFloat
public init(angleThresholdInRads: CGFloat, translationTransform: (rawTranslation: CGFloat) -> CGFloat) { public init(angleThresholdInRads: CGFloat, translationTransform: @escaping (_ rawTranslation: CGFloat) -> CGFloat) {
self.angleThresholdInRads = angleThresholdInRads self.angleThresholdInRads = angleThresholdInRads
self.translationTransform = translationTransform self.translationTransform = translationTransform
} }
@ -68,51 +68,51 @@ class AccessoryViewRevealer: NSObject, UIGestureRecognizerDelegate {
var isEnabled: Bool = true { var isEnabled: Bool = true {
didSet { didSet {
self.panRecognizer.enabled = self.isEnabled self.panRecognizer.isEnabled = self.isEnabled
} }
} }
var config = AccessoryViewRevealerConfig.defaultConfig() var config = AccessoryViewRevealerConfig.defaultConfig()
@objc @objc
private func handlePan(panRecognizer: UIPanGestureRecognizer) { private func handlePan(_ panRecognizer: UIPanGestureRecognizer) {
switch panRecognizer.state { switch panRecognizer.state {
case .Began: case .began:
break break
case .Changed: case .changed:
let translation = panRecognizer.translationInView(self.collectionView) let translation = panRecognizer.translation(in: self.collectionView)
self.revealAccessoryView(atOffset: self.config.translationTransform(rawTranslation: -translation.x)) self.revealAccessoryView(atOffset: self.config.translationTransform(-translation.x))
case .Ended, .Cancelled, .Failed: case .ended, .cancelled, .failed:
self.revealAccessoryView(atOffset: 0) self.revealAccessoryView(atOffset: 0)
default: default:
break break
} }
} }
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true return true
} }
func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool { func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer != self.panRecognizer { if gestureRecognizer != self.panRecognizer {
return true return true
} }
let translation = self.panRecognizer.translationInView(self.collectionView) let translation = self.panRecognizer.translation(in: self.collectionView)
let x = CGFloat.abs(translation.x), y = CGFloat.abs(translation.y) let x = abs(translation.x), y = abs(translation.y)
let angleRads = atan2(y, x) let angleRads = atan2(y, x)
return angleRads <= self.config.angleThresholdInRads return angleRads <= self.config.angleThresholdInRads
} }
private func revealAccessoryView(atOffset offset: CGFloat) { private func revealAccessoryView(atOffset offset: CGFloat) {
// Find max offset (cells can have slighlty different timestamp size ( 3.00 am vs 11.37 pm ) // 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 let offset = min(offset, cells.reduce(0) { (current, cell) -> CGFloat in
return max(current, cell.preferredOffsetToRevealAccessoryView() ?? 0) return max(current, cell.preferredOffsetToRevealAccessoryView() ?? 0)
}) })
for cell in self.collectionView.visibleCells() { for cell in self.collectionView.visibleCells {
if let cell = cell as? AccessoryViewRevealable where cell.allowAccessoryViewRevealing { if let cell = cell as? AccessoryViewRevealable, cell.allowAccessoryViewRevealing {
cell.revealAccessoryView(withOffset: offset, animated: offset == 0) cell.revealAccessoryView(withOffset: offset, animated: offset == 0)
} }
} }

View File

@ -34,18 +34,18 @@ public struct ChatCollectionViewLayoutModel {
let layoutAttributesBySectionAndItem: [[UICollectionViewLayoutAttributes]] let layoutAttributesBySectionAndItem: [[UICollectionViewLayoutAttributes]]
let calculatedForWidth: CGFloat 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 layoutAttributes = [UICollectionViewLayoutAttributes]()
var layoutAttributesBySectionAndItem = [[UICollectionViewLayoutAttributes]]() var layoutAttributesBySectionAndItem = [[UICollectionViewLayoutAttributes]]()
layoutAttributesBySectionAndItem.append([UICollectionViewLayoutAttributes]()) layoutAttributesBySectionAndItem.append([UICollectionViewLayoutAttributes]())
var verticalOffset: CGFloat = 0 var verticalOffset: CGFloat = 0
for (index, layoutData) in itemsLayoutData.enumerate() { for (index, layoutData) in itemsLayoutData.enumerated() {
let indexPath = NSIndexPath(forItem: index, inSection: 0) let indexPath = IndexPath(item: index, section: 0)
let (height, bottomMargin) = layoutData let (height, bottomMargin) = layoutData
let itemSize = CGSize(width: collectionViewWidth, height: height) let itemSize = CGSize(width: collectionViewWidth, height: height)
let frame = CGRect(origin: CGPoint(x: 0, y: verticalOffset), size: itemSize) let frame = CGRect(origin: CGPoint(x: 0, y: verticalOffset), size: itemSize)
let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath) let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = frame attributes.frame = frame
layoutAttributes.append(attributes) layoutAttributes.append(attributes)
layoutAttributesBySectionAndItem[0].append(attributes) layoutAttributesBySectionAndItem[0].append(attributes)
@ -62,27 +62,26 @@ public struct ChatCollectionViewLayoutModel {
} }
} }
open class ChatCollectionViewLayout: UICollectionViewLayout {
public class ChatCollectionViewLayout: UICollectionViewLayout {
var layoutModel: ChatCollectionViewLayoutModel! var layoutModel: ChatCollectionViewLayoutModel!
public weak var delegate: ChatCollectionViewLayoutDelegate? public weak var delegate: ChatCollectionViewLayoutDelegate?
// Optimization: after reloadData we'll get invalidateLayout, but prepareLayout will be delayed until next run loop. // 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. // 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 private var layoutNeedsUpdate = true
public override func invalidateLayout() { open override func invalidateLayout() {
super.invalidateLayout() super.invalidateLayout()
self.layoutNeedsUpdate = true self.layoutNeedsUpdate = true
} }
public override func prepareLayout() { open override func prepare() {
super.prepareLayout() super.prepare()
guard self.layoutNeedsUpdate else { return } guard self.layoutNeedsUpdate else { return }
guard let delegate = self.delegate else { return } guard let delegate = self.delegate else { return }
var oldLayoutModel = self.layoutModel var oldLayoutModel = self.layoutModel
self.layoutModel = delegate.chatCollectionViewLayoutModel() self.layoutModel = delegate.chatCollectionViewLayoutModel()
self.layoutNeedsUpdate = false 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 // Dealloc of layout with 5000 items take 25 ms on tests on iPhone 4s
// This moves dealloc out of main thread // This moves dealloc out of main thread
if oldLayoutModel != nil { 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 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) } 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 { if indexPath.section < self.layoutModel.layoutAttributesBySectionAndItem.count && indexPath.item < self.layoutModel.layoutAttributesBySectionAndItem[indexPath.section].count {
return self.layoutModel.layoutAttributesBySectionAndItem[indexPath.section][indexPath.item] return self.layoutModel.layoutAttributesBySectionAndItem[indexPath.section][indexPath.item]
} }
@ -108,7 +107,7 @@ public class ChatCollectionViewLayout: UICollectionViewLayout {
return nil return nil
} }
public override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool { open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return self.layoutModel.calculatedForWidth != newBounds.width return self.layoutModel.calculatedForWidth != newBounds.width
} }
} }

View File

@ -25,16 +25,16 @@
import Foundation import Foundation
public enum UpdateType { public enum UpdateType {
case Normal case normal
case FirstLoad case firstLoad
case Pagination case pagination
case Reload case reload
case MessageCountReduction case messageCountReduction
} }
public protocol ChatDataSourceDelegateProtocol: class { public protocol ChatDataSourceDelegateProtocol: class {
func chatDataSourceDidUpdate(chatDataSource: ChatDataSourceProtocol) func chatDataSourceDidUpdate(_ chatDataSource: ChatDataSourceProtocol)
func chatDataSourceDidUpdate(chatDataSource: ChatDataSourceProtocol, updateType: UpdateType) func chatDataSourceDidUpdate(_ chatDataSource: ChatDataSourceProtocol, updateType: UpdateType)
} }
public protocol ChatDataSourceProtocol: class { public protocol ChatDataSourceProtocol: class {
@ -45,5 +45,5 @@ public protocol ChatDataSourceProtocol: class {
func loadNext() // Should trigger chatDataSourceDidUpdate with UpdateType.Pagination func loadNext() // Should trigger chatDataSourceDidUpdate with UpdateType.Pagination
func loadPrevious() // 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 import Foundation
public protocol ChatItemPresenterFactoryProtocol { public protocol ChatItemPresenterFactoryProtocol {
func createChatItemPresenter(chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol func createChatItemPresenter(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol
func configure(withCollectionView collectionView: UICollectionView) func configure(withCollectionView collectionView: UICollectionView)
} }
@ -36,7 +36,7 @@ final class ChatItemPresenterFactory: ChatItemPresenterFactoryProtocol {
self.presenterBuildersByType = presenterBuildersByType self.presenterBuildersByType = presenterBuildersByType
} }
func createChatItemPresenter(chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol { func createChatItemPresenter(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol {
for builder in self.presenterBuildersByType[chatItem.type] ?? [] { for builder in self.presenterBuildersByType[chatItem.type] ?? [] {
if builder.canHandleChatItem(chatItem) { if builder.canHandleChatItem(chatItem) {
return builder.createPresenterWithChatItem(chatItem) return builder.createPresenterWithChatItem(chatItem)

View File

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

View File

@ -27,12 +27,12 @@ import Foundation
class KeyboardTracker { class KeyboardTracker {
private enum KeyboardStatus { private enum KeyboardStatus {
case Hidden case hidden
case Showing case showing
case Shown case shown
} }
private var keyboardStatus: KeyboardStatus = .Hidden private var keyboardStatus: KeyboardStatus = .hidden
private let view: UIView private let view: UIView
var trackingView: UIView { var trackingView: UIView {
return self.keyboardTrackerView return self.keyboardTrackerView
@ -50,20 +50,20 @@ class KeyboardTracker {
var isTracking = false var isTracking = false
var inputContainer: UIView 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 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.view = viewController.view
self.layoutBlock = layoutBlock self.layoutBlock = layoutBlock
self.inputContainer = inputContainer self.inputContainer = inputContainer
self.notificationCenter = notificationCenter self.notificationCenter = notificationCenter
self.notificationCenter.addObserver(self, selector: #selector(KeyboardTracker.keyboardWillShow(_:)), name: UIKeyboardWillShowNotification, 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: UIKeyboardDidShowNotification, 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: UIKeyboardWillHideNotification, 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: UIKeyboardWillChangeFrameNotification, object: nil) self.notificationCenter.addObserver(self, selector: #selector(KeyboardTracker.keyboardWillChangeFrame(_:)), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
} }
deinit { deinit {
@ -79,60 +79,60 @@ class KeyboardTracker {
} }
@objc @objc
private func keyboardWillShow(notification: NSNotification) { private func keyboardWillShow(_ notification: Notification) {
guard self.isTracking else { return } guard self.isTracking else { return }
guard !self.isPerformingForcedLayout else { return } guard !self.isPerformingForcedLayout else { return }
let bottomConstraint = self.bottomConstraintFromNotification(notification) let bottomConstraint = self.bottomConstraintFromNotification(notification)
guard bottomConstraint > 0 else { return } // Some keyboards may report initial willShow/DidShow notifications with invalid positions 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) self.layoutInputContainer(withBottomConstraint: bottomConstraint)
} }
@objc @objc
private func keyboardDidShow(notification: NSNotification) { private func keyboardDidShow(_ notification: Notification) {
guard self.isTracking else { return } guard self.isTracking else { return }
guard !self.isPerformingForcedLayout else { return } guard !self.isPerformingForcedLayout else { return }
let bottomConstraint = self.bottomConstraintFromNotification(notification) let bottomConstraint = self.bottomConstraintFromNotification(notification)
guard bottomConstraint > 0 else { return } // Some keyboards may report initial willShow/DidShow notifications with invalid positions 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.layoutInputContainer(withBottomConstraint: bottomConstraint)
self.adjustTrackingViewSizeIfNeeded() self.adjustTrackingViewSizeIfNeeded()
} }
@objc @objc
private func keyboardWillChangeFrame(notification: NSNotification) { private func keyboardWillChangeFrame(_ notification: Notification) {
guard self.isTracking else { return } guard self.isTracking else { return }
let bottomConstraint = self.bottomConstraintFromNotification(notification) let bottomConstraint = self.bottomConstraintFromNotification(notification)
if bottomConstraint == 0 { if bottomConstraint == 0 {
self.keyboardStatus = .Hidden self.keyboardStatus = .hidden
self.layoutInputAtBottom() self.layoutInputAtBottom()
} }
} }
@objc @objc
private func keyboardWillHide(notification: NSNotification) { private func keyboardWillHide(_ notification: Notification) {
guard self.isTracking else { return } guard self.isTracking else { return }
self.keyboardStatus = .Hidden self.keyboardStatus = .hidden
self.layoutInputAtBottom() self.layoutInputAtBottom()
} }
private func bottomConstraintFromNotification(notification: NSNotification) -> CGFloat { private func bottomConstraintFromNotification(_ notification: Notification) -> CGFloat {
guard let rect = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.CGRectValue() else { return 0 } guard let rect = ((notification as NSNotification).userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { return 0 }
guard rect.height > 0 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 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 { private func bottomConstraintFromTrackingView() -> CGFloat {
guard self.keyboardTrackerView.superview != nil else { return 0 } 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) return max(0, self.view.bounds.height - trackingViewRect.maxY)
} }
func adjustTrackingViewSizeIfNeeded() { func adjustTrackingViewSizeIfNeeded() {
guard self.isTracking && self.keyboardStatus == .Shown else { return } guard self.isTracking && self.keyboardStatus == .shown else { return }
self.adjustTrackingViewSize() self.adjustTrackingViewSize()
} }
@ -153,13 +153,13 @@ class KeyboardTracker {
var isPerformingForcedLayout: Bool = false var isPerformingForcedLayout: Bool = false
func layoutInputAtTrackingViewIfNeeded() { func layoutInputAtTrackingViewIfNeeded() {
guard self.isTracking && self.keyboardStatus == .Shown else { return } guard self.isTracking && self.keyboardStatus == .shown else { return }
self.layoutInputContainer(withBottomConstraint: self.bottomConstraintFromTrackingView()) self.layoutInputContainer(withBottomConstraint: self.bottomConstraintFromTrackingView())
} }
private func layoutInputContainer(withBottomConstraint constraint: CGFloat) { private func layoutInputContainer(withBottomConstraint constraint: CGFloat) {
self.isPerformingForcedLayout = true self.isPerformingForcedLayout = true
self.layoutBlock(bottomMargin: constraint) self.layoutBlock(constraint)
self.isPerformingForcedLayout = false self.isPerformingForcedLayout = false
} }
} }
@ -185,14 +185,14 @@ private class KeyboardTrackingView: UIView {
self.commonInit() self.commonInit()
} }
private func commonInit() { func commonInit() {
self.autoresizingMask = .FlexibleHeight self.autoresizingMask = .flexibleHeight
self.userInteractionEnabled = false self.isUserInteractionEnabled = false
self.backgroundColor = UIColor.clearColor() self.backgroundColor = UIColor.clear
self.hidden = true self.isHidden = true
} }
private var preferredSize: CGSize = .zero { var preferredSize: CGSize = .zero {
didSet { didSet {
if oldValue != self.preferredSize { if oldValue != self.preferredSize {
self.invalidateIntrinsicContentSize() self.invalidateIntrinsicContentSize()
@ -201,30 +201,30 @@ private class KeyboardTrackingView: UIView {
} }
} }
private override func intrinsicContentSize() -> CGSize { override var intrinsicContentSize: CGSize {
return self.preferredSize return self.preferredSize
} }
override func willMoveToSuperview(newSuperview: UIView?) { override func willMove(toSuperview newSuperview: UIView?) {
if let observedView = self.observedView { if let observedView = self.observedView {
observedView.removeObserver(self, forKeyPath: "center") observedView.removeObserver(self, forKeyPath: "center")
self.observedView = nil self.observedView = nil
} }
if let newSuperview = newSuperview { 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 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>) { override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let object = object, superview = self.superview else { return } guard let object = object as? UIView, let superview = self.superview else { return }
if object === superview { if object === superview {
guard let sChange = change else { return } guard let sChange = change else { return }
let oldCenter = (sChange[NSKeyValueChangeOldKey] as! NSValue).CGPointValue() let oldCenter = (sChange[NSKeyValueChangeKey.oldKey] as! NSValue).cgPointValue
let newCenter = (sChange[NSKeyValueChangeNewKey] as! NSValue).CGPointValue() let newCenter = (sChange[NSKeyValueChangeKey.newKey] as! NSValue).cgPointValue
if oldCenter != newCenter { if oldCenter != newCenter {
self.positionChangedCallback?() self.positionChangedCallback?()
} }

View File

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

View File

@ -23,21 +23,21 @@ THE SOFTWARE.
*/ */
import Foundation import Foundation
public struct ReadOnlyOrderedDictionary<T where T: UniqueIdentificable>: CollectionType { public struct ReadOnlyOrderedDictionary<T>: Collection where T: UniqueIdentificable {
private let items: [T] private let items: [T]
private let itemIndexesById: [String: Int] // Maping to the position in the array instead the item itself for better performance private let itemIndexesById: [String: Int] // Maping to the position in the array instead the item itself for better performance
public init(items: [T]) { public init(items: [T]) {
var dictionary = [String: Int](minimumCapacity: items.count) var dictionary = [String: Int](minimumCapacity: items.count)
for (index, item) in items.enumerate() { for (index, item) in items.enumerated() {
dictionary[item.uid] = index dictionary[item.uid] = index
} }
self.items = items self.items = items
self.itemIndexesById = dictionary self.itemIndexesById = dictionary
} }
public func indexOf(uid: String) -> Int? { public func indexOf(_ uid: String) -> Int? {
return self.itemIndexesById[uid] return self.itemIndexesById[uid]
} }
@ -52,8 +52,20 @@ public struct ReadOnlyOrderedDictionary<T where T: UniqueIdentificable>: Collect
return nil return nil
} }
public func generate() -> IndexingGenerator<[T]> { public func makeIterator() -> IndexingIterator<[T]> {
return self.items.generate() 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 { public var startIndex: Int {

View File

@ -24,10 +24,10 @@
import Foundation import Foundation
public typealias TaskClosure = (completion: () -> Void) -> Void public typealias TaskClosure = (_ completion: @escaping () -> Void) -> Void
public protocol SerialTaskQueueProtocol { public protocol SerialTaskQueueProtocol {
func addTask(task: TaskClosure) func addTask(_ task: @escaping TaskClosure)
func start() func start()
func stop() func stop()
func flushQueue() func flushQueue()
@ -43,7 +43,7 @@ public final class SerialTaskQueue: SerialTaskQueueProtocol {
public init() {} public init() {}
public func addTask(task: TaskClosure) { public func addTask(_ task: @escaping TaskClosure) {
self.tasksQueue.append(task) self.tasksQueue.append(task)
self.maybeExecuteNextTask() self.maybeExecuteNextTask()
} }
@ -70,7 +70,7 @@ public final class SerialTaskQueue: SerialTaskQueueProtocol {
if !self.isEmpty { if !self.isEmpty {
let firstTask = self.tasksQueue.removeFirst() let firstTask = self.tasksQueue.removeFirst()
self.isBusy = true self.isBusy = true
firstTask(completion: { [weak self] () -> Void in firstTask({ [weak self] () -> Void in
self?.isBusy = false self?.isBusy = false
self?.maybeExecuteNextTask() self?.maybeExecuteNextTask()
}) })

32
Chatto/Source/Utils.swift Normal file
View File

@ -0,0 +1,32 @@
/*
The MIT License (MIT)
Copyright (c) 2015-present Badoo Trading Limited.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
import Foundation
private let scale = UIScreen.main.scale
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>! var presenter: BaseChatItemPresenter<UICollectionViewCell>!
override func setUp() { override func setUp() {
super.setUp()
self.presenter = BaseChatItemPresenter() self.presenter = BaseChatItemPresenter()
} }

View File

@ -24,7 +24,7 @@ THE SOFTWARE.
@testable import Chatto @testable import Chatto
func createFakeChatItems(count count: Int) -> [ChatItemProtocol] { func createFakeChatItems(count: Int) -> [ChatItemProtocol] {
var items = [ChatItemProtocol]() var items = [ChatItemProtocol]()
for i in 0..<count { for i in 0..<count {
items.append(FakeChatItem(uid: "\(i)", type: "fake-type")) items.append(FakeChatItem(uid: "\(i)", type: "fake-type"))
@ -66,17 +66,17 @@ class FakeDataSource: ChatDataSourceProtocol {
if let chatItemsForLoadNext = self.chatItemsForLoadNext { if let chatItemsForLoadNext = self.chatItemsForLoadNext {
self.chatItems = chatItemsForLoadNext self.chatItems = chatItemsForLoadNext
} }
self.delegate?.chatDataSourceDidUpdate(self, updateType: .Pagination) self.delegate?.chatDataSourceDidUpdate(self, updateType: .pagination)
} }
func loadPrevious() { func loadPrevious() {
self.wasRequestedForPrevious = true 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 self.wasRequestedForMessageCountContention = true
completion(didAdjust: false) completion((didAdjust: false))
} }
} }
@ -84,11 +84,11 @@ class FakeCell: UICollectionViewCell { }
class FakePresenterBuilder: ChatItemPresenterBuilderProtocol { class FakePresenterBuilder: ChatItemPresenterBuilderProtocol {
var presentersCreatedCount: Int = 0 var presentersCreatedCount: Int = 0
func canHandleChatItem(chatItem: ChatItemProtocol) -> Bool { func canHandleChatItem(_ chatItem: ChatItemProtocol) -> Bool {
return chatItem.type == "fake-type" return chatItem.type == "fake-type"
} }
func createPresenterWithChatItem(chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol { func createPresenterWithChatItem(_ chatItem: ChatItemProtocol) -> ChatItemPresenterProtocol {
self.presentersCreatedCount += 1 self.presentersCreatedCount += 1
return FakePresenter() return FakePresenter()
} }
@ -99,21 +99,21 @@ class FakePresenterBuilder: ChatItemPresenterBuilderProtocol {
} }
class FakePresenter: BaseChatItemPresenter<FakeCell> { class FakePresenter: BaseChatItemPresenter<FakeCell> {
override class func registerCells(collectionView: UICollectionView) { override class func registerCells(_ collectionView: UICollectionView) {
collectionView.registerClass(FakeCell.self, forCellWithReuseIdentifier: "fake-cell") collectionView.register(FakeCell.self, forCellWithReuseIdentifier: "fake-cell")
} }
override func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat { override func heightForCell(maximumWidth width: CGFloat, decorationAttributes: ChatItemDecorationAttributesProtocol?) -> CGFloat {
return 10 return 10
} }
override func dequeueCell(collectionView collectionView: UICollectionView, indexPath: NSIndexPath) -> UICollectionViewCell { override func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCellWithReuseIdentifier("fake-cell", forIndexPath: indexPath) 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 let fakeCell = cell as! FakeCell
fakeCell.backgroundColor = UIColor.redColor() fakeCell.backgroundColor = UIColor.red
} }
} }
@ -127,13 +127,14 @@ class FakeChatItem: ChatItemProtocol {
} }
final class SerialTaskQueueTestHelper: SerialTaskQueueProtocol { final class SerialTaskQueueTestHelper: SerialTaskQueueProtocol {
var onAllTasksFinished: (() -> Void)? var onAllTasksFinished: (() -> Void)?
var isBusy = false var isBusy = false
var isStopped = true var isStopped = true
var tasksQueue = [TaskClosure]() var tasksQueue = [TaskClosure]()
func addTask(task: TaskClosure) { func addTask(_ task: @escaping TaskClosure) {
self.tasksQueue.append(task) self.tasksQueue.append(task)
self.maybeExecuteNextTask() self.maybeExecuteNextTask()
} }
@ -160,7 +161,7 @@ final class SerialTaskQueueTestHelper: SerialTaskQueueProtocol {
if !self.isEmpty { if !self.isEmpty {
let firstTask = self.tasksQueue.removeFirst() let firstTask = self.tasksQueue.removeFirst()
self.isBusy = true self.isBusy = true
firstTask(completion: { [weak self] () -> Void in firstTask({ [weak self] () -> Void in
self?.isBusy = false self?.isBusy = false
self?.maybeExecuteNextTask() self?.maybeExecuteNextTask()
}) })

View File

@ -61,7 +61,7 @@ class ChatViewControllerTests: XCTestCase {
} }
func testThat_WhenDataSourceChanges_ThenCollectionViewUpdatesAsynchronously() { func testThat_WhenDataSourceChanges_ThenCollectionViewUpdatesAsynchronously() {
let asyncExpectation = expectationWithDescription("update") let asyncExpectation = expectation(description: "update")
let presenterBuilder = FakePresenterBuilder() let presenterBuilder = FakePresenterBuilder()
let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]]) let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]])
let fakeDataSource = FakeDataSource() let fakeDataSource = FakeDataSource()
@ -74,7 +74,7 @@ class ChatViewControllerTests: XCTestCase {
asyncExpectation.fulfill() asyncExpectation.fulfill()
completion() completion()
} }
self.waitForExpectationsWithTimeout(1) { (error) -> Void in self.waitForExpectations(timeout: 1) { (error) -> Void in
XCTAssertEqual(3, controller.collectionView(controller.collectionView, numberOfItemsInSection: 0)) XCTAssertEqual(3, controller.collectionView(controller.collectionView, numberOfItemsInSection: 0))
} }
} }
@ -91,7 +91,7 @@ class ChatViewControllerTests: XCTestCase {
} }
func testThat_GivenManyItems_WhenScrollToTop_ThenLoadsPreviousPage() { func testThat_GivenManyItems_WhenScrollToTop_ThenLoadsPreviousPage() {
let asyncExpectation = expectationWithDescription("update") let asyncExpectation = expectation(description: "update")
let presenterBuilder = FakePresenterBuilder() let presenterBuilder = FakePresenterBuilder()
let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]]) let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]])
let fakeDataSource = FakeDataSource() let fakeDataSource = FakeDataSource()
@ -105,13 +105,13 @@ class ChatViewControllerTests: XCTestCase {
completion() completion()
asyncExpectation.fulfill() asyncExpectation.fulfill()
} }
self.waitForExpectationsWithTimeout(1) { (error) -> Void in self.waitForExpectations(timeout: 1) { (error) -> Void in
XCTAssertTrue(fakeDataSource.wasRequestedForPrevious) XCTAssertTrue(fakeDataSource.wasRequestedForPrevious)
} }
} }
func testThat_WhenLoadsNextPage_ThenPreservesScrollPosition() { func testThat_WhenLoadsNextPage_ThenPreservesScrollPosition() {
let asyncExpectation = expectationWithDescription("update") let asyncExpectation = expectation(description: "update")
let presenterBuilder = FakePresenterBuilder() let presenterBuilder = FakePresenterBuilder()
let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]]) let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]])
let fakeDataSource = FakeDataSource() let fakeDataSource = FakeDataSource()
@ -129,14 +129,14 @@ class ChatViewControllerTests: XCTestCase {
completion() completion()
} }
self.waitForExpectationsWithTimeout(1) { (error) -> Void in self.waitForExpectations(timeout: 1) { (error) -> Void in
XCTAssertEqual(3000, controller.collectionView(controller.collectionView, numberOfItemsInSection: 0)) XCTAssertEqual(3000, controller.collectionView(controller.collectionView, numberOfItemsInSection: 0))
XCTAssertEqual(contentOffset, controller.collectionView.contentOffset) XCTAssertEqual(contentOffset, controller.collectionView.contentOffset)
} }
} }
func testThat_WhenManyMessagesAreLoaded_ThenRequestForMessageCountContention() { func testThat_WhenManyMessagesAreLoaded_ThenRequestForMessageCountContention() {
let asyncExpectation = expectationWithDescription("update") let asyncExpectation = expectation(description: "update")
let updateQueue = SerialTaskQueueTestHelper() let updateQueue = SerialTaskQueueTestHelper()
let presenterBuilder = FakePresenterBuilder() let presenterBuilder = FakePresenterBuilder()
let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]]) let controller = TesteableChatViewController(presenterBuilders: ["fake-type": [presenterBuilder]])
@ -148,13 +148,13 @@ class ChatViewControllerTests: XCTestCase {
updateQueue.onAllTasksFinished = { updateQueue.onAllTasksFinished = {
asyncExpectation.fulfill() asyncExpectation.fulfill()
} }
self.waitForExpectationsWithTimeout(1) { (error) -> Void in self.waitForExpectations(timeout: 1) { (error) -> Void in
XCTAssertTrue(fakeDataSource.wasRequestedForMessageCountContention) XCTAssertTrue(fakeDataSource.wasRequestedForMessageCountContention)
} }
} }
func testThat_WhenUpdatesFinish_ControllerIsNotRetained() { func testThat_WhenUpdatesFinish_ControllerIsNotRetained() {
let asyncExpectation = expectationWithDescription("update") let asyncExpectation = expectation(description: "update")
let updateQueue = SerialTaskQueueTestHelper() let updateQueue = SerialTaskQueueTestHelper()
var controller: TesteableChatViewController! = TesteableChatViewController(presenterBuilders: ["fake-type": [FakePresenterBuilder()]]) var controller: TesteableChatViewController! = TesteableChatViewController(presenterBuilders: ["fake-type": [FakePresenterBuilder()]])
weak var weakController = controller weak var weakController = controller
@ -171,7 +171,7 @@ class ChatViewControllerTests: XCTestCase {
updateQueue.onAllTasksFinished = { updateQueue.onAllTasksFinished = {
asyncExpectation.fulfill() asyncExpectation.fulfill()
} }
self.waitForExpectationsWithTimeout(1) { (error) -> Void in self.waitForExpectations(timeout: 1) { (error) -> Void in
controller = nil controller = nil
XCTAssertNil(weakController) XCTAssertNil(weakController)
} }
@ -193,28 +193,28 @@ class ChatViewControllerTests: XCTestCase {
func testThat_LayoutAdaptsWhenKeyboardIsShown() { func testThat_LayoutAdaptsWhenKeyboardIsShown() {
let controller = TesteableChatViewController() let controller = TesteableChatViewController()
let notificationCenter = NSNotificationCenter() let notificationCenter = NotificationCenter()
controller.notificationCenter = notificationCenter controller.notificationCenter = notificationCenter
let fakeDataSource = FakeDataSource() let fakeDataSource = FakeDataSource()
fakeDataSource.chatItems = createFakeChatItems(count: 2) fakeDataSource.chatItems = createFakeChatItems(count: 2)
controller.chatDataSource = fakeDataSource controller.chatDataSource = fakeDataSource
self.fakeDidAppearAndLayout(controller: controller) self.fakeDidAppearAndLayout(controller: controller)
notificationCenter.postNotificationName(UIKeyboardWillShowNotification, object: self, userInfo: [UIKeyboardFrameEndUserInfoKey: NSValue(CGRect: CGRect(x: 0, y: 400, width: 400, height: 500))]) 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.convertRect(controller.chatInputView.bounds, fromView: controller.chatInputView).maxY) XCTAssertEqual(400, controller.view.convert(controller.chatInputView.bounds, from: controller.chatInputView).maxY)
} }
func testThat_LayoutAdaptsWhenKeyboardIsHidden() { func testThat_LayoutAdaptsWhenKeyboardIsHidden() {
let controller = TesteableChatViewController() let controller = TesteableChatViewController()
let notificationCenter = NSNotificationCenter() let notificationCenter = NotificationCenter()
controller.notificationCenter = notificationCenter controller.notificationCenter = notificationCenter
let fakeDataSource = FakeDataSource() let fakeDataSource = FakeDataSource()
fakeDataSource.chatItems = createFakeChatItems(count: 2) fakeDataSource.chatItems = createFakeChatItems(count: 2)
controller.chatDataSource = fakeDataSource controller.chatDataSource = fakeDataSource
self.fakeDidAppearAndLayout(controller: controller) self.fakeDidAppearAndLayout(controller: controller)
notificationCenter.postNotificationName(UIKeyboardWillShowNotification, object: self, userInfo: [UIKeyboardFrameEndUserInfoKey: NSValue(CGRect: CGRect(x: 0, y: 400, width: 400, height: 500))]) notificationCenter.post(name: NSNotification.Name.UIKeyboardWillShow, 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.post(name: NSNotification.Name.UIKeyboardDidShow, 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))]) 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.convertRect(controller.chatInputView.bounds, fromView: controller.chatInputView).maxY) XCTAssertEqual(900, controller.view.convert(controller.chatInputView.bounds, from: controller.chatInputView).maxY)
} }
func testThat_GivenCoalescingIsEnabled_WhenMultipleUpdatesAreRequested_ThenUpdatesAreCoalesced() { func testThat_GivenCoalescingIsEnabled_WhenMultipleUpdatesAreRequested_ThenUpdatesAreCoalesced() {
@ -225,7 +225,7 @@ class ChatViewControllerTests: XCTestCase {
let updateQueue = SerialTaskQueueTestHelper() let updateQueue = SerialTaskQueueTestHelper()
controller.updateQueue = updateQueue controller.updateQueue = updateQueue
controller.setChatDataSource(fakeDataSource, triggeringUpdateType: .None) controller.setChatDataSource(fakeDataSource, triggeringUpdateType: .none)
controller.chatDataSourceDidUpdate(fakeDataSource) // running controller.chatDataSourceDidUpdate(fakeDataSource) // running
controller.chatDataSourceDidUpdate(fakeDataSource) // discarded controller.chatDataSourceDidUpdate(fakeDataSource) // discarded
controller.chatDataSourceDidUpdate(fakeDataSource) // discarded controller.chatDataSourceDidUpdate(fakeDataSource) // discarded
@ -242,7 +242,7 @@ class ChatViewControllerTests: XCTestCase {
controller.updateQueue = updateQueue controller.updateQueue = updateQueue
updateQueue.start() updateQueue.start()
controller.setChatDataSource(fakeDataSource, triggeringUpdateType: .None) controller.setChatDataSource(fakeDataSource, triggeringUpdateType: .none)
controller.chatDataSourceDidUpdate(fakeDataSource) // running controller.chatDataSourceDidUpdate(fakeDataSource) // running
controller.chatDataSourceDidUpdate(fakeDataSource) // queued controller.chatDataSourceDidUpdate(fakeDataSource) // queued
controller.chatDataSourceDidUpdate(fakeDataSource) // queued controller.chatDataSourceDidUpdate(fakeDataSource) // queued
@ -253,7 +253,7 @@ class ChatViewControllerTests: XCTestCase {
// MARK: helpers // 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.view.frame = CGRect(x: 0, y: 0, width: 400, height: 900)
controller.viewWillAppear(true) controller.viewWillAppear(true)
controller.viewDidAppear(true) controller.viewDidAppear(true)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -500,7 +500,7 @@
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastSwiftUpdateCheck = 0720; LastSwiftUpdateCheck = 0720;
LastUpgradeCheck = 0720; LastUpgradeCheck = 0800;
ORGANIZATIONNAME = Badoo; ORGANIZATIONNAME = Badoo;
TargetAttributes = { TargetAttributes = {
C3C0CBB71BFE49320052747C = { C3C0CBB71BFE49320052747C = {
@ -670,8 +670,10 @@
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
@ -699,7 +701,7 @@
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 2.3; SWIFT_VERSION = 3.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = ""; VERSION_INFO_PREFIX = "";
@ -719,8 +721,10 @@
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
@ -741,7 +745,7 @@
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 2.3; SWIFT_VERSION = 3.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES; VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";

View File

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

View File

@ -26,15 +26,15 @@ import Foundation
import Chatto import Chatto
public enum MessageStatus { public enum MessageStatus {
case Failed case failed
case Sending case sending
case Success case success
} }
public protocol MessageModelProtocol: ChatItemProtocol { public protocol MessageModelProtocol: ChatItemProtocol {
var senderId: String { get } var senderId: String { get }
var isIncoming: Bool { get } var isIncoming: Bool { get }
var date: NSDate { get } var date: Date { get }
var status: MessageStatus { get } var status: MessageStatus { get }
} }
@ -59,7 +59,7 @@ public extension DecoratedMessageModelProtocol {
return self.messageModel.isIncoming return self.messageModel.isIncoming
} }
var date: NSDate { var date: Date {
return self.messageModel.date return self.messageModel.date
} }
@ -68,15 +68,15 @@ public extension DecoratedMessageModelProtocol {
} }
} }
public class MessageModel: MessageModelProtocol { open class MessageModel: MessageModelProtocol {
public var uid: String open var uid: String
public var senderId: String open var senderId: String
public var type: String open var type: String
public var isIncoming: Bool open var isIncoming: Bool
public var date: NSDate open var date: Date
public var status: MessageStatus 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.uid = uid
self.senderId = senderId self.senderId = senderId
self.type = type self.type = type

View File

@ -29,24 +29,24 @@ public protocol ViewModelBuilderProtocol {
associatedtype ModelT: MessageModelProtocol associatedtype ModelT: MessageModelProtocol
associatedtype ViewModelT: MessageViewModelProtocol associatedtype ViewModelT: MessageViewModelProtocol
func canCreateViewModel(fromModel model: Any) -> Bool func canCreateViewModel(fromModel model: Any) -> Bool
func createViewModel(model: ModelT) -> ViewModelT func createViewModel(_ model: ModelT) -> ViewModelT
} }
public protocol BaseMessageInteractionHandlerProtocol { public protocol BaseMessageInteractionHandlerProtocol {
associatedtype ViewModelT associatedtype ViewModelT
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)
} }
public class BaseMessagePresenter<BubbleViewT, ViewModelBuilderT, InteractionHandlerT where open class BaseMessagePresenter<BubbleViewT, ViewModelBuilderT, InteractionHandlerT>: BaseChatItemPresenter<BaseMessageCollectionViewCell<BubbleViewT>> where
ViewModelBuilderT: ViewModelBuilderProtocol, ViewModelBuilderT: ViewModelBuilderProtocol,
ViewModelBuilderT.ViewModelT: MessageViewModelProtocol, ViewModelBuilderT.ViewModelT: MessageViewModelProtocol,
InteractionHandlerT: BaseMessageInteractionHandlerProtocol, InteractionHandlerT: BaseMessageInteractionHandlerProtocol,
InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT, 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 CellT = BaseMessageCollectionViewCell<BubbleViewT>
public typealias ModelT = ViewModelBuilderT.ModelT public typealias ModelT = ViewModelBuilderT.ModelT
public typealias ViewModelT = ViewModelBuilderT.ViewModelT public typealias ViewModelT = ViewModelBuilderT.ViewModelT
@ -74,12 +74,12 @@ public class BaseMessagePresenter<BubbleViewT, ViewModelBuilderT, InteractionHan
return self.createViewModel() return self.createViewModel()
}() }()
public func createViewModel() -> ViewModelT { open func createViewModel() -> ViewModelT {
let viewModel = self.viewModelBuilder.createViewModel(self.messageModel) let viewModel = self.viewModelBuilder.createViewModel(self.messageModel)
return viewModel 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 { guard let cell = cell as? CellT else {
assert(false, "Invalid cell given to presenter") assert(false, "Invalid cell given to presenter")
return return
@ -94,13 +94,11 @@ public class BaseMessagePresenter<BubbleViewT, ViewModelBuilderT, InteractionHan
} }
public var decorationAttributes: ChatItemDecorationAttributes! 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 cell.performBatchUpdates({ () -> Void in
self.messageViewModel.showsTail = decorationAttributes.showsTail self.messageViewModel.showsTail = decorationAttributes.showsTail
if !decorationAttributes.canShowAvatar { cell.avatarView.isHidden = !decorationAttributes.canShowAvatar
self.messageViewModel.avatarImage.value = nil cell.bubbleView.isUserInteractionEnabled = true // just in case something went wrong while showing UIMenuController
}
cell.bubbleView.userInteractionEnabled = true // just in case something went wrong while showing UIMenuController
cell.baseStyle = self.cellStyle cell.baseStyle = self.cellStyle
cell.messageViewModel = self.messageViewModel cell.messageViewModel = self.messageViewModel
cell.onBubbleTapped = { [weak self] (cell) in cell.onBubbleTapped = { [weak self] (cell) in
@ -127,65 +125,73 @@ public class BaseMessagePresenter<BubbleViewT, ViewModelBuilderT, InteractionHan
}, animated: animated, completion: nil) }, 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 { guard let decorationAttributes = decorationAttributes as? ChatItemDecorationAttributes else {
assert(false, "Expecting decoration attributes") assert(false, "Expecting decoration attributes")
return 0 return 0
} }
self.configureCell(self.sizingCell, decorationAttributes: decorationAttributes, animated: false, additionalConfiguration: nil) 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 return self.sizingCell.canCalculateSizeInBackground
} }
public override func shouldShowMenu() -> Bool { open override func cellWillBeShown() {
self.messageViewModel.willBeShown()
}
open override func cellWasHidden() {
self.messageViewModel.wasHidden()
}
open override func shouldShowMenu() -> Bool {
guard self.canShowMenu() else { return false } guard self.canShowMenu() else { return false }
guard let cell = self.cell else { guard let cell = self.cell else {
assert(false, "Investigate -> Fix or remove assert") assert(false, "Investigate -> Fix or remove assert")
return false return false
} }
cell.bubbleView.userInteractionEnabled = false // This is a hack for UITextView, shouldn't harm to all bubbles cell.bubbleView.isUserInteractionEnabled = 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) NotificationCenter.default.addObserver(self, selector: #selector(BaseMessagePresenter.willShowMenu(_:)), name: NSNotification.Name.UIMenuControllerWillShowMenu, object: nil)
return true return true
} }
@objc @objc
func willShowMenu(notification: NSNotification) { func willShowMenu(_ notification: Notification) {
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIMenuControllerWillShowMenuNotification, object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIMenuControllerWillShowMenu, object: nil)
guard let cell = self.cell, menuController = notification.object as? UIMenuController else { guard let cell = self.cell, let menuController = notification.object as? UIMenuController else {
assert(false, "Investigate -> Fix or remove assert") assert(false, "Investigate -> Fix or remove assert")
return return
} }
cell.bubbleView.userInteractionEnabled = true cell.bubbleView.isUserInteractionEnabled = true
menuController.setMenuVisible(false, animated: false) 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) menuController.setMenuVisible(true, animated: true)
} }
public func canShowMenu() -> Bool { open func canShowMenu() -> Bool {
// Override in subclass // Override in subclass
return false return false
} }
public func onCellBubbleTapped() { open func onCellBubbleTapped() {
self.interactionHandler?.userDidTapOnBubble(viewModel: self.messageViewModel) self.interactionHandler?.userDidTapOnBubble(viewModel: self.messageViewModel)
} }
public func onCellBubbleLongPressBegan() { open func onCellBubbleLongPressBegan() {
self.interactionHandler?.userDidBeginLongPressOnBubble(viewModel: self.messageViewModel) self.interactionHandler?.userDidBeginLongPressOnBubble(viewModel: self.messageViewModel)
} }
public func onCellBubbleLongPressEnded() { open func onCellBubbleLongPressEnded() {
self.interactionHandler?.userDidEndLongPressOnBubble(viewModel: self.messageViewModel) self.interactionHandler?.userDidEndLongPressOnBubble(viewModel: self.messageViewModel)
} }
public func onCellAvatarTapped() { open func onCellAvatarTapped() {
self.interactionHandler?.userDidTapOnAvatar(viewModel: self.messageViewModel) self.interactionHandler?.userDidTapOnAvatar(viewModel: self.messageViewModel)
} }
public func onCellFailedButtonTapped(failedButtonView: UIView) { open func onCellFailedButtonTapped(_ failedButtonView: UIView) {
self.interactionHandler?.userDidTapOnFailIcon(viewModel: self.messageViewModel, failIconView: failedButtonView) self.interactionHandler?.userDidTapOnFailIcon(viewModel: self.messageViewModel, failIconView: failedButtonView)
} }
} }

View File

@ -25,20 +25,20 @@
import Foundation import Foundation
public enum MessageViewModelStatus { public enum MessageViewModelStatus {
case Success case success
case Sending case sending
case Failed case failed
} }
public extension MessageStatus { public extension MessageStatus {
public func viewModelStatus() -> MessageViewModelStatus { public func viewModelStatus() -> MessageViewModelStatus {
switch self { switch self {
case .Success: case .success:
return MessageViewModelStatus.Success return MessageViewModelStatus.success
case .Failed: case .failed:
return MessageViewModelStatus.Failed return MessageViewModelStatus.failed
case .Sending: case .sending:
return MessageViewModelStatus.Sending return MessageViewModelStatus.sending
} }
} }
} }
@ -50,6 +50,13 @@ public protocol MessageViewModelProtocol: class { // why class? https://gist.git
var date: String { get } var date: String { get }
var status: MessageViewModelStatus { get } var status: MessageViewModelStatus { get }
var avatarImage: Observable<UIImage?> { set get } var avatarImage: Observable<UIImage?> { set get }
func willBeShown() // Optional
func wasHidden() // Optional
}
extension MessageViewModelProtocol {
public func willBeShown() {}
public func wasHidden() {}
} }
public protocol DecoratedMessageViewModelProtocol: MessageViewModelProtocol { public protocol DecoratedMessageViewModelProtocol: MessageViewModelProtocol {
@ -90,32 +97,32 @@ extension DecoratedMessageViewModelProtocol {
} }
} }
public class MessageViewModel: MessageViewModelProtocol { open class MessageViewModel: MessageViewModelProtocol {
public var isIncoming: Bool { open var isIncoming: Bool {
return self.messageModel.isIncoming return self.messageModel.isIncoming
} }
public var status: MessageViewModelStatus { open var status: MessageViewModelStatus {
return self.messageModel.status.viewModelStatus() return self.messageModel.status.viewModelStatus()
} }
public var showsTail: Bool open var showsTail: Bool
public lazy var date: String = { open lazy var date: String = {
return self.dateFormatter.stringFromDate(self.messageModel.date) 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 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.dateFormatter = dateFormatter
self.showsTail = showsTail self.showsTail = showsTail
self.messageModel = messageModel self.messageModel = messageModel
self.avatarImage = Observable<UIImage?>(avatarImage) self.avatarImage = Observable<UIImage?>(avatarImage)
} }
public var showsFailedIcon: Bool { open var showsFailedIcon: Bool {
return self.status == .Failed return self.status == .failed
} }
public var avatarImage: Observable<UIImage?> public var avatarImage: Observable<UIImage?>
@ -125,15 +132,15 @@ public class MessageViewModelDefaultBuilder {
public init() {} public init() {}
static let dateFormatter: NSDateFormatter = { static let dateFormatter: DateFormatter = {
let formatter = NSDateFormatter() let formatter = DateFormatter()
formatter.locale = NSLocale.currentLocale() formatter.locale = Locale.current
formatter.dateStyle = .NoStyle formatter.dateStyle = .none
formatter.timeStyle = .ShortStyle formatter.timeStyle = .short
return formatter return formatter
}() }()
public func createMessageViewModel(message: MessageModelProtocol) -> MessageViewModelProtocol { public func createMessageViewModel(_ message: MessageModelProtocol) -> MessageViewModelProtocol {
// Override to use default avatarImage // Override to use default avatarImage
return MessageViewModel(dateFormatter: MessageViewModelDefaultBuilder.dateFormatter, showsTail: false, messageModel: message, avatarImage: nil) return MessageViewModel(dateFormatter: MessageViewModelDefaultBuilder.dateFormatter, showsTail: false, messageModel: message, avatarImage: nil)
} }

View File

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

View File

@ -24,7 +24,7 @@
import UIKit import UIKit
public class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionViewCellStyleProtocol { open class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionViewCellStyleProtocol {
typealias Class = BaseMessageCollectionViewCellDefaultStyle typealias Class = BaseMessageCollectionViewCellDefaultStyle
@ -32,8 +32,8 @@ public class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionVie
let incoming: () -> UIColor let incoming: () -> UIColor
let outgoing: () -> UIColor let outgoing: () -> UIColor
public init( public init(
@autoclosure(escaping) incoming: () -> UIColor, incoming: @autoclosure @escaping () -> UIColor,
@autoclosure(escaping) outgoing: () -> UIColor) { outgoing: @autoclosure @escaping () -> UIColor) {
self.incoming = incoming self.incoming = incoming
self.outgoing = outgoing self.outgoing = outgoing
} }
@ -45,10 +45,10 @@ public class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionVie
public let borderOutgoingTail: () -> UIImage public let borderOutgoingTail: () -> UIImage
public let borderOutgoingNoTail: () -> UIImage public let borderOutgoingNoTail: () -> UIImage
public init( public init(
@autoclosure(escaping) borderIncomingTail: () -> UIImage, borderIncomingTail: @autoclosure @escaping () -> UIImage,
@autoclosure(escaping) borderIncomingNoTail: () -> UIImage, borderIncomingNoTail: @autoclosure @escaping () -> UIImage,
@autoclosure(escaping) borderOutgoingTail: () -> UIImage, borderOutgoingTail: @autoclosure @escaping () -> UIImage,
@autoclosure(escaping) borderOutgoingNoTail: () -> UIImage) { borderOutgoingNoTail: @autoclosure @escaping () -> UIImage) {
self.borderIncomingTail = borderIncomingTail self.borderIncomingTail = borderIncomingTail
self.borderIncomingNoTail = borderIncomingNoTail self.borderIncomingNoTail = borderIncomingNoTail
self.borderOutgoingTail = borderOutgoingTail self.borderOutgoingTail = borderOutgoingTail
@ -60,8 +60,8 @@ public class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionVie
let normal: () -> UIImage let normal: () -> UIImage
let highlighted: () -> UIImage let highlighted: () -> UIImage
public init( public init(
@autoclosure(escaping) normal: () -> UIImage, normal: @autoclosure @escaping () -> UIImage,
@autoclosure(escaping) highlighted: () -> UIImage) { highlighted: @autoclosure @escaping () -> UIImage) {
self.normal = normal self.normal = normal
self.highlighted = highlighted self.highlighted = highlighted
} }
@ -71,8 +71,8 @@ public class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionVie
let font: () -> UIFont let font: () -> UIFont
let color: () -> UIColor let color: () -> UIColor
public init( public init(
@autoclosure(escaping) font: () -> UIFont, font: @autoclosure @escaping () -> UIFont,
@autoclosure(escaping) color: () -> UIColor) { color: @autoclosure @escaping () -> UIColor) {
self.font = font self.font = font
self.color = color self.color = color
} }
@ -81,7 +81,7 @@ public class BaseMessageCollectionViewCellDefaultStyle: BaseMessageCollectionVie
public struct AvatarStyle { public struct AvatarStyle {
let size: CGSize let size: CGSize
let alignment: VerticalAlignment let alignment: VerticalAlignment
public init(size: CGSize = .zero, alignment: VerticalAlignment = .Bottom) { public init(size: CGSize = .zero, alignment: VerticalAlignment = .bottom) {
self.size = size self.size = size
self.alignment = alignment 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) 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) { switch (viewModel.isIncoming, viewModel.showsTail) {
case (true, true): case (true, true):
return self.borderIncomingTail 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 return self.avatarStyle.size
} }
public func avatarVerticalAlignment(viewModel viewModel: MessageViewModelProtocol) -> VerticalAlignment { open func avatarVerticalAlignment(viewModel: MessageViewModelProtocol) -> VerticalAlignment {
return self.avatarStyle.alignment return self.avatarStyle.alignment
} }
public func layoutConstants(viewModel viewModel: MessageViewModelProtocol) -> BaseMessageCollectionViewCellLayoutConstants { open func layoutConstants(viewModel: MessageViewModelProtocol) -> BaseMessageCollectionViewCellLayoutConstants {
return self.layoutConstants return self.layoutConstants
} }
} }
@ -165,25 +165,25 @@ public extension BaseMessageCollectionViewCellDefaultStyle { // Default values
static public func createDefaultBubbleBorderImages() -> BubbleBorderImages { static public func createDefaultBubbleBorderImages() -> BubbleBorderImages {
return BubbleBorderImages( return BubbleBorderImages(
borderIncomingTail: UIImage(named: "bubble-incoming-border-tail", 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", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!, borderIncomingNoTail: UIImage(named: "bubble-incoming-border", in: Bundle(for: Class.self), compatibleWith: nil)!,
borderOutgoingTail: UIImage(named: "bubble-outgoing-border-tail", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!, borderOutgoingTail: UIImage(named: "bubble-outgoing-border-tail", in: Bundle(for: Class.self), compatibleWith: nil)!,
borderOutgoingNoTail: UIImage(named: "bubble-outgoing-border", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)! borderOutgoingNoTail: UIImage(named: "bubble-outgoing-border", in: Bundle(for: Class.self), compatibleWith: nil)!
) )
} }
static public func createDefaultFailedIconImages() -> FailedIconImages { static public func createDefaultFailedIconImages() -> FailedIconImages {
let normal = { 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( return FailedIconImages(
normal: normal(), 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 { 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 { static public func createDefaultLayoutConstants() -> BaseMessageCollectionViewCellLayoutConstants {

View File

@ -25,8 +25,8 @@
import Foundation import Foundation
public enum ViewContext { public enum ViewContext {
case Normal case normal
case Sizing // You may skip some cell updates for faster sizing case sizing // You may skip some cell updates for faster sizing
} }
public protocol MaximumLayoutWidthSpecificable { public protocol MaximumLayoutWidthSpecificable {

View File

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

View File

@ -24,12 +24,12 @@
import Foundation import Foundation
public class PhotoMessagePresenter<ViewModelBuilderT, InteractionHandlerT where open class PhotoMessagePresenter<ViewModelBuilderT, InteractionHandlerT>
: BaseMessagePresenter<PhotoBubbleView, ViewModelBuilderT, InteractionHandlerT> where
ViewModelBuilderT: ViewModelBuilderProtocol, ViewModelBuilderT: ViewModelBuilderProtocol,
ViewModelBuilderT.ViewModelT: PhotoMessageViewModelProtocol, ViewModelBuilderT.ViewModelT: PhotoMessageViewModelProtocol,
InteractionHandlerT: BaseMessageInteractionHandlerProtocol, InteractionHandlerT: BaseMessageInteractionHandlerProtocol,
InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT> InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT {
: BaseMessagePresenter<PhotoBubbleView, ViewModelBuilderT, InteractionHandlerT> {
public typealias ModelT = ViewModelBuilderT.ModelT public typealias ModelT = ViewModelBuilderT.ModelT
public typealias ViewModelT = ViewModelBuilderT.ViewModelT public typealias ViewModelT = ViewModelBuilderT.ViewModelT
@ -52,19 +52,20 @@ public class PhotoMessagePresenter<ViewModelBuilderT, InteractionHandlerT where
) )
} }
public override class func registerCells(collectionView: UICollectionView) { public final override class func registerCells(_ collectionView: UICollectionView) {
collectionView.registerClass(PhotoMessageCollectionViewCell.self, forCellWithReuseIdentifier: "photo-message") collectionView.register(PhotoMessageCollectionViewCell.self, forCellWithReuseIdentifier: "photo-message")
} }
public override func dequeueCell(collectionView collectionView: UICollectionView, indexPath: NSIndexPath) -> UICollectionViewCell { public final override func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCellWithReuseIdentifier("photo-message", forIndexPath: indexPath) 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 viewModel = self.viewModelBuilder.createViewModel(self.messageModel)
let updateClosure = { [weak self] (old: Any, new: Any) -> () in let updateClosure = { [weak self] (old: Any, new: Any) -> () in
self?.updateCurrentCell() self?.updateCurrentCell()
} }
viewModel.avatarImage.observe(self, closure: updateClosure)
viewModel.image.observe(self, closure: updateClosure) viewModel.image.observe(self, closure: updateClosure)
viewModel.transferDirection.observe(self, closure: updateClosure) viewModel.transferDirection.observe(self, closure: updateClosure)
viewModel.transferProgress.observe(self, closure: updateClosure) viewModel.transferProgress.observe(self, closure: updateClosure)
@ -83,7 +84,7 @@ public class PhotoMessagePresenter<ViewModelBuilderT, InteractionHandlerT where
return nil 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 { guard let cell = cell as? PhotoMessageCollectionViewCell else {
assert(false, "Invalid cell received") assert(false, "Invalid cell received")
return return
@ -96,18 +97,9 @@ public class PhotoMessagePresenter<ViewModelBuilderT, InteractionHandlerT where
} }
} }
public override func cellWillBeShown() {
self.messageViewModel.willBeShown()
}
public override func cellWasHidden() {
self.messageViewModel.wasHidden()
}
public func updateCurrentCell() { public func updateCurrentCell() {
if let cell = self.photoCell, decorationAttributes = self.decorationAttributes { if let cell = self.photoCell, let decorationAttributes = self.decorationAttributes {
self.configureCell(cell, decorationAttributes: decorationAttributes, animated: self.itemVisibility != .Appearing, additionalConfiguration: nil) self.configureCell(cell, decorationAttributes: decorationAttributes, animated: self.itemVisibility != .appearing, additionalConfiguration: nil)
} }
} }
} }

View File

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

View File

@ -25,15 +25,15 @@
import UIKit import UIKit
public enum TransferDirection { public enum TransferDirection {
case Upload case upload
case Download case download
} }
public enum TransferStatus { public enum TransferStatus {
case Idle case idle
case Transfering case transfering
case Failed case failed
case Success case success
} }
public protocol PhotoMessageViewModelProtocol: DecoratedMessageViewModelProtocol { public protocol PhotoMessageViewModelProtocol: DecoratedMessageViewModelProtocol {
@ -42,30 +42,23 @@ public protocol PhotoMessageViewModelProtocol: DecoratedMessageViewModelProtocol
var transferStatus: Observable<TransferStatus> { get set } var transferStatus: Observable<TransferStatus> { get set }
var image: Observable<UIImage?> { get set } var image: Observable<UIImage?> { get set }
var imageSize: CGSize { get } var imageSize: CGSize { get }
func willBeShown() // Optional
func wasHidden() // Optional
} }
public extension PhotoMessageViewModelProtocol { open class PhotoMessageViewModel<PhotoMessageModelT: PhotoMessageModelProtocol>: PhotoMessageViewModelProtocol {
func willBeShown() {}
func wasHidden() {}
}
public class PhotoMessageViewModel<PhotoMessageModelT: PhotoMessageModelProtocol>: PhotoMessageViewModelProtocol {
public var photoMessage: PhotoMessageModelProtocol { public var photoMessage: PhotoMessageModelProtocol {
return self._photoMessage return self._photoMessage
} }
public let _photoMessage: PhotoMessageModelT // Can't make photoMessage: PhotoMessageModelT: https://gist.github.com/diegosanchezr/5a66c7af862e1117b556 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 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 image: Observable<UIImage?>
public var imageSize: CGSize { open var imageSize: CGSize {
return self.photoMessage.imageSize return self.photoMessage.imageSize
} }
public let messageViewModel: MessageViewModelProtocol public let messageViewModel: MessageViewModelProtocol
public var showsFailedIcon: Bool { open var showsFailedIcon: Bool {
return self.messageViewModel.showsFailedIcon || self.transferStatus.value == .Failed return self.messageViewModel.showsFailedIcon || self.transferStatus.value == .failed
} }
public init(photoMessage: PhotoMessageModelT, messageViewModel: MessageViewModelProtocol) { public init(photoMessage: PhotoMessageModelT, messageViewModel: MessageViewModelProtocol) {
@ -74,27 +67,27 @@ public class PhotoMessageViewModel<PhotoMessageModelT: PhotoMessageModelProtocol
self.messageViewModel = messageViewModel self.messageViewModel = messageViewModel
} }
public func willBeShown() { open func willBeShown() {
// Need to declare empty. Otherwise subclass code won't execute (as of Xcode 7.2) // 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) // Need to declare empty. Otherwise subclass code won't execute (as of Xcode 7.2)
} }
} }
public class PhotoMessageViewModelDefaultBuilder<PhotoMessageModelT: PhotoMessageModelProtocol>: ViewModelBuilderProtocol { open class PhotoMessageViewModelDefaultBuilder<PhotoMessageModelT: PhotoMessageModelProtocol>: ViewModelBuilderProtocol {
public init() {} public init() {}
let messageViewModelBuilder = MessageViewModelDefaultBuilder() let messageViewModelBuilder = MessageViewModelDefaultBuilder()
public func createViewModel(model: PhotoMessageModelT) -> PhotoMessageViewModel<PhotoMessageModelT> { open func createViewModel(_ model: PhotoMessageModelT) -> PhotoMessageViewModel<PhotoMessageModelT> {
let messageViewModel = self.messageViewModelBuilder.createMessageViewModel(model) let messageViewModel = self.messageViewModelBuilder.createMessageViewModel(model)
let photoMessageViewModel = PhotoMessageViewModel(photoMessage: model, messageViewModel: messageViewModel) let photoMessageViewModel = PhotoMessageViewModel(photoMessage: model, messageViewModel: messageViewModel)
return photoMessageViewModel return photoMessageViewModel
} }
public func canCreateViewModel(fromModel model: Any) -> Bool { open func canCreateViewModel(fromModel model: Any) -> Bool {
return model is PhotoMessageModelT return model is PhotoMessageModelT
} }
} }

View File

@ -25,19 +25,19 @@
import UIKit import UIKit
public protocol PhotoBubbleViewStyleProtocol { public protocol PhotoBubbleViewStyleProtocol {
func maskingImage(viewModel viewModel: PhotoMessageViewModelProtocol) -> UIImage func maskingImage(viewModel: PhotoMessageViewModelProtocol) -> UIImage
func borderImage(viewModel viewModel: PhotoMessageViewModelProtocol) -> UIImage? func borderImage(viewModel: PhotoMessageViewModelProtocol) -> UIImage?
func placeholderBackgroundImage(viewModel viewModel: PhotoMessageViewModelProtocol) -> UIImage func placeholderBackgroundImage(viewModel: PhotoMessageViewModelProtocol) -> UIImage
func placeholderIconImage(viewModel viewModel: PhotoMessageViewModelProtocol) -> (icon: UIImage?, tintColor: UIColor?) func placeholderIconImage(viewModel: PhotoMessageViewModelProtocol) -> (icon: UIImage?, tintColor: UIColor?)
func tailWidth(viewModel viewModel: PhotoMessageViewModelProtocol) -> CGFloat func tailWidth(viewModel: PhotoMessageViewModelProtocol) -> CGFloat
func bubbleSize(viewModel viewModel: PhotoMessageViewModelProtocol) -> CGSize func bubbleSize(viewModel: PhotoMessageViewModelProtocol) -> CGSize
func progressIndicatorColor(viewModel viewModel: PhotoMessageViewModelProtocol) -> UIColor func progressIndicatorColor(viewModel: PhotoMessageViewModelProtocol) -> UIColor
func overlayColor(viewModel 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 animationDuration: CFTimeInterval = 0.33
public var preferredMaxLayoutWidth: CGFloat = 0 public var preferredMaxLayoutWidth: CGFloat = 0
@ -60,11 +60,11 @@ public class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, Background
public private(set) lazy var imageView: UIImageView = { public private(set) lazy var imageView: UIImageView = {
let imageView = UIImageView() let imageView = UIImageView()
imageView.autoresizingMask = .None imageView.autoresizingMask = UIViewAutoresizing()
imageView.clipsToBounds = true imageView.clipsToBounds = true
imageView.autoresizesSubviews = false imageView.autoresizesSubviews = false
imageView.autoresizingMask = .None imageView.autoresizingMask = UIViewAutoresizing()
imageView.contentMode = .ScaleAspectFill imageView.contentMode = .scaleAspectFill
imageView.addSubview(self.borderView) imageView.addSubview(self.borderView)
return imageView return imageView
}() }()
@ -78,12 +78,12 @@ public class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, Background
public private(set) var progressIndicatorView: CircleProgressIndicatorView = { public private(set) var progressIndicatorView: CircleProgressIndicatorView = {
let progressView = CircleProgressIndicatorView(size: CGSize(width: 33, height: 33)) let progressView = CircleProgressIndicatorView(size: CGSize(width: 33, height: 33))
return progressView return progressView!
}() }()
private var placeholderIconView: UIImageView = { private var placeholderIconView: UIImageView = {
let imageView = UIImageView() let imageView = UIImageView()
imageView.autoresizingMask = .None imageView.autoresizingMask = UIViewAutoresizing()
return imageView return imageView
}() }()
@ -100,7 +100,7 @@ public class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, Background
} }
public private(set) var isUpdating: Bool = false 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 self.isUpdating = true
let updateAndRefreshViews = { let updateAndRefreshViews = {
updateClosure() updateClosure()
@ -111,7 +111,7 @@ public class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, Background
} }
} }
if animated { if animated {
UIView.animateWithDuration(self.animationDuration, animations: updateAndRefreshViews, completion: { (finished) -> Void in UIView.animate(withDuration: self.animationDuration, animations: updateAndRefreshViews, completion: { (finished) -> Void in
completion?() completion?()
}) })
} else { } else {
@ -119,10 +119,10 @@ public class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, Background
} }
} }
public func updateViews() { open func updateViews() {
if self.viewContext == .Sizing { return } if self.viewContext == .sizing { return }
if isUpdating { return } if isUpdating { return }
guard let _ = self.photoMessageViewModel, _ = self.photoMessageStyle else { return } guard let _ = self.photoMessageViewModel, let _ = self.photoMessageStyle else { return }
self.updateProgressIndicator() self.updateProgressIndicator()
self.updateImages() self.updateImages()
@ -132,23 +132,23 @@ public class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, Background
private func updateProgressIndicator() { private func updateProgressIndicator() {
let transferStatus = self.photoMessageViewModel.transferStatus.value let transferStatus = self.photoMessageViewModel.transferStatus.value
let transferProgress = self.photoMessageViewModel.transferProgress.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.progressLineColor = self.photoMessageStyle.progressIndicatorColor(viewModel: self.photoMessageViewModel)
self.progressIndicatorView.progressLineWidth = 1 self.progressIndicatorView.progressLineWidth = 1
self.progressIndicatorView.setProgress(CGFloat(transferProgress)) self.progressIndicatorView.setProgress(CGFloat(transferProgress))
switch transferStatus { switch transferStatus {
case .Idle, .Success, .Failed: case .idle, .success, .failed:
break break
case .Transfering: case .transfering:
switch transferProgress { switch transferProgress {
case 0: case 0:
if self.progressIndicatorView.progressStatus != .Starting { self.progressIndicatorView.progressStatus = .Starting } if self.progressIndicatorView.progressStatus != .starting { self.progressIndicatorView.progressStatus = .starting }
case 1: case 1:
if self.progressIndicatorView.progressStatus != .Completed { self.progressIndicatorView.progressStatus = .Completed } if self.progressIndicatorView.progressStatus != .completed { self.progressIndicatorView.progressStatus = .completed }
default: 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() { private func updateImages() {
if let image = self.photoMessageViewModel.image.value { if let image = self.photoMessageViewModel.image.value {
self.imageView.image = image self.imageView.image = image
self.placeholderIconView.hidden = true self.placeholderIconView.isHidden = true
} else { } else {
self.imageView.image = self.photoMessageStyle.placeholderBackgroundImage(viewModel: self.photoMessageViewModel) self.imageView.image = self.photoMessageStyle.placeholderBackgroundImage(viewModel: self.photoMessageViewModel)
let (icon, tintColor) = photoMessageStyle.placeholderIconImage(viewModel: self.photoMessageViewModel) let (icon, tintColor) = photoMessageStyle.placeholderIconImage(viewModel: self.photoMessageViewModel)
self.placeholderIconView.image = icon self.placeholderIconView.image = icon
self.placeholderIconView.tintColor = tintColor self.placeholderIconView.tintColor = tintColor
self.placeholderIconView.hidden = false self.placeholderIconView.isHidden = false
} }
if let overlayColor = self.photoMessageStyle.overlayColor(viewModel: self.photoMessageViewModel) { 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 self.imageView.layer.mask = UIImageView(image: self.photoMessageStyle.maskingImage(viewModel: self.photoMessageViewModel)).layer
} }
// MARK: Layout // MARK: Layout
public override func sizeThatFits(size: CGSize) -> CGSize { open override func sizeThatFits(_ size: CGSize) -> CGSize {
return self.calculateTextBubbleLayout(maximumWidth: size.width).size return self.calculateTextBubbleLayout(maximumWidth: size.width).size
} }
public override func layoutSubviews() { open override func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
let layout = self.calculateTextBubbleLayout(maximumWidth: self.preferredMaxLayoutWidth) let layout = self.calculateTextBubbleLayout(maximumWidth: self.preferredMaxLayoutWidth)
self.progressIndicatorView.center = layout.visualCenter self.progressIndicatorView.center = layout.visualCenter
@ -197,20 +196,19 @@ public class PhotoBubbleView: UIView, MaximumLayoutWidthSpecificable, Background
self.borderView.bma_rect = self.imageView.bounds 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 layoutContext = PhotoBubbleLayoutModel.LayoutContext(photoMessageViewModel: self.photoMessageViewModel, style: self.photoMessageStyle, containerWidth: maximumWidth)
let layoutModel = PhotoBubbleLayoutModel(layoutContext: layoutContext) let layoutModel = PhotoBubbleLayoutModel(layoutContext: layoutContext)
layoutModel.calculateLayout() layoutModel.calculateLayout()
return layoutModel return layoutModel
} }
public var canCalculateSizeInBackground: Bool { open var canCalculateSizeInBackground: Bool {
return true return true
} }
} }
private class PhotoBubbleLayoutModel { private class PhotoBubbleLayoutModel {
var photoFrame: CGRect = CGRect.zero 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 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 { static func sizingCell() -> PhotoMessageCollectionViewCell {
let cell = PhotoMessageCollectionViewCell(frame: CGRect.zero) let cell = PhotoMessageCollectionViewCell(frame: CGRect.zero)
cell.viewContext = .Sizing cell.viewContext = .sizing
return cell return cell
} }
public override init(frame: CGRect) {
super.init(frame: frame)
}
public override func createBubbleView() -> PhotoBubbleView { public override func createBubbleView() -> PhotoBubbleView {
return 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 super.performBatchUpdates({ () -> Void in
self.bubbleView.performBatchUpdates(updateClosure, animated: false, completion: nil) self.bubbleView.performBatchUpdates(updateClosure, animated: false, completion: nil)
}, animated: animated, completion: completion) }, animated: animated, completion: completion)

View File

@ -24,7 +24,7 @@
import UIKit import UIKit
public class PhotoMessageCollectionViewCellDefaultStyle: PhotoMessageCollectionViewCellStyleProtocol { open class PhotoMessageCollectionViewCellDefaultStyle: PhotoMessageCollectionViewCellStyleProtocol {
typealias Class = PhotoMessageCollectionViewCellDefaultStyle typealias Class = PhotoMessageCollectionViewCellDefaultStyle
public struct BubbleMasks { public struct BubbleMasks {
@ -34,10 +34,10 @@ public class PhotoMessageCollectionViewCellDefaultStyle: PhotoMessageCollectionV
public let outgoingNoTail: () -> UIImage public let outgoingNoTail: () -> UIImage
public let tailWidth: CGFloat public let tailWidth: CGFloat
public init( public init(
@autoclosure(escaping) incomingTail: () -> UIImage, incomingTail: @autoclosure @escaping () -> UIImage,
@autoclosure(escaping) incomingNoTail: () -> UIImage, incomingNoTail: @autoclosure @escaping () -> UIImage,
@autoclosure(escaping) outgoingTail: () -> UIImage, outgoingTail: @autoclosure @escaping () -> UIImage,
@autoclosure(escaping) outgoingNoTail: () -> UIImage, outgoingNoTail: @autoclosure @escaping () -> UIImage,
tailWidth: CGFloat) { tailWidth: CGFloat) {
self.incomingTail = incomingTail self.incomingTail = incomingTail
self.incomingNoTail = incomingNoTail self.incomingNoTail = incomingNoTail
@ -48,12 +48,12 @@ public class PhotoMessageCollectionViewCellDefaultStyle: PhotoMessageCollectionV
} }
public struct Sizes { public struct Sizes {
public let aspectRatioIntervalForSquaredSize: ClosedInterval<CGFloat> public let aspectRatioIntervalForSquaredSize: ClosedRange<CGFloat>
public let photoSizeLandscape: CGSize public let photoSizeLandscape: CGSize
public let photoSizePortrait: CGSize public let photoSizePortrait: CGSize
public let photoSizeSquare: CGSize public let photoSizeSquare: CGSize
public init( public init(
aspectRatioIntervalForSquaredSize: ClosedInterval<CGFloat>, aspectRatioIntervalForSquaredSize: ClosedRange<CGFloat>,
photoSizeLandscape: CGSize, photoSizeLandscape: CGSize,
photoSizePortrait: CGSize, photoSizePortrait: CGSize,
photoSizeSquare: CGSize) { photoSizeSquare: CGSize) {
@ -113,10 +113,10 @@ public class PhotoMessageCollectionViewCellDefaultStyle: PhotoMessageCollectionV
}() }()
lazy private var placeholderIcon: UIImage = { 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) { switch (viewModel.isIncoming, viewModel.showsTail) {
case (true, true): case (true, true):
return self.maskImageIncomingTail 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) 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 return viewModel.isIncoming ? self.placeholderBackgroundIncoming : self.placeholderBackgroundOutgoing
} }
public func placeholderIconImage(viewModel viewModel: PhotoMessageViewModelProtocol) -> (icon: UIImage?, tintColor: UIColor?) { open func placeholderIconImage(viewModel: PhotoMessageViewModelProtocol) -> (icon: UIImage?, tintColor: UIColor?) {
if viewModel.image.value == nil && viewModel.transferStatus.value == .Failed { if viewModel.image.value == nil && viewModel.transferStatus.value == .failed {
let tintColor = viewModel.isIncoming ? self.colors.placeholderIconTintIncoming : self.colors.placeholderIconTintOutgoing let tintColor = viewModel.isIncoming ? self.colors.placeholderIconTintIncoming : self.colors.placeholderIconTintOutgoing
return (self.placeholderIcon, tintColor) return (self.placeholderIcon, tintColor)
} }
return (nil, nil) return (nil, nil)
} }
public func tailWidth(viewModel viewModel: PhotoMessageViewModelProtocol) -> CGFloat { open func tailWidth(viewModel: PhotoMessageViewModelProtocol) -> CGFloat {
return self.bubbleMasks.tailWidth 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 let aspectRatio = viewModel.imageSize.height > 0 ? viewModel.imageSize.width / viewModel.imageSize.height : 0
if aspectRatio == 0 || self.sizes.aspectRatioIntervalForSquaredSize.contains(aspectRatio) { if aspectRatio == 0 || self.sizes.aspectRatioIntervalForSquaredSize.contains(aspectRatio) {
return self.sizes.photoSizeSquare return self.sizes.photoSizeSquare
} else if aspectRatio < self.sizes.aspectRatioIntervalForSquaredSize.start { } else if aspectRatio < self.sizes.aspectRatioIntervalForSquaredSize.lowerBound {
return self.sizes.photoSizePortrait return self.sizes.photoSizePortrait
} else { } else {
return self.sizes.photoSizeLandscape 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 return viewModel.isIncoming ? self.colors.progressIndicatorColorIncoming : self.colors.progressIndicatorColorOutgoing
} }
public func overlayColor(viewModel viewModel: PhotoMessageViewModelProtocol) -> UIColor? { open func overlayColor(viewModel: PhotoMessageViewModelProtocol) -> UIColor? {
let showsOverlay = viewModel.image.value != nil && (viewModel.transferStatus.value == .Transfering || viewModel.status != MessageViewModelStatus.Success) let showsOverlay = viewModel.image.value != nil && (viewModel.transferStatus.value == .transfering || viewModel.status != MessageViewModelStatus.success)
return showsOverlay ? self.colors.overlayColor : nil return showsOverlay ? self.colors.overlayColor : nil
} }
@ -176,10 +176,10 @@ public extension PhotoMessageCollectionViewCellDefaultStyle { // Default values
static public func createDefaultBubbleMasks() -> BubbleMasks { static public func createDefaultBubbleMasks() -> BubbleMasks {
return BubbleMasks( return BubbleMasks(
incomingTail: UIImage(named: "bubble-incoming-tail", 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", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!, incomingNoTail: UIImage(named: "bubble-incoming", in: Bundle(for: Class.self), compatibleWith: nil)!,
outgoingTail: UIImage(named: "bubble-outgoing-tail", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!, outgoingTail: UIImage(named: "bubble-outgoing-tail", in: Bundle(for: Class.self), compatibleWith: nil)!,
outgoingNoTail: UIImage(named: "bubble-outgoing", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!, outgoingNoTail: UIImage(named: "bubble-outgoing", in: Bundle(for: Class.self), compatibleWith: nil)!,
tailWidth: 6 tailWidth: 6
) )
} }
@ -198,8 +198,8 @@ public extension PhotoMessageCollectionViewCellDefaultStyle { // Default values
placeholderIconTintIncoming: UIColor.bma_color(rgb: 0xced6dc), placeholderIconTintIncoming: UIColor.bma_color(rgb: 0xced6dc),
placeholderIconTintOutgoing: UIColor.bma_color(rgb: 0x508dfc), placeholderIconTintOutgoing: UIColor.bma_color(rgb: 0x508dfc),
progressIndicatorColorIncoming: UIColor.bma_color(rgb: 0x98a3ab), progressIndicatorColorIncoming: UIColor.bma_color(rgb: 0x98a3ab),
progressIndicatorColorOutgoing: UIColor.whiteColor(), progressIndicatorColorOutgoing: UIColor.white,
overlayColor: UIColor.blackColor().colorWithAlphaComponent(0.70) overlayColor: UIColor.black.withAlphaComponent(0.70)
) )
} }
} }

View File

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

View File

@ -24,12 +24,12 @@
import UIKit import UIKit
public class TextMessagePresenter<ViewModelBuilderT, InteractionHandlerT where open class TextMessagePresenter<ViewModelBuilderT, InteractionHandlerT>
: BaseMessagePresenter<TextBubbleView, ViewModelBuilderT, InteractionHandlerT> where
ViewModelBuilderT: ViewModelBuilderProtocol, ViewModelBuilderT: ViewModelBuilderProtocol,
ViewModelBuilderT.ViewModelT: TextMessageViewModelProtocol, ViewModelBuilderT.ViewModelT: TextMessageViewModelProtocol,
InteractionHandlerT: BaseMessageInteractionHandlerProtocol, InteractionHandlerT: BaseMessageInteractionHandlerProtocol,
InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT> InteractionHandlerT.ViewModelT == ViewModelBuilderT.ViewModelT {
: BaseMessagePresenter<TextBubbleView, ViewModelBuilderT, InteractionHandlerT> {
public typealias ModelT = ViewModelBuilderT.ModelT public typealias ModelT = ViewModelBuilderT.ModelT
public typealias ViewModelT = ViewModelBuilderT.ViewModelT public typealias ViewModelT = ViewModelBuilderT.ViewModelT
@ -40,7 +40,7 @@ public class TextMessagePresenter<ViewModelBuilderT, InteractionHandlerT where
sizingCell: TextMessageCollectionViewCell, sizingCell: TextMessageCollectionViewCell,
baseCellStyle: BaseMessageCollectionViewCellStyleProtocol, baseCellStyle: BaseMessageCollectionViewCellStyleProtocol,
textCellStyle: TextMessageCollectionViewCellStyleProtocol, textCellStyle: TextMessageCollectionViewCellStyleProtocol,
layoutCache: NSCache) { layoutCache: NSCache<AnyObject, AnyObject>) {
self.layoutCache = layoutCache self.layoutCache = layoutCache
self.textCellStyle = textCellStyle self.textCellStyle = textCellStyle
super.init( super.init(
@ -52,20 +52,40 @@ public class TextMessagePresenter<ViewModelBuilderT, InteractionHandlerT where
) )
} }
let layoutCache: NSCache let layoutCache: NSCache<AnyObject, AnyObject>
let textCellStyle: TextMessageCollectionViewCellStyleProtocol let textCellStyle: TextMessageCollectionViewCellStyleProtocol
public override class func registerCells(collectionView: UICollectionView) { public final override class func registerCells(_ collectionView: UICollectionView) {
collectionView.registerClass(TextMessageCollectionViewCell.self, forCellWithReuseIdentifier: "text-message-incoming") collectionView.register(TextMessageCollectionViewCell.self, forCellWithReuseIdentifier: "text-message-incoming")
collectionView.registerClass(TextMessageCollectionViewCell.self, forCellWithReuseIdentifier: "text-message-outcoming") 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" 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 configureCell(cell: BaseMessageCollectionViewCell<TextBubbleView>, decorationAttributes: ChatItemDecorationAttributes, animated: Bool, additionalConfiguration: (() -> Void)?) { open override func createViewModel() -> ViewModelBuilderT.ViewModelT {
let viewModel = self.viewModelBuilder.createViewModel(self.messageModel)
let updateClosure = { [weak self] (old: Any, new: Any) -> () in
self?.updateCurrentCell()
}
viewModel.avatarImage.observe(self, closure: updateClosure)
return viewModel
}
public var textCell: TextMessageCollectionViewCell? {
if let cell = self.cell {
if let textCell = cell as? TextMessageCollectionViewCell {
return textCell
} else {
assert(false, "Invalid cell was given to presenter!")
}
}
return nil
}
open override func configureCell(_ cell: BaseMessageCollectionViewCell<TextBubbleView>, decorationAttributes: ChatItemDecorationAttributes, animated: Bool, additionalConfiguration: (() -> Void)?) {
guard let cell = cell as? TextMessageCollectionViewCell else { guard let cell = cell as? TextMessageCollectionViewCell else {
assert(false, "Invalid cell received") assert(false, "Invalid cell received")
return return
@ -79,17 +99,25 @@ public class TextMessagePresenter<ViewModelBuilderT, InteractionHandlerT where
} }
} }
public override func canShowMenu() -> Bool { public func updateCurrentCell() {
if let cell = self.textCell, let decorationAttributes = self.decorationAttributes {
self.configureCell(cell, decorationAttributes: decorationAttributes, animated: self.itemVisibility != .appearing, additionalConfiguration: nil)
}
}
open override func canShowMenu() -> Bool {
return true return true
} }
public override func canPerformMenuControllerAction(action: Selector) -> Bool { open override func canPerformMenuControllerAction(_ action: Selector) -> Bool {
return action == #selector(NSObject.copy(_:)) let selector = #selector(UIResponderStandardEditActions.copy(_:))
return action == selector
} }
public override func performMenuControllerAction(action: Selector) { open override func performMenuControllerAction(_ action: Selector) {
if action == #selector(NSObject.copy(_:)) { let selector = #selector(UIResponderStandardEditActions.copy(_:))
UIPasteboard.generalPasteboard().string = self.messageViewModel.text if action == selector {
UIPasteboard.general.string = self.messageViewModel.text
} else { } else {
assert(false, "Unexpected action") assert(false, "Unexpected action")
} }

View File

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

View File

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

View File

@ -25,25 +25,25 @@
import UIKit import UIKit
public protocol TextBubbleViewStyleProtocol { public protocol TextBubbleViewStyleProtocol {
func bubbleImage(viewModel viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIImage func bubbleImage(viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIImage
func bubbleImageBorder(viewModel viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIImage? func bubbleImageBorder(viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIImage?
func textFont(viewModel viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIFont func textFont(viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIFont
func textColor(viewModel viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIColor func textColor(viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIColor
func textInsets(viewModel viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIEdgeInsets func textInsets(viewModel: TextMessageViewModelProtocol, isSelected: Bool) -> UIEdgeInsets
} }
public final class TextBubbleView: UIView, MaximumLayoutWidthSpecificable, BackgroundSizingQueryable { public final class TextBubbleView: UIView, MaximumLayoutWidthSpecificable, BackgroundSizingQueryable {
public var preferredMaxLayoutWidth: CGFloat = 0 public var preferredMaxLayoutWidth: CGFloat = 0
public var animationDuration: CFTimeInterval = 0.33 public var animationDuration: CFTimeInterval = 0.33
public var viewContext: ViewContext = .Normal { public var viewContext: ViewContext = .normal {
didSet { didSet {
if self.viewContext == .Sizing { if self.viewContext == .sizing {
self.textView.dataDetectorTypes = .None self.textView.dataDetectorTypes = UIDataDetectorTypes()
self.textView.selectable = false self.textView.isSelectable = false
} else { } else {
self.textView.dataDetectorTypes = .All self.textView.dataDetectorTypes = .all
self.textView.selectable = true self.textView.isSelectable = true
} }
} }
} }
@ -93,25 +93,25 @@ public final class TextBubbleView: UIView, MaximumLayoutWidthSpecificable, Backg
private var textView: UITextView = { private var textView: UITextView = {
let textView = ChatMessageTextView() let textView = ChatMessageTextView()
UIView.performWithoutAnimation({ () -> Void in // fixes iOS 8 blinking when cell appears UIView.performWithoutAnimation({ () -> Void in // fixes iOS 8 blinking when cell appears
textView.backgroundColor = UIColor.clearColor() textView.backgroundColor = UIColor.clear
}) })
textView.editable = false textView.isEditable = false
textView.selectable = true textView.isSelectable = true
textView.dataDetectorTypes = .All textView.dataDetectorTypes = .all
textView.scrollsToTop = false textView.scrollsToTop = false
textView.scrollEnabled = false textView.isScrollEnabled = false
textView.bounces = false textView.bounces = false
textView.bouncesZoom = false textView.bouncesZoom = false
textView.showsHorizontalScrollIndicator = false textView.showsHorizontalScrollIndicator = false
textView.showsVerticalScrollIndicator = false textView.showsVerticalScrollIndicator = false
textView.layoutManager.allowsNonContiguousLayout = true textView.layoutManager.allowsNonContiguousLayout = true
textView.exclusiveTouch = true textView.isExclusiveTouch = true
textView.textContainer.lineFragmentPadding = 0 textView.textContainer.lineFragmentPadding = 0
return textView return textView
}() }()
public private(set) var isUpdating: Bool = false 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 self.isUpdating = true
let updateAndRefreshViews = { let updateAndRefreshViews = {
updateClosure() updateClosure()
@ -122,7 +122,7 @@ public final class TextBubbleView: UIView, MaximumLayoutWidthSpecificable, Backg
} }
} }
if animated { if animated {
UIView.animateWithDuration(self.animationDuration, animations: updateAndRefreshViews, completion: { (finished) -> Void in UIView.animate(withDuration: self.animationDuration, animations: updateAndRefreshViews, completion: { (finished) -> Void in
completion?() completion?()
}) })
} else { } else {
@ -131,7 +131,7 @@ public final class TextBubbleView: UIView, MaximumLayoutWidthSpecificable, Backg
} }
private func updateViews() { private func updateViews() {
if self.viewContext == .Sizing { return } if self.viewContext == .sizing { return }
if isUpdating { return } if isUpdating { return }
guard let style = self.style else { return } guard let style = self.style else { return }
@ -143,7 +143,7 @@ public final class TextBubbleView: UIView, MaximumLayoutWidthSpecificable, Backg
} }
private func updateTextView() { 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 font = style.textFont(viewModel: viewModel, isSelected: self.selected)
let textColor = style.textColor(viewModel: viewModel, isSelected: self.selected) let textColor = style.textColor(viewModel: viewModel, isSelected: self.selected)
@ -159,12 +159,12 @@ public final class TextBubbleView: UIView, MaximumLayoutWidthSpecificable, Backg
self.textView.textColor = textColor self.textView.textColor = textColor
self.textView.linkTextAttributes = [ self.textView.linkTextAttributes = [
NSForegroundColorAttributeName: textColor, NSForegroundColorAttributeName: textColor,
NSUnderlineStyleAttributeName : NSUnderlineStyle.StyleSingle.rawValue NSUnderlineStyleAttributeName : NSUnderlineStyle.styleSingle.rawValue
] ]
needsToUpdateText = true needsToUpdateText = true
} }
if needsToUpdateText || self.textView != viewModel.text { if needsToUpdateText || self.textView.text != viewModel.text {
self.textView.text = viewModel.text self.textView.text = viewModel.text
} }
@ -176,7 +176,7 @@ public final class TextBubbleView: UIView, MaximumLayoutWidthSpecificable, Backg
return self.style.bubbleImage(viewModel: self.textMessageViewModel, isSelected: self.selected) 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 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 self.borderImageView.bma_rect = self.bubbleImageView.bounds
} }
public var layoutCache: NSCache! public var layoutCache: NSCache<AnyObject, AnyObject>!
private func calculateTextBubbleLayout(preferredMaxLayoutWidth preferredMaxLayoutWidth: CGFloat) -> TextBubbleLayoutModel { private func calculateTextBubbleLayout(preferredMaxLayoutWidth: CGFloat) -> TextBubbleLayoutModel {
let layoutContext = TextBubbleLayoutModel.LayoutContext( let layoutContext = TextBubbleLayoutModel.LayoutContext(
text: self.textMessageViewModel.text, text: self.textMessageViewModel.text,
font: self.style.textFont(viewModel: self.textMessageViewModel, isSelected: self.selected), font: self.style.textFont(viewModel: self.textMessageViewModel, isSelected: self.selected),
@ -198,14 +198,14 @@ public final class TextBubbleView: UIView, MaximumLayoutWidthSpecificable, Backg
preferredMaxLayoutWidth: preferredMaxLayoutWidth 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 return layoutModel
} }
let layoutModel = TextBubbleLayoutModel(layoutContext: layoutContext) let layoutModel = TextBubbleLayoutModel(layoutContext: layoutContext)
layoutModel.calculateLayout() layoutModel.calculateLayout()
self.layoutCache.setObject(layoutModel, forKey: layoutContext.hashValue) self.layoutCache.setObject(layoutModel, forKey: layoutContext.hashValue as AnyObject)
return layoutModel return layoutModel
} }
@ -214,7 +214,6 @@ public final class TextBubbleView: UIView, MaximumLayoutWidthSpecificable, Backg
} }
} }
private final class TextBubbleLayoutModel { private final class TextBubbleLayoutModel {
let layoutContext: LayoutContext let layoutContext: LayoutContext
var textFrame: CGRect = CGRect.zero 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 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() { func calculateLayout() {
@ -248,9 +253,9 @@ private final class TextBubbleLayoutModel {
self.size = bubbleSize self.size = bubbleSize
} }
private func textSizeThatFitsWidth(width: CGFloat) -> CGSize { private func textSizeThatFitsWidth(_ width: CGFloat) -> CGSize {
let textContainer: NSTextContainer = { let textContainer: NSTextContainer = {
let size = CGSize(width: width, height: .max) let size = CGSize(width: width, height: .greatestFiniteMagnitude)
let container = NSTextContainer(size: size) let container = NSTextContainer(size: size)
container.lineFragmentPadding = 0 container.lineFragmentPadding = 0
return container return container
@ -264,7 +269,7 @@ private final class TextBubbleLayoutModel {
return layoutManager return layoutManager
}() }()
let rect = layoutManager.usedRectForTextContainer(textContainer) let rect = layoutManager.usedRect(for: textContainer)
return rect.size.bma_round() 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... /// UITextView with hacks to avoid selection, loupe, define...
private final class ChatMessageTextView: UITextView { private final class ChatMessageTextView: UITextView {
override func canBecomeFirstResponder() -> Bool { override var canBecomeFirstResponder: Bool {
return false return false
} }
override func addGestureRecognizer(gestureRecognizer: UIGestureRecognizer) { override func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
if gestureRecognizer.dynamicType == UILongPressGestureRecognizer.self && gestureRecognizer.delaysTouchesEnded { if type(of: gestureRecognizer) == UILongPressGestureRecognizer.self && gestureRecognizer.delaysTouchesEnded {
super.addGestureRecognizer(gestureRecognizer) super.addGestureRecognizer(gestureRecognizer)
} }
} }
override func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool { override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return false return false
} }

View File

@ -30,21 +30,17 @@ public final class TextMessageCollectionViewCell: BaseMessageCollectionViewCell<
public static func sizingCell() -> TextMessageCollectionViewCell { public static func sizingCell() -> TextMessageCollectionViewCell {
let cell = TextMessageCollectionViewCell(frame: CGRect.zero) let cell = TextMessageCollectionViewCell(frame: CGRect.zero)
cell.viewContext = .Sizing cell.viewContext = .sizing
return cell return cell
} }
override init(frame: CGRect) {
super.init(frame: frame)
}
// MARK: Subclassing (view creation) // MARK: Subclassing (view creation)
public override func createBubbleView() -> TextBubbleView { public override func createBubbleView() -> TextBubbleView {
return 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 super.performBatchUpdates({ () -> Void in
self.bubbleView.performBatchUpdates(updateClosure, animated: false, completion: nil) self.bubbleView.performBatchUpdates(updateClosure, animated: false, completion: nil)
}, animated: animated, completion: completion) }, animated: animated, completion: completion)
@ -71,13 +67,13 @@ public final class TextMessageCollectionViewCell: BaseMessageCollectionViewCell<
} }
} }
override public var selected: Bool { override public var isSelected: Bool {
didSet { didSet {
self.bubbleView.selected = self.selected self.bubbleView.selected = self.isSelected
} }
} }
public var layoutCache: NSCache! { public var layoutCache: NSCache<AnyObject, AnyObject>! {
didSet { didSet {
self.bubbleView.layoutCache = self.layoutCache self.bubbleView.layoutCache = self.layoutCache
} }

View File

@ -24,7 +24,7 @@
import UIKit import UIKit
public class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionViewCellStyleProtocol { open class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionViewCellStyleProtocol {
typealias Class = TextMessageCollectionViewCellDefaultStyle typealias Class = TextMessageCollectionViewCellDefaultStyle
public struct BubbleImages { public struct BubbleImages {
@ -33,10 +33,10 @@ public class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionVie
let outgoingTail: () -> UIImage let outgoingTail: () -> UIImage
let outgoingNoTail: () -> UIImage let outgoingNoTail: () -> UIImage
public init( public init(
@autoclosure(escaping) incomingTail: () -> UIImage, incomingTail: @autoclosure @escaping () -> UIImage,
@autoclosure(escaping) incomingNoTail: () -> UIImage, incomingNoTail: @autoclosure @escaping () -> UIImage,
@autoclosure(escaping) outgoingTail: () -> UIImage, outgoingTail: @autoclosure @escaping () -> UIImage,
@autoclosure(escaping) outgoingNoTail: () -> UIImage) { outgoingNoTail: @autoclosure @escaping () -> UIImage) {
self.incomingTail = incomingTail self.incomingTail = incomingTail
self.incomingNoTail = incomingNoTail self.incomingNoTail = incomingNoTail
self.outgoingTail = outgoingTail self.outgoingTail = outgoingTail
@ -51,9 +51,9 @@ public class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionVie
let incomingInsets: UIEdgeInsets let incomingInsets: UIEdgeInsets
let outgoingInsets: UIEdgeInsets let outgoingInsets: UIEdgeInsets
public init( public init(
@autoclosure(escaping) font: () -> UIFont, font: @autoclosure @escaping () -> UIFont,
@autoclosure(escaping) incomingColor: () -> UIColor, incomingColor: @autoclosure @escaping () -> UIColor,
@autoclosure(escaping) outgoingColor: () -> UIColor, outgoingColor: @autoclosure @escaping () -> UIColor,
incomingInsets: UIEdgeInsets, incomingInsets: UIEdgeInsets,
outgoingInsets: UIEdgeInsets) { outgoingInsets: UIEdgeInsets) {
self.font = font self.font = font
@ -64,10 +64,9 @@ public class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionVie
} }
} }
let bubbleImages: BubbleImages public let bubbleImages: BubbleImages
let textStyle: TextStyle public let textStyle: TextStyle
let baseStyle: BaseMessageCollectionViewCellDefaultStyle public let baseStyle: BaseMessageCollectionViewCellDefaultStyle
public init ( public init (
bubbleImages: BubbleImages = Class.createDefaultBubbleImages(), bubbleImages: BubbleImages = Class.createDefaultBubbleImages(),
textStyle: TextStyle = Class.createDefaultTextStyle(), textStyle: TextStyle = Class.createDefaultTextStyle(),
@ -90,23 +89,23 @@ public class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionVie
lazy var incomingColor: UIColor = self.textStyle.incomingColor() lazy var incomingColor: UIColor = self.textStyle.incomingColor()
lazy var outgoingColor: UIColor = self.textStyle.outgoingColor() 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 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 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 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) 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) let key = ImageKey.normal(isIncoming: viewModel.isIncoming, status: viewModel.status, showsTail: viewModel.showsTail, isSelected: isSelected)
if let image = self.images[key] { if let image = self.images[key] {
@ -124,18 +123,18 @@ public class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionVie
return UIImage() return UIImage()
} }
private 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 var color = isIncoming ? self.baseStyle.baseColorIncoming : self.baseStyle.baseColorOutgoing
switch status { switch status {
case .Success: case .success:
break break
case .Failed, .Sending: case .failed, .sending:
color = color.bma_blendWithColor(UIColor.whiteColor().colorWithAlphaComponent(0.70)) color = color.bma_blendWithColor(UIColor.white.withAlphaComponent(0.70))
} }
if isSelected { 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) return image.bma_tintWithColor(color)
@ -155,12 +154,10 @@ public class TextMessageCollectionViewCellDefaultStyle: TextMessageCollectionVie
} }
private func calculateHash(withHashValues hashes: [Int]) -> Int { 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 { static func == (lhs: TextMessageCollectionViewCellDefaultStyle.ImageKey, rhs: TextMessageCollectionViewCellDefaultStyle.ImageKey) -> Bool {
switch (lhs, rhs) { switch (lhs, rhs) {
case let (.template(lhsValues), .template(rhsValues)): case let (.template(lhsValues), .template(rhsValues)):
return lhsValues == rhsValues return lhsValues == rhsValues
@ -170,23 +167,25 @@ private func == (lhs: TextMessageCollectionViewCellDefaultStyle.ImageKey, rhs: T
return false return false
} }
} }
}
}
public extension TextMessageCollectionViewCellDefaultStyle { // Default values public extension TextMessageCollectionViewCellDefaultStyle { // Default values
static public func createDefaultBubbleImages() -> BubbleImages { static public func createDefaultBubbleImages() -> BubbleImages {
return BubbleImages( return BubbleImages(
incomingTail: UIImage(named: "bubble-incoming-tail", 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", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!, incomingNoTail: UIImage(named: "bubble-incoming", in: Bundle(for: Class.self), compatibleWith: nil)!,
outgoingTail: UIImage(named: "bubble-outgoing-tail", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!, outgoingTail: UIImage(named: "bubble-outgoing-tail", in: Bundle(for: Class.self), compatibleWith: nil)!,
outgoingNoTail: UIImage(named: "bubble-outgoing", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)! outgoingNoTail: UIImage(named: "bubble-outgoing", in: Bundle(for: Class.self), compatibleWith: nil)!
) )
} }
static public func createDefaultTextStyle() -> TextStyle { static public func createDefaultTextStyle() -> TextStyle {
return TextStyle( return TextStyle(
font: UIFont.systemFontOfSize(16), font: UIFont.systemFont(ofSize: 16),
incomingColor: UIColor.blackColor(), incomingColor: UIColor.black,
outgoingColor: UIColor.whiteColor(), outgoingColor: UIColor.white,
incomingInsets: UIEdgeInsets(top: 10, left: 19, bottom: 10, right: 15), incomingInsets: UIEdgeInsets(top: 10, left: 19, bottom: 10, right: 15),
outgoingInsets: UIEdgeInsets(top: 10, left: 15, bottom: 10, right: 19) outgoingInsets: UIEdgeInsets(top: 10, left: 15, bottom: 10, right: 19)
) )

View File

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

View File

@ -25,17 +25,17 @@
import UIKit import UIKit
public protocol ChatInputBarDelegate: class { public protocol ChatInputBarDelegate: class {
func inputBarShouldBeginTextEditing(inputBar: ChatInputBar) -> Bool func inputBarShouldBeginTextEditing(_ inputBar: ChatInputBar) -> Bool
func inputBarDidBeginEditing(inputBar: ChatInputBar) func inputBarDidBeginEditing(_ inputBar: ChatInputBar)
func inputBarDidEndEditing(inputBar: ChatInputBar) func inputBarDidEndEditing(_ inputBar: ChatInputBar)
func inputBarDidChangeText(inputBar: ChatInputBar) func inputBarDidChangeText(_ inputBar: ChatInputBar)
func inputBarSendButtonPressed(inputBar: ChatInputBar) func inputBarSendButtonPressed(_ inputBar: ChatInputBar)
func inputBar(inputBar: ChatInputBar, shouldFocusOnItem item: ChatInputItemProtocol) -> Bool func inputBar(_ inputBar: ChatInputBar, shouldFocusOnItem item: ChatInputItemProtocol) -> Bool
func inputBar(inputBar: ChatInputBar, didReceiveFocusOnItem item: ChatInputItemProtocol) func inputBar(_ inputBar: ChatInputBar, didReceiveFocusOnItem item: ChatInputItemProtocol)
} }
@objc @objc
public class ChatInputBar: ReusableXibView { open class ChatInputBar: ReusableXibView {
public weak var delegate: ChatInputBarDelegate? public weak var delegate: ChatInputBarDelegate?
weak var presenter: ChatInputBarPresenter? weak var presenter: ChatInputBarPresenter?
@ -56,8 +56,8 @@ public class ChatInputBar: ReusableXibView {
@IBOutlet var constraintsForHiddenSendButton: [NSLayoutConstraint]! @IBOutlet var constraintsForHiddenSendButton: [NSLayoutConstraint]!
@IBOutlet var tabBarContainerHeightConstraint: NSLayoutConstraint! @IBOutlet var tabBarContainerHeightConstraint: NSLayoutConstraint!
class public func loadNib() -> ChatInputBar { class open func loadNib() -> ChatInputBar {
let view = NSBundle(forClass: self).loadNibNamed(self.nibName(), owner: nil, options: nil)!.first as! ChatInputBar let view = Bundle(for: self).loadNibNamed(self.nibName(), owner: nil, options: nil)!.first as! ChatInputBar
view.translatesAutoresizingMaskIntoConstraints = false view.translatesAutoresizingMaskIntoConstraints = false
view.frame = CGRect.zero view.frame = CGRect.zero
return view return view
@ -67,34 +67,34 @@ public class ChatInputBar: ReusableXibView {
return "ChatInputBar" return "ChatInputBar"
} }
public override func awakeFromNib() { open override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()
self.topBorderHeightConstraint.constant = 1 / UIScreen.mainScreen().scale self.topBorderHeightConstraint.constant = 1 / UIScreen.main.scale
self.textView.scrollsToTop = false self.textView.scrollsToTop = false
self.textView.delegate = self self.textView.delegate = self
self.scrollView.scrollsToTop = false self.scrollView.scrollsToTop = false
self.sendButton.enabled = false self.sendButton.isEnabled = false
} }
public override func updateConstraints() { open override func updateConstraints() {
if self.showsTextView { if self.showsTextView {
NSLayoutConstraint.activateConstraints(self.constraintsForVisibleTextView) NSLayoutConstraint.activate(self.constraintsForVisibleTextView)
NSLayoutConstraint.deactivateConstraints(self.constraintsForHiddenTextView) NSLayoutConstraint.deactivate(self.constraintsForHiddenTextView)
} else { } else {
NSLayoutConstraint.deactivateConstraints(self.constraintsForVisibleTextView) NSLayoutConstraint.deactivate(self.constraintsForVisibleTextView)
NSLayoutConstraint.activateConstraints(self.constraintsForHiddenTextView) NSLayoutConstraint.activate(self.constraintsForHiddenTextView)
} }
if self.showsSendButton { if self.showsSendButton {
NSLayoutConstraint.deactivateConstraints(self.constraintsForHiddenSendButton) NSLayoutConstraint.deactivate(self.constraintsForHiddenSendButton)
NSLayoutConstraint.activateConstraints(self.constraintsForVisibleSendButton) NSLayoutConstraint.activate(self.constraintsForVisibleSendButton)
} else { } else {
NSLayoutConstraint.deactivateConstraints(self.constraintsForVisibleSendButton) NSLayoutConstraint.deactivate(self.constraintsForVisibleSendButton)
NSLayoutConstraint.activateConstraints(self.constraintsForHiddenSendButton) NSLayoutConstraint.activate(self.constraintsForHiddenSendButton)
} }
super.updateConstraints() super.updateConstraints()
} }
public var showsTextView: Bool = true { open var showsTextView: Bool = true {
didSet { didSet {
self.setNeedsUpdateConstraints() self.setNeedsUpdateConstraints()
self.setNeedsLayout() self.setNeedsLayout()
@ -102,7 +102,7 @@ public class ChatInputBar: ReusableXibView {
} }
} }
public var showsSendButton: Bool = true { open var showsSendButton: Bool = true {
didSet { didSet {
self.setNeedsUpdateConstraints() self.setNeedsUpdateConstraints()
self.setNeedsLayout() self.setNeedsLayout()
@ -113,15 +113,15 @@ public class ChatInputBar: ReusableXibView {
public var maxCharactersCount: UInt? // nil -> unlimited public var maxCharactersCount: UInt? // nil -> unlimited
private func updateIntrinsicContentSizeAnimated() { private func updateIntrinsicContentSizeAnimated() {
let options: UIViewAnimationOptions = [.BeginFromCurrentState, .AllowUserInteraction, .CurveEaseInOut] let options: UIViewAnimationOptions = [.beginFromCurrentState, .allowUserInteraction]
UIView.animateWithDuration(0.25, delay: 0, options: options, animations: { () -> Void in UIView.animate(withDuration: 0.25, delay: 0, options: options, animations: { () -> Void in
self.invalidateIntrinsicContentSize() self.invalidateIntrinsicContentSize()
self.layoutIfNeeded() self.layoutIfNeeded()
self.superview?.layoutIfNeeded() self.superview?.layoutIfNeeded()
}, completion: nil) }, 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 self.updateConstraints() // Interface rotation or size class changes will reset constraints as defined in interface builder -> constraintsForVisibleTextView will be activated
super.layoutSubviews() super.layoutSubviews()
} }
@ -138,10 +138,10 @@ public class ChatInputBar: ReusableXibView {
} }
} }
public func becomeFirstResponderWithInputView(inputView: UIView?) { open func becomeFirstResponderWithInputView(_ inputView: UIView?) {
self.textView.inputView = inputView self.textView.inputView = inputView
if self.textView.isFirstResponder() { if self.textView.isFirstResponder {
self.textView.reloadInputViews() self.textView.reloadInputViews()
} else { } else {
self.textView.becomeFirstResponder() self.textView.becomeFirstResponder()
@ -158,23 +158,27 @@ public class ChatInputBar: ReusableXibView {
} }
} }
private func updateSendButton() { fileprivate func updateSendButton() {
self.sendButton.enabled = self.shouldEnableSendButton(self) self.sendButton.isEnabled = self.shouldEnableSendButton(self)
} }
@IBAction func buttonTapped(sender: AnyObject) { @IBAction func buttonTapped(_ sender: AnyObject) {
self.presenter?.onSendButtonPressed() self.presenter?.onSendButtonPressed()
self.delegate?.inputBarSendButtonPressed(self) self.delegate?.inputBarSendButtonPressed(self)
} }
public func setTextViewPlaceholderAccessibilityIdentifer(_ accessibilityIdentifer: String) {
self.textView.setTextPlaceholderAccessibilityIdentifier(accessibilityIdentifer)
}
} }
// MARK: - ChatInputItemViewDelegate // MARK: - ChatInputItemViewDelegate
extension ChatInputBar: ChatInputItemViewDelegate { extension ChatInputBar: ChatInputItemViewDelegate {
func inputItemViewTapped(view: ChatInputItemView) { func inputItemViewTapped(_ view: ChatInputItemView) {
self.focusOnInputItem(view.inputItem) self.focusOnInputItem(view.inputItem)
} }
public func focusOnInputItem(inputItem: ChatInputItemProtocol) { public func focusOnInputItem(_ inputItem: ChatInputItemProtocol) {
let shouldFocus = self.delegate?.inputBar(self, shouldFocusOnItem: inputItem) ?? true let shouldFocus = self.delegate?.inputBar(self, shouldFocusOnItem: inputItem) ?? true
guard shouldFocus else { return } guard shouldFocus else { return }
@ -185,7 +189,7 @@ extension ChatInputBar: ChatInputItemViewDelegate {
// MARK: - ChatInputBarAppearance // MARK: - ChatInputBarAppearance
extension ChatInputBar { extension ChatInputBar {
public func setAppearance(appearance: ChatInputBarAppearance) { public func setAppearance(_ appearance: ChatInputBarAppearance) {
self.textView.font = appearance.textInputAppearance.font self.textView.font = appearance.textInputAppearance.font
self.textView.textColor = appearance.textInputAppearance.textColor self.textView.textColor = appearance.textInputAppearance.textColor
self.textView.textContainerInset = appearance.textInputAppearance.textInsets self.textView.textContainerInset = appearance.textInputAppearance.textInsets
@ -195,9 +199,9 @@ extension ChatInputBar {
self.tabBarInterItemSpacing = appearance.tabBarAppearance.interItemSpacing self.tabBarInterItemSpacing = appearance.tabBarAppearance.interItemSpacing
self.tabBarContentInsets = appearance.tabBarAppearance.contentInsets self.tabBarContentInsets = appearance.tabBarAppearance.contentInsets
self.sendButton.contentEdgeInsets = appearance.sendButtonAppearance.insets 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 appearance.sendButtonAppearance.titleColors.forEach { (state, color) in
self.sendButton.setTitleColor(color, forState: state) self.sendButton.setTitleColor(color, for: state.controlState)
} }
self.sendButton.titleLabel?.font = appearance.sendButtonAppearance.font self.sendButton.titleLabel?.font = appearance.sendButtonAppearance.font
self.tabBarContainerHeightConstraint.constant = appearance.tabBarAppearance.height self.tabBarContainerHeightConstraint.constant = appearance.tabBarAppearance.height
@ -226,30 +230,30 @@ extension ChatInputBar { // Tabar
// MARK: UITextViewDelegate // MARK: UITextViewDelegate
extension ChatInputBar: UITextViewDelegate { extension ChatInputBar: UITextViewDelegate {
public func textViewShouldBeginEditing(textView: UITextView) -> Bool { public func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
return self.delegate?.inputBarShouldBeginTextEditing(self) ?? true return self.delegate?.inputBarShouldBeginTextEditing(self) ?? true
} }
public func textViewDidEndEditing(textView: UITextView) { public func textViewDidEndEditing(_ textView: UITextView) {
self.presenter?.onDidEndEditing() self.presenter?.onDidEndEditing()
self.delegate?.inputBarDidEndEditing(self) self.delegate?.inputBarDidEndEditing(self)
} }
public func textViewDidBeginEditing(textView: UITextView) { public func textViewDidBeginEditing(_ textView: UITextView) {
self.presenter?.onDidBeginEditing() self.presenter?.onDidBeginEditing()
self.delegate?.inputBarDidBeginEditing(self) self.delegate?.inputBarDidBeginEditing(self)
} }
public func textViewDidChange(textView: UITextView) { public func textViewDidChange(_ textView: UITextView) {
self.updateSendButton() self.updateSendButton()
self.delegate?.inputBarDidChangeText(self) 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) let range = self.textView.text.bma_rangeFromNSRange(nsRange)
if let maxCharactersCount = self.maxCharactersCount { if let maxCharactersCount = self.maxCharactersCount {
let currentCount = textView.text.characters.count 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 let nextCount = currentCount - rangeLength + text.characters.count
return UInt(nextCount) <= maxCharactersCount return UInt(nextCount) <= maxCharactersCount
} }
@ -258,12 +262,13 @@ extension ChatInputBar: UITextViewDelegate {
} }
private extension String { private extension String {
func bma_rangeFromNSRange(nsRange: NSRange) -> Range<String.Index> { func bma_rangeFromNSRange(_ nsRange: NSRange) -> Range<String.Index> {
let from16 = self.utf16.startIndex.advancedBy(nsRange.location, limit: self.utf16.endIndex) guard
let to16 = from16.advancedBy(nsRange.length, limit: self.utf16.endIndex) let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex),
if let from = String.Index(from16, within: self), to = String.Index(to16, within: self) { 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 return from ..< to
} }
return self.startIndex...self.startIndex
}
} }

View File

@ -24,13 +24,13 @@
public struct ChatInputBarAppearance { public struct ChatInputBarAppearance {
public struct SendButtonAppearance { 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 insets = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
public var title = "" public var title = ""
public var titleColors: [UIControlState: UIColor] = [ public var titleColors: [UIControlStateWrapper: UIColor] = [
.Disabled: UIColor.bma_color(rgb: 0x9AA3AB), UIControlStateWrapper(state: .disabled): UIColor.bma_color(rgb: 0x9AA3AB),
.Normal: UIColor.bma_color(rgb: 0x007AFF), UIControlStateWrapper(state: .normal): UIColor.bma_color(rgb: 0x007AFF),
.Highlighted: UIColor.bma_color(rgb: 0x007AFF).bma_blendWithColor(UIColor.whiteColor().colorWithAlphaComponent(0.4)) 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 struct TextInputAppearance {
public var font = UIFont.systemFontOfSize(12) public var font = UIFont.systemFont(ofSize: 12)
public var textColor = UIColor.blackColor() public var textColor = UIColor.black
public var placeholderFont = UIFont.systemFontOfSize(12) public var placeholderFont = UIFont.systemFont(ofSize: 12)
public var placeholderColor = UIColor.grayColor() public var placeholderColor = UIColor.gray
public var placeholderText = "" public var placeholderText = ""
public var textInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0) public var textInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
} }
@ -56,9 +56,20 @@ public struct ChatInputBarAppearance {
public init() {} public init() {}
} }
// Workaround for SR-2223
public struct UIControlStateWrapper: Hashable {
public let controlState: UIControlState
public init(state: UIControlState) {
self.controlState = state
}
extension UIControlState: Hashable {
public var hashValue: Int { public var hashValue: Int {
return Int(self.rawValue) return Int(self.controlState.rawValue)
} }
} }
public func == (lhs: UIControlStateWrapper, rhs: UIControlStateWrapper) -> Bool {
return lhs.controlState == rhs.controlState
}

View File

@ -29,18 +29,19 @@ protocol ChatInputBarPresenter: class {
func onDidBeginEditing() func onDidBeginEditing()
func onDidEndEditing() func onDidEndEditing()
func onSendButtonPressed() 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 chatInputBar: ChatInputBar
let chatInputItems: [ChatInputItemProtocol] let chatInputItems: [ChatInputItemProtocol]
let notificationCenter: NSNotificationCenter let notificationCenter: NotificationCenter
public init(chatInputBar: ChatInputBar, public init(chatInputBar: ChatInputBar,
chatInputItems: [ChatInputItemProtocol], chatInputItems: [ChatInputItemProtocol],
chatInputBarAppearance: ChatInputBarAppearance, chatInputBarAppearance: ChatInputBarAppearance,
notificationCenter: NSNotificationCenter = NSNotificationCenter.defaultCenter()) { notificationCenter: NotificationCenter = NotificationCenter.default) {
self.chatInputBar = chatInputBar self.chatInputBar = chatInputBar
self.chatInputItems = chatInputItems self.chatInputItems = chatInputItems
self.chatInputBar.setAppearance(chatInputBarAppearance) self.chatInputBar.setAppearance(chatInputBarAppearance)
@ -49,16 +50,16 @@ protocol ChatInputBarPresenter: class {
self.chatInputBar.presenter = self self.chatInputBar.presenter = self
self.chatInputBar.inputItems = self.chatInputItems self.chatInputBar.inputItems = self.chatInputItems
self.notificationCenter.addObserver(self, selector: #selector(BasicChatInputBarPresenter.keyboardDidChangeFrame), name: UIKeyboardDidChangeFrameNotification, 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: UIKeyboardWillHideNotification, 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: UIKeyboardWillShowNotification, object: nil) self.notificationCenter.addObserver(self, selector: #selector(BasicChatInputBarPresenter.keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
} }
deinit { deinit {
self.notificationCenter.removeObserver(self) self.notificationCenter.removeObserver(self)
} }
private(set) var focusedItem: ChatInputItemProtocol? { fileprivate(set) var focusedItem: ChatInputItemProtocol? {
willSet { willSet {
self.focusedItem?.selected = false self.focusedItem?.selected = false
} }
@ -67,11 +68,11 @@ protocol ChatInputBarPresenter: class {
} }
} }
private func updateFirstResponderWithInputItem(inputItem: ChatInputItemProtocol) { fileprivate func updateFirstResponderWithInputItem(_ inputItem: ChatInputItemProtocol) {
let responder = self.chatInputBar.textView let responder = self.chatInputBar.textView!
let inputView = inputItem.inputView let inputView = inputItem.inputView
responder.inputView = inputView responder.inputView = inputView
if responder.isFirstResponder() { if responder.isFirstResponder {
self.setHeight(forInputView: inputView) self.setHeight(forInputView: inputView)
responder.reloadInputViews() responder.reloadInputViews()
} else { } else {
@ -79,10 +80,10 @@ protocol ChatInputBarPresenter: class {
} }
} }
private func firstKeyboardInputItem() -> ChatInputItemProtocol? { fileprivate func firstKeyboardInputItem() -> ChatInputItemProtocol? {
var firstKeyboardInputItem: ChatInputItemProtocol? = nil var firstKeyboardInputItem: ChatInputItemProtocol? = nil
for inputItem in self.chatInputItems { for inputItem in self.chatInputItems {
if inputItem.presentationMode == .Keyboard { if inputItem.presentationMode == .keyboard {
firstKeyboardInputItem = inputItem firstKeyboardInputItem = inputItem
break break
} }
@ -97,13 +98,13 @@ protocol ChatInputBarPresenter: class {
guard let keyboardHeight = self.lastKnownKeyboardHeight else { return } guard let keyboardHeight = self.lastKnownKeyboardHeight else { return }
var mask = inputView.autoresizingMask var mask = inputView.autoresizingMask
mask.remove(.FlexibleHeight) mask.remove(.flexibleHeight)
inputView.autoresizingMask = mask inputView.autoresizingMask = mask
let accessoryViewHeight = self.chatInputBar.textView.inputAccessoryView?.bounds.height ?? 0 let accessoryViewHeight = self.chatInputBar.textView.inputAccessoryView?.bounds.height ?? 0
let inputViewHeight = keyboardHeight - accessoryViewHeight 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 heightConstraint.constant = inputViewHeight
} else { } else {
inputView.frame.size.height = inputViewHeight inputView.frame.size.height = inputViewHeight
@ -113,19 +114,19 @@ protocol ChatInputBarPresenter: class {
private var allowListenToChangeFrameEvents = true private var allowListenToChangeFrameEvents = true
@objc @objc
private func keyboardDidChangeFrame(notification: NSNotification) { private func keyboardDidChangeFrame(_ notification: Notification) {
guard self.allowListenToChangeFrameEvents else { return } guard self.allowListenToChangeFrameEvents else { return }
guard let value = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue else { return } guard let value = (notification as NSNotification).userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue else { return }
self.lastKnownKeyboardHeight = value.CGRectValue().height self.lastKnownKeyboardHeight = value.cgRectValue.height
} }
@objc @objc
private func keyboardWillHide(notification: NSNotification) { private func keyboardWillHide(_ notification: Notification) {
self.allowListenToChangeFrameEvents = false self.allowListenToChangeFrameEvents = false
} }
@objc @objc
private func keyboardWillShow(notification: NSNotification) { private func keyboardWillShow(_ notification: Notification) {
self.allowListenToChangeFrameEvents = true self.allowListenToChangeFrameEvents = true
} }
} }
@ -147,20 +148,20 @@ extension BasicChatInputBarPresenter {
func onSendButtonPressed() { func onSendButtonPressed() {
if let focusedItem = self.focusedItem { if let focusedItem = self.focusedItem {
focusedItem.handleInput(self.chatInputBar.inputText) focusedItem.handleInput(self.chatInputBar.inputText as AnyObject)
} else if let keyboardItem = self.firstKeyboardInputItem() { } else if let keyboardItem = self.firstKeyboardInputItem() {
keyboardItem.handleInput(self.chatInputBar.inputText) keyboardItem.handleInput(self.chatInputBar.inputText as AnyObject)
} }
self.chatInputBar.inputText = "" self.chatInputBar.inputText = ""
} }
func onDidReceiveFocusOnItem(item: ChatInputItemProtocol) { func onDidReceiveFocusOnItem(_ item: ChatInputItemProtocol) {
guard item.presentationMode != .None else { return } guard item.presentationMode != .none else { return }
guard item !== self.focusedItem else { return } guard item !== self.focusedItem else { return }
self.focusedItem = item self.focusedItem = item
self.chatInputBar.showsSendButton = item.showsSendButton self.chatInputBar.showsSendButton = item.showsSendButton
self.chatInputBar.showsTextView = item.presentationMode == .Keyboard self.chatInputBar.showsTextView = item.presentationMode == .keyboard
self.updateFirstResponderWithInputItem(item) self.updateFirstResponderWithInputItem(item)
} }
} }

View File

@ -25,9 +25,9 @@
import Foundation import Foundation
public enum ChatInputItemPresentationMode: UInt { public enum ChatInputItemPresentationMode: UInt {
case Keyboard case keyboard
case CustomView case customView
case None case none
} }
public protocol ChatInputItemProtocol: AnyObject { public protocol ChatInputItemProtocol: AnyObject {
@ -37,5 +37,5 @@ public protocol ChatInputItemProtocol: AnyObject {
var showsSendButton: Bool { get } var showsSendButton: Bool { get }
var selected: Bool { get set } var selected: Bool { get set }
func handleInput(input: AnyObject) func handleInput(_ input: AnyObject)
} }

View File

@ -25,7 +25,7 @@
import Foundation import Foundation
protocol ChatInputItemViewDelegate: class { protocol ChatInputItemViewDelegate: class {
func inputItemViewTapped(view: ChatInputItemView) func inputItemViewTapped(_ view: ChatInputItemView)
} }
class ChatInputItemView: UIView { class ChatInputItemView: UIView {
@ -72,7 +72,7 @@ extension ChatInputItemView {
self.inputItem.tabView.frame = self.bounds self.inputItem.tabView.frame = self.bounds
} }
override func intrinsicContentSize() -> CGSize { override var intrinsicContentSize: CGSize {
return self.inputItem.tabView.intrinsicContentSize() return self.inputItem.tabView.intrinsicContentSize
} }
} }

View File

@ -24,7 +24,7 @@
import UIKit import UIKit
public class ExpandableTextView: UITextView { open class ExpandableTextView: UITextView {
private let placeholder: UITextView = UITextView() private let placeholder: UITextView = UITextView()
@ -33,7 +33,12 @@ public class ExpandableTextView: UITextView {
self.commonInit() self.commonInit()
} }
override public var contentSize: CGSize { override public init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
self.commonInit()
}
override open var contentSize: CGSize {
didSet { didSet {
self.invalidateIntrinsicContentSize() self.invalidateIntrinsicContentSize()
self.layoutIfNeeded() // needed? self.layoutIfNeeded() // needed?
@ -41,55 +46,58 @@ public class ExpandableTextView: UITextView {
} }
deinit { deinit {
NSNotificationCenter.defaultCenter().removeObserver(self) NotificationCenter.default.removeObserver(self)
} }
private func commonInit() { 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.configurePlaceholder()
self.updatePlaceholderVisibility() self.updatePlaceholderVisibility()
} }
override open func layoutSubviews() {
override public func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
self.placeholder.frame = self.bounds self.placeholder.frame = self.bounds
} }
override public func intrinsicContentSize() -> CGSize { override open var intrinsicContentSize: CGSize {
return self.contentSize return self.contentSize
} }
override public var text: String! { override open var text: String! {
didSet { didSet {
self.textDidChange() self.textDidChange()
} }
} }
override public var textContainerInset: UIEdgeInsets { override open var textContainerInset: UIEdgeInsets {
didSet { didSet {
self.configurePlaceholder() self.configurePlaceholder()
} }
} }
override public var textAlignment: NSTextAlignment { override open var textAlignment: NSTextAlignment {
didSet { didSet {
self.configurePlaceholder() self.configurePlaceholder()
} }
} }
public func setTextPlaceholder(textPlaceholder: String) { open func setTextPlaceholder(_ textPlaceholder: String) {
self.placeholder.text = textPlaceholder self.placeholder.text = textPlaceholder
} }
public func setTextPlaceholderColor(color: UIColor) { open func setTextPlaceholderColor(_ color: UIColor) {
self.placeholder.textColor = color self.placeholder.textColor = color
} }
public func setTextPlaceholderFont(font: UIFont) { open func setTextPlaceholderFont(_ font: UIFont) {
self.placeholder.font = font self.placeholder.font = font
} }
open func setTextPlaceholderAccessibilityIdentifier(_ accessibilityIdentifier: String) {
self.placeholder.accessibilityIdentifier = accessibilityIdentifier
}
func textDidChange() { func textDidChange() {
self.updatePlaceholderVisibility() self.updatePlaceholderVisibility()
self.scrollToCaret() self.scrollToCaret()
@ -100,14 +108,14 @@ public class ExpandableTextView: UITextView {
// 2. Paste very long text (so it snaps to nav bar and shows scroll indicators) // 2. Paste very long text (so it snaps to nav bar and shows scroll indicators)
// 3. Select all and cut // 3. Select all and cut
// 4. Paste again: Texview it's smaller than it should be // 4. Paste again: Texview it's smaller than it should be
self.scrollEnabled = false self.isScrollEnabled = false
self.scrollEnabled = true self.isScrollEnabled = true
} }
} }
private func scrollToCaret() { private func scrollToCaret() {
if let textRange = self.selectedTextRange { 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)) rect = CGRect(origin: rect.origin, size: CGSize(width: rect.width, height: rect.height + textContainerInset.bottom))
self.scrollRectToVisible(rect, animated: false) self.scrollRectToVisible(rect, animated: false)
@ -132,11 +140,11 @@ public class ExpandableTextView: UITextView {
private func configurePlaceholder() { private func configurePlaceholder() {
self.placeholder.translatesAutoresizingMaskIntoConstraints = false self.placeholder.translatesAutoresizingMaskIntoConstraints = false
self.placeholder.editable = false self.placeholder.isEditable = false
self.placeholder.selectable = false self.placeholder.isSelectable = false
self.placeholder.userInteractionEnabled = false self.placeholder.isUserInteractionEnabled = false
self.placeholder.textAlignment = self.textAlignment self.placeholder.textAlignment = self.textAlignment
self.placeholder.textContainerInset = self.textContainerInset self.placeholder.textContainerInset = self.textContainerInset
self.placeholder.backgroundColor = UIColor.clearColor() self.placeholder.backgroundColor = UIColor.clear
} }
} }

View File

@ -24,7 +24,7 @@
import UIKit import UIKit
public class HorizontalStackScrollView: UIScrollView { open class HorizontalStackScrollView: UIScrollView {
private var arrangedViews: [UIView] = [] private var arrangedViews: [UIView] = []
private var arrangedViewContraints: [NSLayoutConstraint] = [] private var arrangedViewContraints: [NSLayoutConstraint] = []
@ -34,16 +34,16 @@ public class HorizontalStackScrollView: UIScrollView {
} }
} }
func addArrangedViews(views: [UIView]) { func addArrangedViews(_ views: [UIView]) {
for view in views { for view in views {
view.translatesAutoresizingMaskIntoConstraints = false view.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(view) self.addSubview(view)
} }
self.arrangedViews.appendContentsOf(views) self.arrangedViews.append(contentsOf: views)
self.setNeedsUpdateConstraints() self.setNeedsUpdateConstraints()
} }
override public func updateConstraints() { override open func updateConstraints() {
super.updateConstraints() super.updateConstraints()
self.removeConstraintsForArrangedViews() self.removeConstraintsForArrangedViews()
self.addConstraintsForArrengedViews() self.addConstraintsForArrengedViews()
@ -57,23 +57,23 @@ public class HorizontalStackScrollView: UIScrollView {
} }
private func addConstraintsForArrengedViews() { private func addConstraintsForArrengedViews() {
for (index, view) in arrangedViews.enumerate() { for (index, view) in arrangedViews.enumerated() {
switch index { switch index {
case 0: 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.addConstraint(constraint)
self.arrangedViewContraints.append(constraint) self.arrangedViewContraints.append(constraint)
case arrangedViews.count-1: 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.addConstraint(constraint)
self.arrangedViewContraints.append(constraint) self.arrangedViewContraints.append(constraint)
fallthrough fallthrough
default: 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.addConstraint(constraint)
self.arrangedViewContraints.append(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 captureLayer: AVCaptureVideoPreviewLayer? { get }
var isInitialized: Bool { get } var isInitialized: Bool { get }
var isCapturing: Bool { get } var isCapturing: Bool { get }
func startCapturing(completion: () -> Void) func startCapturing(_ completion: @escaping () -> Void)
func stopCapturing(completion: () -> Void) func stopCapturing(_ completion: @escaping () -> Void)
} }
class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol { class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
@ -38,39 +38,39 @@ class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
var isInitialized: Bool = false var isInitialized: Bool = false
var isCapturing: Bool { var isCapturing: Bool {
return self.isInitialized && self.captureSession.running return self.isInitialized && self.captureSession?.isRunning ?? false
} }
deinit { deinit {
var layer = self.captureLayer var layer = self.captureLayer
layer?.removeFromSuperlayer() layer?.removeFromSuperlayer()
var session: AVCaptureSession? = self.isInitialized ? self.captureSession : nil 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 // 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 layer != nil { layer = nil }
if session != nil { session = nil } if session != nil { session = nil }
} }
} }
func startCapturing(completion: () -> Void) { func startCapturing(_ completion: @escaping () -> Void) {
let operation = NSBlockOperation() let operation = BlockOperation()
operation.addExecutionBlock { [weak operation, weak self] in 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.addInputDevicesIfNeeded()
sSelf.captureSession.startRunning() sSelf.captureSession?.startRunning()
dispatch_async(dispatch_get_main_queue(), completion) DispatchQueue.main.async(execute: completion)
} }
self.queue.cancelAllOperations() self.queue.cancelAllOperations()
self.queue.addOperation(operation) self.queue.addOperation(operation)
} }
func stopCapturing(completion: () -> Void) { func stopCapturing(_ completion: @escaping () -> Void) {
let operation = NSBlockOperation() let operation = BlockOperation()
operation.addExecutionBlock { [weak operation, weak self] in 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.captureSession?.stopRunning()
sSelf.removeInputDevices() sSelf.removeInputDevices()
dispatch_async(dispatch_get_main_queue(), completion) DispatchQueue.main.async(execute: completion)
} }
self.queue.cancelAllOperations() self.queue.cancelAllOperations()
self.queue.addOperation(operation) self.queue.addOperation(operation)
@ -78,30 +78,34 @@ class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
private (set) var captureLayer: AVCaptureVideoPreviewLayer? private (set) var captureLayer: AVCaptureVideoPreviewLayer?
private lazy var queue: NSOperationQueue = { private lazy var queue: OperationQueue = {
let queue = NSOperationQueue() let queue = OperationQueue()
queue.qualityOfService = .UserInitiated queue.qualityOfService = .userInitiated
queue.maxConcurrentOperationCount = 1 queue.maxConcurrentOperationCount = 1
return queue return queue
}() }()
private lazy var captureSession: AVCaptureSession = { 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() let session = AVCaptureSession()
self.captureLayer = AVCaptureVideoPreviewLayer(session: session) self.captureLayer = AVCaptureVideoPreviewLayer(session: session)
self.captureLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill self.captureLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
self.isInitialized = true
return session return session
#else
return nil
#endif
}() }()
private func addInputDevicesIfNeeded() { 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 { if self.captureSession?.inputs?.count == 0 {
let device = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo) let device = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)
do { do {
let input = try AVCaptureDeviceInput(device: device) let input = try AVCaptureDeviceInput(device: device)
self.captureSession.addInput(input) self.captureSession?.addInput(input)
} catch { } catch {
} }
@ -109,9 +113,9 @@ class LiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
} }
private func removeInputDevices() { 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?.inputs?.forEach { (input) in
self.captureSession.removeInput(input as! AVCaptureInput) self.captureSession?.removeInput(input as! AVCaptureInput)
} }
} }
} }

View File

@ -25,18 +25,39 @@
import AVFoundation import AVFoundation
import Foundation import Foundation
import UIKit import UIKit
import Chatto
public struct LiveCameraCellAppearance {
public var backgroundColor: UIColor
public var cameraImageProvider: () -> UIImage?
public var cameraLockImageProvider: () -> UIImage?
public init(backgroundColor: UIColor,
cameraImage: @autoclosure @escaping () -> UIImage?,
cameraLockImage: @autoclosure @escaping () -> UIImage?) {
self.backgroundColor = backgroundColor
self.cameraImageProvider = cameraImage
self.cameraLockImageProvider = cameraLockImage
}
public static func createDefaultAppearance() -> LiveCameraCellAppearance {
return LiveCameraCellAppearance(
backgroundColor: UIColor(red: 24.0/255.0, green: 101.0/255.0, blue: 245.0/255.0, alpha: 1),
cameraImage: UIImage(named: "camera", in: Bundle(for: LiveCameraCell.self), compatibleWith: nil),
cameraLockImage: UIImage(named: "camera_lock", in: Bundle(for: LiveCameraCell.self), compatibleWith: nil)
)
}
}
class LiveCameraCell: UICollectionViewCell { class LiveCameraCell: UICollectionViewCell {
private struct Constants {
static let backgroundColor = UIColor(red: 24.0/255.0, green: 101.0/255.0, blue: 245.0/255.0, alpha: 1)
static let cameraImageName = "camera"
static let lockedCameraImageName = "camera_lock"
}
private var iconImageView: UIImageView! private var iconImageView: UIImageView!
var appearance: LiveCameraCellAppearance = LiveCameraCellAppearance.createDefaultAppearance() {
didSet {
self.contentView.backgroundColor = self.appearance.backgroundColor
}
}
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
self.commonInit() self.commonInit()
@ -49,7 +70,7 @@ class LiveCameraCell: UICollectionViewCell {
private func commonInit() { private func commonInit() {
self.configureIcon() self.configureIcon()
self.contentView.backgroundColor = Constants.backgroundColor self.contentView.backgroundColor = self.appearance.backgroundColor
} }
var captureLayer: CALayer? { var captureLayer: CALayer? {
@ -60,45 +81,45 @@ class LiveCameraCell: UICollectionViewCell {
self.contentView.layer.insertSublayer(captureLayer, below: self.iconImageView.layer) self.contentView.layer.insertSublayer(captureLayer, below: self.iconImageView.layer)
let animation = CABasicAnimation.bma_fadeInAnimationWithDuration(0.25) let animation = CABasicAnimation.bma_fadeInAnimationWithDuration(0.25)
let animationKey = "fadeIn" let animationKey = "fadeIn"
captureLayer.removeAnimationForKey(animationKey) captureLayer.removeAnimation(forKey: animationKey)
captureLayer.addAnimation(animation, forKey: animationKey) captureLayer.add(animation, forKey: animationKey)
} }
self.setNeedsLayout() self.setNeedsLayout()
} }
} }
} }
typealias CellCallback = (cell: LiveCameraCell) -> Void typealias CellCallback = (_ cell: LiveCameraCell) -> Void
var onWasAddedToWindow: CellCallback? var onWasAddedToWindow: CellCallback?
var onWasRemovedFromWindow: CellCallback? var onWasRemovedFromWindow: CellCallback?
override func didMoveToWindow() { override func didMoveToWindow() {
if let _ = self.window { if let _ = self.window {
self.onWasAddedToWindow?(cell: self) self.onWasAddedToWindow?(self)
} else { } else {
self.onWasRemovedFromWindow?(cell: self) self.onWasRemovedFromWindow?(self)
} }
} }
func updateWithAuthorizationStatus(status: AVAuthorizationStatus) { func updateWithAuthorizationStatus(_ status: AVAuthorizationStatus) {
self.authorizationStatus = status self.authorizationStatus = status
self.updateIcon() self.updateIcon()
} }
private var authorizationStatus: AVAuthorizationStatus = .NotDetermined private var authorizationStatus: AVAuthorizationStatus = .notDetermined
private func configureIcon() { private func configureIcon() {
self.iconImageView = UIImageView() self.iconImageView = UIImageView()
self.iconImageView.contentMode = .Center self.iconImageView.contentMode = .center
self.contentView.addSubview(self.iconImageView) self.contentView.addSubview(self.iconImageView)
} }
private func updateIcon() { private func updateIcon() {
switch self.authorizationStatus { switch self.authorizationStatus {
case .NotDetermined, .Authorized: case .notDetermined, .authorized:
self.iconImageView.image = UIImage(named: Constants.cameraImageName, inBundle: NSBundle(forClass: LiveCameraCell.self), compatibleWithTraitCollection: nil) self.iconImageView.image = self.appearance.cameraImageProvider()
case .Restricted, .Denied: case .restricted, .denied:
self.iconImageView.image = UIImage(named: Constants.lockedCameraImageName, inBundle: NSBundle(forClass: LiveCameraCell.self), compatibleWithTraitCollection: nil) self.iconImageView.image = self.appearance.cameraLockImageProvider()
} }
self.setNeedsLayout() self.setNeedsLayout()
} }

View File

@ -25,21 +25,54 @@
import Foundation import Foundation
import Photos import Photos
final class LiveCameraCellPresenter { public final class LiveCameraCellPresenter {
private typealias Class = LiveCameraCellPresenter
public typealias AVAuthorizationStatusProvider = () -> AVAuthorizationStatus
private let cellAppearance: LiveCameraCellAppearance
private let authorizationStatusProvider: () -> AVAuthorizationStatus
public init(cellAppearance: LiveCameraCellAppearance = LiveCameraCellAppearance.createDefaultAppearance(), authorizationStatusProvider: @escaping AVAuthorizationStatusProvider = LiveCameraCellPresenter.createDefaultCameraAuthorizationStatusProvider()) {
self.cellAppearance = cellAppearance
self.authorizationStatusProvider = authorizationStatusProvider
}
deinit { deinit {
self.unsubscribeFromAppNotifications() self.unsubscribeFromAppNotifications()
} }
private static let reuseIdentifier = "LiveCameraCell"
private static func createDefaultCameraAuthorizationStatusProvider() -> AVAuthorizationStatusProvider {
return {
return AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeVideo)
}
}
public static func registerCells(collectionView: UICollectionView) {
collectionView.register(LiveCameraCell.self, forCellWithReuseIdentifier: Class.reuseIdentifier)
}
public func dequeueCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCell(withReuseIdentifier: Class.reuseIdentifier, for: indexPath)
}
private weak var cell: LiveCameraCell? private weak var cell: LiveCameraCell?
func cellWillBeShown(cell: LiveCameraCell) { public func cellWillBeShown(_ cell: UICollectionViewCell) {
guard let cell = cell as? LiveCameraCell else {
assertionFailure("Invalid cell given to presenter")
return
}
self.cell = cell self.cell = cell
self.configureCell() self.configureCell()
self.startCapturing() self.startCapturing()
} }
func cellWasHidden(cell: LiveCameraCell) { public func cellWasHidden(_ cell: UICollectionViewCell) {
guard let cell = cell as? LiveCameraCell else {
assertionFailure("Invalid cell given to presenter")
return
}
if self.cell === cell { if self.cell === cell {
cell.captureLayer = nil cell.captureLayer = nil
self.cell = nil self.cell = nil
@ -50,7 +83,9 @@ final class LiveCameraCellPresenter {
private func configureCell() { private func configureCell() {
guard let cameraCell = self.cell else { return } guard let cameraCell = self.cell else { return }
self.cameraAuthorizationStatus = self.authorizationStatusProvider()
cameraCell.updateWithAuthorizationStatus(self.cameraAuthorizationStatus) cameraCell.updateWithAuthorizationStatus(self.cameraAuthorizationStatus)
cameraCell.appearance = self.cellAppearance
if self.captureSession.isCapturing { if self.captureSession.isCapturing {
cameraCell.captureLayer = self.captureSession.captureLayer cameraCell.captureLayer = self.captureSession.captureLayer
@ -59,14 +94,14 @@ final class LiveCameraCellPresenter {
} }
cameraCell.onWasAddedToWindow = { [weak self] (cell) in 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 { if !sSelf.cameraPickerIsVisible {
sSelf.startCapturing() sSelf.startCapturing()
} }
} }
cameraCell.onWasRemovedFromWindow = { [weak self] (cell) in 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 { if !sSelf.cameraPickerIsVisible {
sSelf.stopCapturing() sSelf.stopCapturing()
} }
@ -74,11 +109,11 @@ final class LiveCameraCellPresenter {
} }
// MARK: - App Notifications // MARK: - App Notifications
lazy var notificationCenter = NSNotificationCenter.defaultCenter() lazy var notificationCenter = NotificationCenter.default
private func subscribeToAppNotifications() { private func subscribeToAppNotifications() {
self.notificationCenter.addObserver(self, selector: #selector(LiveCameraCellPresenter.handleWillResignActiveNotification), name: UIApplicationWillResignActiveNotification, 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: UIApplicationDidBecomeActiveNotification, object: nil) self.notificationCenter.addObserver(self, selector: #selector(LiveCameraCellPresenter.handleDidBecomeActiveNotification), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)
} }
private func unsubscribeFromAppNotifications() { private func unsubscribeFromAppNotifications() {
@ -89,7 +124,7 @@ final class LiveCameraCellPresenter {
@objc @objc
private func handleWillResignActiveNotification() { private func handleWillResignActiveNotification() {
if self.captureSession.isCapturing ?? false { if self.captureSession.isCapturing {
self.needsRestoreCaptureSession = true self.needsRestoreCaptureSession = true
self.stopCapturing() self.stopCapturing()
} }
@ -132,16 +167,16 @@ final class LiveCameraCellPresenter {
private var isCaptureAvailable: Bool { private var isCaptureAvailable: Bool {
switch self.cameraAuthorizationStatus { switch self.cameraAuthorizationStatus {
case .NotDetermined, .Restricted, .Denied: case .notDetermined, .restricted, .denied:
return false return false
case .Authorized: case .authorized:
return true return true
} }
} }
lazy var captureSession: LiveCameraCaptureSessionProtocol = LiveCameraCaptureSession() lazy var captureSession: LiveCameraCaptureSessionProtocol = LiveCameraCaptureSession()
var cameraAuthorizationStatus: AVAuthorizationStatus = .NotDetermined { private var cameraAuthorizationStatus: AVAuthorizationStatus = .notDetermined {
didSet { didSet {
if self.isCaptureAvailable { if self.isCaptureAvailable {
self.subscribeToAppNotifications() self.subscribeToAppNotifications()

View File

@ -24,7 +24,7 @@
import Foundation import Foundation
public class PhotosChatInputItem: ChatInputItemProtocol { open class PhotosChatInputItem: ChatInputItemProtocol {
typealias Class = PhotosChatInputItem typealias Class = PhotosChatInputItem
public var photoInputHandler: ((UIImage) -> Void)? public var photoInputHandler: ((UIImage) -> Void)?
@ -33,55 +33,63 @@ public class PhotosChatInputItem: ChatInputItemProtocol {
public weak var presentingController: UIViewController? public weak var presentingController: UIViewController?
let buttonAppearance: TabInputButtonAppearance let buttonAppearance: TabInputButtonAppearance
public init(presentingController: UIViewController?, tabInputButtonAppearance: TabInputButtonAppearance = Class.createDefaultButtonAppearance()) { let inputViewAppearance: PhotosInputViewAppearance
public init(presentingController: UIViewController?,
tabInputButtonAppearance: TabInputButtonAppearance = Class.createDefaultButtonAppearance(),
inputViewAppearance: PhotosInputViewAppearance = Class.createDefaultInputViewAppearance()) {
self.presentingController = presentingController self.presentingController = presentingController
self.buttonAppearance = tabInputButtonAppearance self.buttonAppearance = tabInputButtonAppearance
self.inputViewAppearance = inputViewAppearance
} }
public class func createDefaultButtonAppearance() -> TabInputButtonAppearance { public static func createDefaultButtonAppearance() -> TabInputButtonAppearance {
let images: [UIControlState: UIImage] = [ let images: [UIControlStateWrapper: UIImage] = [
.Normal: UIImage(named: "camera-icon-unselected", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!, UIControlStateWrapper(state: .normal): UIImage(named: "camera-icon-unselected", in: Bundle(for: Class.self), compatibleWith: nil)!,
.Selected: UIImage(named: "camera-icon-selected", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)!, UIControlStateWrapper(state: .selected): UIImage(named: "camera-icon-selected", in: Bundle(for: Class.self), compatibleWith: nil)!,
.Highlighted: UIImage(named: "camera-icon-selected", inBundle: NSBundle(forClass: Class.self), compatibleWithTraitCollection: nil)! UIControlStateWrapper(state: .highlighted): UIImage(named: "camera-icon-selected", in: Bundle(for: Class.self), compatibleWith: nil)!
] ]
return TabInputButtonAppearance(images: images, size: nil) return TabInputButtonAppearance(images: images, size: nil)
} }
public static func createDefaultInputViewAppearance() -> PhotosInputViewAppearance {
return PhotosInputViewAppearance(liveCameraCellAppearence: LiveCameraCellAppearance.createDefaultAppearance())
}
lazy private var internalTabView: UIButton = { lazy private var internalTabView: UIButton = {
return TabInputButton.makeInputButton(withAppearance: self.buttonAppearance) return TabInputButton.makeInputButton(withAppearance: self.buttonAppearance, accessibilityID: "photos.chat.input.view")
}() }()
lazy var photosInputView: PhotosInputViewProtocol = { lazy var photosInputView: PhotosInputViewProtocol = {
let photosInputView = PhotosInputView(presentingController: self.presentingController) let photosInputView = PhotosInputView(presentingController: self.presentingController, appearance: self.inputViewAppearance)
photosInputView.delegate = self photosInputView.delegate = self
return photosInputView return photosInputView
}() }()
public var selected = false { open var selected = false {
didSet { didSet {
self.internalTabView.selected = self.selected self.internalTabView.isSelected = self.selected
} }
} }
// MARK: - ChatInputItemProtocol // MARK: - ChatInputItemProtocol
public var presentationMode: ChatInputItemPresentationMode { open var presentationMode: ChatInputItemPresentationMode {
return .CustomView return .customView
} }
public var showsSendButton: Bool { open var showsSendButton: Bool {
return false return false
} }
public var inputView: UIView? { open var inputView: UIView? {
return self.photosInputView as? UIView return self.photosInputView as? UIView
} }
public var tabView: UIView { open var tabView: UIView {
return self.internalTabView return self.internalTabView
} }
public func handleInput(input: AnyObject) { open func handleInput(_ input: AnyObject) {
if let image = input as? UIImage { if let image = input as? UIImage {
self.photoInputHandler?(image) self.photoInputHandler?(image)
} }
@ -90,15 +98,15 @@ public class PhotosChatInputItem: ChatInputItemProtocol {
// MARK: - PhotosInputViewDelegate // MARK: - PhotosInputViewDelegate
extension PhotosChatInputItem: PhotosInputViewDelegate { extension PhotosChatInputItem: PhotosInputViewDelegate {
func inputView(inputView: PhotosInputViewProtocol, didSelectImage image: UIImage) { func inputView(_ inputView: PhotosInputViewProtocol, didSelectImage image: UIImage) {
self.photoInputHandler?(image) self.photoInputHandler?(image)
} }
func inputViewDidRequestCameraPermission(inputView: PhotosInputViewProtocol) { func inputViewDidRequestCameraPermission(_ inputView: PhotosInputViewProtocol) {
self.cameraPermissionHandler?() self.cameraPermissionHandler?()
} }
func inputViewDidRequestPhotoLibraryPermission(inputView: PhotosInputViewProtocol) { func inputViewDidRequestPhotoLibraryPermission(_ inputView: PhotosInputViewProtocol) {
self.photosPermissionHandler?() self.photosPermissionHandler?()
} }
} }

View File

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

View File

@ -44,8 +44,8 @@ class PhotosInputPlaceholderCell: UICollectionViewCell {
private var imageView: UIImageView! private var imageView: UIImageView!
private func commonInit() { private func commonInit() {
self.imageView = UIImageView() self.imageView = UIImageView()
self.imageView.contentMode = .Center self.imageView.contentMode = .center
self.imageView.image = UIImage(named: Constants.imageName, inBundle: NSBundle(forClass: PhotosInputPlaceholderCell.self), compatibleWithTraitCollection: nil) self.imageView.image = UIImage(named: Constants.imageName, in: Bundle(for: PhotosInputPlaceholderCell.self), compatibleWith: nil)
self.contentView.addSubview(self.imageView) self.contentView.addSubview(self.imageView)
self.contentView.backgroundColor = Constants.backgroundColor self.contentView.backgroundColor = Constants.backgroundColor
} }
@ -77,7 +77,7 @@ class PhotosInputCell: UICollectionViewCell {
private func commonInit() { private func commonInit() {
self.clipsToBounds = true self.clipsToBounds = true
self.imageView = UIImageView() self.imageView = UIImageView()
self.imageView.contentMode = .ScaleAspectFill self.imageView.contentMode = .scaleAspectFill
self.contentView.addSubview(self.imageView) self.contentView.addSubview(self.imageView)
self.contentView.backgroundColor = Constants.backgroundColor self.contentView.backgroundColor = Constants.backgroundColor
} }

View File

@ -25,7 +25,7 @@
import UIKit import UIKit
protocol PhotosInputCellProviderProtocol { protocol PhotosInputCellProviderProtocol {
func cellForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewCell func cellForItemAtIndexPath(_ indexPath: IndexPath) -> UICollectionViewCell
} }
class PhotosInputPlaceholderCellProvider: PhotosInputCellProviderProtocol { class PhotosInputPlaceholderCellProvider: PhotosInputCellProviderProtocol {
@ -33,11 +33,11 @@ class PhotosInputPlaceholderCellProvider: PhotosInputCellProviderProtocol {
private let collectionView: UICollectionView private let collectionView: UICollectionView
init(collectionView: UICollectionView) { init(collectionView: UICollectionView) {
self.collectionView = collectionView self.collectionView = collectionView
self.collectionView.registerClass(PhotosInputPlaceholderCell.self, forCellWithReuseIdentifier: self.reuseIdentifier) self.collectionView.register(PhotosInputPlaceholderCell.self, forCellWithReuseIdentifier: self.reuseIdentifier)
} }
func cellForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewCell { func cellForItemAtIndexPath(_ indexPath: IndexPath) -> UICollectionViewCell {
return self.collectionView.dequeueReusableCellWithReuseIdentifier(self.reuseIdentifier, forIndexPath: indexPath) return self.collectionView.dequeueReusableCell(withReuseIdentifier: self.reuseIdentifier, for: indexPath)
} }
} }
@ -48,20 +48,20 @@ class PhotosInputCellProvider: PhotosInputCellProviderProtocol {
init(collectionView: UICollectionView, dataProvider: PhotosInputDataProviderProtocol) { init(collectionView: UICollectionView, dataProvider: PhotosInputDataProviderProtocol) {
self.dataProvider = dataProvider self.dataProvider = dataProvider
self.collectionView = collectionView self.collectionView = collectionView
self.collectionView.registerClass(PhotosInputCell.self, forCellWithReuseIdentifier: self.reuseIdentifier) self.collectionView.register(PhotosInputCell.self, forCellWithReuseIdentifier: self.reuseIdentifier)
} }
func cellForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewCell { func cellForItemAtIndexPath(_ indexPath: IndexPath) -> UICollectionViewCell {
let cell = self.collectionView.dequeueReusableCellWithReuseIdentifier(self.reuseIdentifier, forIndexPath: indexPath) as! PhotosInputCell let cell = self.collectionView.dequeueReusableCell(withReuseIdentifier: self.reuseIdentifier, for: indexPath) as! PhotosInputCell
self.configureCell(cell, atIndexPath: indexPath) self.configureCell(cell, atIndexPath: indexPath)
return cell return cell
} }
private let previewRequests = NSMapTable.weakToStrongObjectsMapTable() private let previewRequests = NSMapTable<PhotosInputCell, NSNumber>.weakToStrongObjects()
private func configureCell(cell: PhotosInputCell, atIndexPath indexPath: NSIndexPath) { private func configureCell(_ cell: PhotosInputCell, atIndexPath indexPath: IndexPath) {
if let requestID = self.previewRequests.objectForKey(cell) as? NSNumber { if let requestID = self.previewRequests.object(forKey: cell) {
self.previewRequests.removeObjectForKey(cell) self.previewRequests.removeObject(forKey: cell)
self.dataProvider.cancelPreviewImageRequest(requestID.intValue) self.dataProvider.cancelPreviewImageRequest(requestID.int32Value)
} }
let index = indexPath.item - 1 let index = indexPath.item - 1
@ -69,17 +69,17 @@ class PhotosInputCellProvider: PhotosInputCellProviderProtocol {
var imageProvidedSynchronously = true var imageProvidedSynchronously = true
var requestID: Int32 = -1 var requestID: Int32 = -1
requestID = self.dataProvider.requestPreviewImageAtIndex(index, targetSize: targetSize) { [weak self, weak cell] image in 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) // 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 // 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 // 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 { if imageIsForThisCell {
sCell.image = image sCell.image = image
} }
} }
imageProvidedSynchronously = false 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 import PhotosUI
protocol PhotosInputDataProviderDelegate: class { protocol PhotosInputDataProviderDelegate: class {
func handlePhotosInpudDataProviderUpdate(dataProvider: PhotosInputDataProviderProtocol, updateBlock: () -> Void) func handlePhotosInpudDataProviderUpdate(_ dataProvider: PhotosInputDataProviderProtocol, updateBlock: @escaping () -> Void)
} }
protocol PhotosInputDataProviderProtocol { protocol PhotosInputDataProviderProtocol {
weak var delegate: PhotosInputDataProviderDelegate? { get set } weak var delegate: PhotosInputDataProviderDelegate? { get set }
var count: Int { get } var count: Int { get }
func requestPreviewImageAtIndex(index: Int, targetSize: CGSize, completion: (UIImage) -> Void) -> Int32 func requestPreviewImageAtIndex(_ index: Int, targetSize: CGSize, completion: @escaping (UIImage) -> Void) -> Int32
func requestFullImageAtIndex(index: Int, completion: (UIImage) -> Void) func requestFullImageAtIndex(_ index: Int, completion: @escaping (UIImage) -> Void)
func cancelPreviewImageRequest(requestID: Int32) func cancelPreviewImageRequest(_ requestID: Int32)
} }
class PhotosInputPlaceholderDataProvider: PhotosInputDataProviderProtocol { class PhotosInputPlaceholderDataProvider: PhotosInputDataProviderProtocol {
@ -49,14 +49,14 @@ class PhotosInputPlaceholderDataProvider: PhotosInputDataProviderProtocol {
return self.numberOfPlaceholders 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 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,44 +64,53 @@ class PhotosInputPlaceholderDataProvider: PhotosInputDataProviderProtocol {
class PhotosInputDataProvider: NSObject, PhotosInputDataProviderProtocol, PHPhotoLibraryChangeObserver { class PhotosInputDataProvider: NSObject, PhotosInputDataProviderProtocol, PHPhotoLibraryChangeObserver {
weak var delegate: PhotosInputDataProviderDelegate? weak var delegate: PhotosInputDataProviderDelegate?
private var imageManager = PHCachingImageManager() private var imageManager = PHCachingImageManager()
private var fetchResult: PHFetchResult! private var fetchResult: PHFetchResult<PHAsset>!
override init() { override init() {
func fetchOptions(_ predicate: NSPredicate?) -> PHFetchOptions {
let options = PHFetchOptions() let options = PHFetchOptions()
options.sortDescriptors = [ NSSortDescriptor(key: "modificationDate", ascending: false) ] options.sortDescriptors = [ NSSortDescriptor(key: "creationDate", ascending: false) ]
self.fetchResult = PHAsset.fetchAssetsWithMediaType(.Image, options: options) options.predicate = predicate
return options
}
if let userLibraryCollection = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: nil).firstObject {
self.fetchResult = PHAsset.fetchAssets(in: userLibraryCollection, options: fetchOptions(NSPredicate(format: "mediaType = \(PHAssetMediaType.image.rawValue)")))
} else {
self.fetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions(nil))
}
super.init() super.init()
PHPhotoLibrary.sharedPhotoLibrary().registerChangeObserver(self) PHPhotoLibrary.shared().register(self)
} }
deinit { deinit {
PHPhotoLibrary.sharedPhotoLibrary().unregisterChangeObserver(self) PHPhotoLibrary.shared().unregisterChangeObserver(self)
} }
var count: Int { var count: Int {
return self.fetchResult.count 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") 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() let options = PHImageRequestOptions()
options.deliveryMode = .HighQualityFormat options.deliveryMode = .highQualityFormat
return self.imageManager.requestImageForAsset(asset, targetSize: targetSize, contentMode: .AspectFill, options: options) { (image, info) in return self.imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options) { (image, info) in
if let image = image { if let image = image {
completion(image) completion(image)
} }
} }
} }
func cancelPreviewImageRequest(requestID: Int32) { func cancelPreviewImageRequest(_ requestID: Int32) {
self.imageManager.cancelImageRequest(requestID) 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") assert(index >= 0 && index < self.fetchResult.count, "Index out of bounds")
let asset = self.fetchResult[index] as! PHAsset let asset = self.fetchResult[index]
self.imageManager.requestImageDataForAsset(asset, options: .None) { (data, dataUTI, orientation, info) -> Void in self.imageManager.requestImageData(for: asset, options: .none) { (data, dataUTI, orientation, info) -> Void in
if let data = data, image = UIImage(data: data) { if let data = data, let image = UIImage(data: data) {
completion(image) completion(image)
} }
} }
@ -109,14 +118,14 @@ class PhotosInputDataProvider: NSObject, PhotosInputDataProviderProtocol, PHPhot
// MARK: PHPhotoLibraryChangeObserver // 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. // 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 } 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 let updateBlock = { () -> Void in
self?.fetchResult = changeDetails.fetchResultAfterChanges self?.fetchResult = changeDetails.fetchResultAfterChanges as! PHFetchResult<PHAsset>
} }
sSelf.delegate?.handlePhotosInpudDataProviderUpdate(sSelf, updateBlock: updateBlock) sSelf.delegate?.handlePhotosInpudDataProviderUpdate(sSelf, updateBlock: updateBlock)
} }
@ -139,7 +148,7 @@ class PhotosInputWithPlaceholdersDataProvider: PhotosInputDataProviderProtocol,
return max(self.photosDataProvider.count, self.placeholdersDataProvider.count) 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 { if index < self.photosDataProvider.count {
return self.photosDataProvider.requestPreviewImageAtIndex(index, targetSize: targetSize, completion: completion) return self.photosDataProvider.requestPreviewImageAtIndex(index, targetSize: targetSize, completion: completion)
} else { } else {
@ -147,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 { if index < self.photosDataProvider.count {
return self.photosDataProvider.requestFullImageAtIndex(index, completion: completion) return self.photosDataProvider.requestFullImageAtIndex(index, completion: completion)
} else { } else {
@ -155,13 +164,13 @@ class PhotosInputWithPlaceholdersDataProvider: PhotosInputDataProviderProtocol,
} }
} }
func cancelPreviewImageRequest(requestID: Int32) { func cancelPreviewImageRequest(_ requestID: Int32) {
return self.photosDataProvider.cancelPreviewImageRequest(requestID) return self.photosDataProvider.cancelPreviewImageRequest(requestID)
} }
// MARK: PhotosInputDataProviderDelegate // MARK: PhotosInputDataProviderDelegate
func handlePhotosInpudDataProviderUpdate(dataProvider: PhotosInputDataProviderProtocol, updateBlock: () -> Void) { func handlePhotosInpudDataProviderUpdate(_ dataProvider: PhotosInputDataProviderProtocol, updateBlock: @escaping () -> Void) {
self.delegate?.handlePhotosInpudDataProviderUpdate(self, updateBlock: updateBlock) self.delegate?.handlePhotosInpudDataProviderUpdate(self, updateBlock: updateBlock)
} }
} }

View File

@ -26,32 +26,39 @@ import UIKit
import Photos import Photos
import Chatto import Chatto
public struct PhotosInputViewAppearance {
public var liveCameraCellAppearence: LiveCameraCellAppearance
public init(liveCameraCellAppearence: LiveCameraCellAppearance) {
self.liveCameraCellAppearence = liveCameraCellAppearence
}
}
protocol PhotosInputViewProtocol { protocol PhotosInputViewProtocol {
weak var delegate: PhotosInputViewDelegate? { get set } weak var delegate: PhotosInputViewDelegate? { get set }
weak var presentingController: UIViewController? { get } weak var presentingController: UIViewController? { get }
} }
protocol PhotosInputViewDelegate: class { protocol PhotosInputViewDelegate: class {
func inputView(inputView: PhotosInputViewProtocol, didSelectImage image: UIImage) func inputView(_ inputView: PhotosInputViewProtocol, didSelectImage image: UIImage)
func inputViewDidRequestCameraPermission(inputView: PhotosInputViewProtocol) func inputViewDidRequestCameraPermission(_ inputView: PhotosInputViewProtocol)
func inputViewDidRequestPhotoLibraryPermission(inputView: PhotosInputViewProtocol) func inputViewDidRequestPhotoLibraryPermission(_ inputView: PhotosInputViewProtocol)
} }
class PhotosInputView: UIView, PhotosInputViewProtocol { class PhotosInputView: UIView, PhotosInputViewProtocol {
private struct Constants { fileprivate struct Constants {
static let liveCameraItemIndex = 0 static let liveCameraItemIndex = 0
} }
private lazy var collectionViewQueue = SerialTaskQueue() fileprivate lazy var collectionViewQueue = SerialTaskQueue()
private var collectionView: UICollectionView! fileprivate var collectionView: UICollectionView!
private var collectionViewLayout: UICollectionViewFlowLayout! fileprivate var collectionViewLayout: UICollectionViewFlowLayout!
private var dataProvider: PhotosInputDataProviderProtocol! fileprivate var dataProvider: PhotosInputDataProviderProtocol!
private var cellProvider: PhotosInputCellProviderProtocol! fileprivate var cellProvider: PhotosInputCellProviderProtocol!
private var itemSizeCalculator: PhotosInputViewItemSizeCalculator! fileprivate var itemSizeCalculator: PhotosInputViewItemSizeCalculator!
var cameraAuthorizationStatus: AVAuthorizationStatus { var cameraAuthorizationStatus: AVAuthorizationStatus {
return AVCaptureDevice.authorizationStatusForMediaType(AVMediaTypeVideo) return AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeVideo)
} }
var photoLibraryAuthorizationStatus: PHAuthorizationStatus { var photoLibraryAuthorizationStatus: PHAuthorizationStatus {
@ -70,9 +77,11 @@ class PhotosInputView: UIView, PhotosInputViewProtocol {
} }
weak var presentingController: UIViewController? weak var presentingController: UIViewController?
init(presentingController: UIViewController?) { var appearance: PhotosInputViewAppearance?
init(presentingController: UIViewController?, appearance: PhotosInputViewAppearance) {
super.init(frame: CGRect.zero) super.init(frame: CGRect.zero)
self.presentingController = presentingController self.presentingController = presentingController
self.appearance = appearance
self.commonInit() self.commonInit()
} }
@ -82,7 +91,7 @@ class PhotosInputView: UIView, PhotosInputViewProtocol {
} }
private func commonInit() { private func commonInit() {
self.autoresizingMask = [.FlexibleWidth, .FlexibleHeight] self.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.configureCollectionView() self.configureCollectionView()
self.configureItemSizeCalculator() self.configureItemSizeCalculator()
self.dataProvider = PhotosInputPlaceholderDataProvider() self.dataProvider = PhotosInputPlaceholderDataProvider()
@ -99,10 +108,10 @@ class PhotosInputView: UIView, PhotosInputViewProtocol {
} }
private func requestAccessToVideo() { private func requestAccessToVideo() {
guard self.cameraAuthorizationStatus != .Authorized else { return } guard self.cameraAuthorizationStatus != .authorized else { return }
AVCaptureDevice.requestAccessForMediaType(AVMediaTypeVideo) { (success) -> Void in AVCaptureDevice.requestAccess(forMediaType: AVMediaTypeVideo) { (success) -> Void in
dispatch_async(dispatch_get_main_queue(), { () -> Void in DispatchQueue.main.async(execute: { () -> Void in
self.reloadVideoItem() self.reloadVideoItem()
}) })
} }
@ -113,22 +122,22 @@ class PhotosInputView: UIView, PhotosInputViewProtocol {
guard let sSelf = self else { return } guard let sSelf = self else { return }
sSelf.collectionView.performBatchUpdates({ 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 }, completion: { (finished) in
dispatch_async(dispatch_get_main_queue(), completion) DispatchQueue.main.async(execute: completion)
}) })
} }
} }
private func requestAccessToPhoto() { private func requestAccessToPhoto() {
guard self.photoLibraryAuthorizationStatus != .Authorized else { guard self.photoLibraryAuthorizationStatus != .authorized else {
self.replacePlaceholderItemsWithPhotoItems() self.replacePlaceholderItemsWithPhotoItems()
return return
} }
PHPhotoLibrary.requestAuthorization { (status: PHAuthorizationStatus) -> Void in PHPhotoLibrary.requestAuthorization { (status: PHAuthorizationStatus) -> Void in
if status == PHAuthorizationStatus.Authorized { if status == PHAuthorizationStatus.authorized {
dispatch_async(dispatch_get_main_queue(), { () -> Void in DispatchQueue.main.async(execute: { () -> Void in
self.replacePlaceholderItemsWithPhotoItems() self.replacePlaceholderItemsWithPhotoItems()
}) })
} }
@ -144,22 +153,24 @@ class PhotosInputView: UIView, PhotosInputViewProtocol {
sSelf.dataProvider = newDataProvider sSelf.dataProvider = newDataProvider
sSelf.cellProvider = PhotosInputCellProvider(collectionView: sSelf.collectionView, dataProvider: newDataProvider) sSelf.cellProvider = PhotosInputCellProvider(collectionView: sSelf.collectionView, dataProvider: newDataProvider)
sSelf.collectionView.reloadData() sSelf.collectionView.reloadData()
dispatch_async(dispatch_get_main_queue(), completion) DispatchQueue.main.async(execute: completion)
} }
} }
func reload() { func reload() {
self.collectionViewQueue.addTask { [weak self] (completion) in self.collectionViewQueue.addTask { [weak self] (completion) in
self?.collectionView.reloadData() 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) return PhotosInputCameraPicker(presentingController: self.presentingController)
}() }()
private lazy var liveCameraPresenter = LiveCameraCellPresenter() fileprivate lazy var liveCameraPresenter: LiveCameraCellPresenter = {
return LiveCameraCellPresenter(cellAppearance: self.appearance?.liveCameraCellAppearence ?? LiveCameraCellAppearance.createDefaultAppearance())
}()
} }
extension PhotosInputView: UICollectionViewDataSource { extension PhotosInputView: UICollectionViewDataSource {
@ -167,30 +178,28 @@ extension PhotosInputView: UICollectionViewDataSource {
func configureCollectionView() { func configureCollectionView() {
self.collectionViewLayout = PhotosInputCollectionViewLayout() self.collectionViewLayout = PhotosInputCollectionViewLayout()
self.collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: self.collectionViewLayout) self.collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: self.collectionViewLayout)
self.collectionView.backgroundColor = UIColor.whiteColor() self.collectionView.backgroundColor = UIColor.white
self.collectionView.translatesAutoresizingMaskIntoConstraints = false self.collectionView.translatesAutoresizingMaskIntoConstraints = false
self.collectionView.registerClass(LiveCameraCell.self, forCellWithReuseIdentifier: "LiveCameraCell") LiveCameraCellPresenter.registerCells(collectionView: self.collectionView)
self.collectionView.dataSource = self self.collectionView.dataSource = self
self.collectionView.delegate = self self.collectionView.delegate = self
self.addSubview(self.collectionView) 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: .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: .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: .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: .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 return self.dataProvider.count + 1
} }
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
var cell: UICollectionViewCell var cell: UICollectionViewCell
if indexPath.item == Constants.liveCameraItemIndex { if indexPath.item == Constants.liveCameraItemIndex {
let liveCameraCell = collectionView.dequeueReusableCellWithReuseIdentifier("LiveCameraCell", forIndexPath: indexPath) as! LiveCameraCell cell = self.liveCameraPresenter.dequeueCell(collectionView: collectionView, indexPath: indexPath)
self.liveCameraPresenter.cameraAuthorizationStatus = self.cameraAuthorizationStatus
cell = liveCameraCell
} else { } else {
cell = self.cellProvider.cellForItemAtIndexPath(indexPath) cell = self.cellProvider.cellForItemAtIndexPath(indexPath)
} }
@ -199,9 +208,9 @@ extension PhotosInputView: UICollectionViewDataSource {
} }
extension PhotosInputView: UICollectionViewDelegateFlowLayout { extension PhotosInputView: UICollectionViewDelegateFlowLayout {
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.item == Constants.liveCameraItemIndex { if indexPath.item == Constants.liveCameraItemIndex {
if self.cameraAuthorizationStatus != .Authorized { if self.cameraAuthorizationStatus != .authorized {
self.delegate?.inputViewDidRequestCameraPermission(self) self.delegate?.inputViewDidRequestCameraPermission(self)
} else { } else {
self.liveCameraPresenter.cameraPickerWillAppear() self.liveCameraPresenter.cameraPickerWillAppear()
@ -216,7 +225,7 @@ extension PhotosInputView: UICollectionViewDelegateFlowLayout {
}) })
} }
} else { } else {
if self.photoLibraryAuthorizationStatus != .Authorized { if self.photoLibraryAuthorizationStatus != .authorized {
self.delegate?.inputViewDidRequestPhotoLibraryPermission(self) self.delegate?.inputViewDidRequestPhotoLibraryPermission(self)
} else { } else {
self.dataProvider.requestFullImageAtIndex(indexPath.item - 1) { image in self.dataProvider.requestFullImageAtIndex(indexPath.item - 1) { image in
@ -226,46 +235,46 @@ 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) 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 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 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 { if indexPath.item == Constants.liveCameraItemIndex {
self.liveCameraPresenter.cellWillBeShown(cell as! LiveCameraCell) 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 { if indexPath.item == Constants.liveCameraItemIndex {
self.liveCameraPresenter.cellWasHidden(cell as! LiveCameraCell) self.liveCameraPresenter.cellWasHidden(cell)
} }
} }
} }
extension PhotosInputView: PhotosInputDataProviderDelegate { extension PhotosInputView: PhotosInputDataProviderDelegate {
func handlePhotosInpudDataProviderUpdate(dataProvider: PhotosInputDataProviderProtocol, updateBlock: () -> Void) { func handlePhotosInpudDataProviderUpdate(_ dataProvider: PhotosInputDataProviderProtocol, updateBlock: @escaping () -> Void) {
self.collectionViewQueue.addTask { [weak self] (completion) in self.collectionViewQueue.addTask { [weak self] (completion) in
guard let sSelf = self else { return } guard let sSelf = self else { return }
updateBlock() updateBlock()
sSelf.collectionView.reloadData() sSelf.collectionView.reloadData()
dispatch_async(dispatch_get_main_queue(), completion) DispatchQueue.main.async(execute: completion)
} }
} }
} }
private class PhotosInputCollectionViewLayout: UICollectionViewFlowLayout { 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 return newBounds.width != self.collectionView?.bounds.width
} }
} }

View File

@ -26,7 +26,7 @@ struct PhotosInputViewItemSizeCalculator {
var itemsPerRow: Int = 0 var itemsPerRow: Int = 0
var interitemSpace: CGFloat = 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)) let availableWidth = width - self.interitemSpace * CGFloat((self.itemsPerRow - 1))
if availableWidth <= 0 { if availableWidth <= 0 {
return CGSize.zero return CGSize.zero

View File

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

View File

@ -25,10 +25,10 @@
import Foundation import Foundation
public struct TabInputButtonAppearance { public struct TabInputButtonAppearance {
public var images: [UIControlState: UIImage] public var images: [UIControlStateWrapper: UIImage]
public var size: CGSize? public var size: CGSize?
public init(images: [UIControlState: UIImage], size: CGSize?) { public init(images: [UIControlStateWrapper: UIImage], size: CGSize?) {
self.images = images self.images = images
self.size = size self.size = size
} }
@ -36,12 +36,15 @@ public struct TabInputButtonAppearance {
public class TabInputButton: UIButton { public class TabInputButton: UIButton {
static public func makeInputButton(withAppearance appearance: TabInputButtonAppearance) -> TabInputButton { static public func makeInputButton(withAppearance appearance: TabInputButtonAppearance, accessibilityID: String? = nil) -> TabInputButton {
let images = appearance.images let images = appearance.images
let button = TabInputButton(type: .Custom) let button = TabInputButton(type: .custom)
button.exclusiveTouch = true button.isExclusiveTouch = true
images.forEach { (state, image) in images.forEach { (state, image) in
button.setImage(image, forState: state) button.setImage(image, for: state.controlState)
}
if let accessibilityIdentifier = accessibilityID {
button.accessibilityIdentifier = accessibilityIdentifier
} }
button.size = appearance.size button.size = appearance.size
return button return button
@ -49,10 +52,10 @@ public class TabInputButton: UIButton {
private var size: CGSize? private var size: CGSize?
public override func intrinsicContentSize() -> CGSize { public override var intrinsicContentSize: CGSize {
if let size = self.size { if let size = self.size {
return size return size
} }
return super.intrinsicContentSize() return super.intrinsicContentSize
} }
} }

View File

@ -24,7 +24,7 @@
import Foundation import Foundation
public class TextChatInputItem { open class TextChatInputItem {
typealias Class = TextChatInputItem typealias Class = TextChatInputItem
public var textInputHandler: ((String) -> Void)? public var textInputHandler: ((String) -> Void)?
@ -33,22 +33,22 @@ public class TextChatInputItem {
self.buttonAppearance = tabInputButtonAppearance self.buttonAppearance = tabInputButtonAppearance
} }
public class func createDefaultButtonAppearance() -> TabInputButtonAppearance { public static func createDefaultButtonAppearance() -> TabInputButtonAppearance {
let images: [UIControlState: UIImage] = [ let images: [UIControlStateWrapper: UIImage] = [
.Normal: UIImage(named: "text-icon-unselected", inBundle: NSBundle(forClass: TextChatInputItem.self), compatibleWithTraitCollection: nil)!, UIControlStateWrapper(state: .normal): UIImage(named: "text-icon-unselected", in: Bundle(for: TextChatInputItem.self), compatibleWith: nil)!,
.Selected: UIImage(named: "text-icon-selected", inBundle: NSBundle(forClass: TextChatInputItem.self), compatibleWithTraitCollection: nil)!, UIControlStateWrapper(state: .selected): UIImage(named: "text-icon-selected", in: Bundle(for: TextChatInputItem.self), compatibleWith: nil)!,
.Highlighted: UIImage(named: "text-icon-selected", inBundle: NSBundle(forClass: TextChatInputItem.self), compatibleWithTraitCollection: nil)! UIControlStateWrapper(state: .highlighted): UIImage(named: "text-icon-selected", in: Bundle(for: TextChatInputItem.self), compatibleWith: nil)!
] ]
return TabInputButtonAppearance(images: images, size: nil) return TabInputButtonAppearance(images: images, size: nil)
} }
lazy private var internalTabView: TabInputButton = { lazy fileprivate var internalTabView: TabInputButton = {
return TabInputButton.makeInputButton(withAppearance: self.buttonAppearance) return TabInputButton.makeInputButton(withAppearance: self.buttonAppearance, accessibilityID: "text.chat.input.view")
}() }()
public var selected = false { open var selected = false {
didSet { didSet {
self.internalTabView.selected = self.selected self.internalTabView.isSelected = self.selected
} }
} }
} }
@ -56,7 +56,7 @@ public class TextChatInputItem {
// MARK: - ChatInputItemProtocol // MARK: - ChatInputItemProtocol
extension TextChatInputItem : ChatInputItemProtocol { extension TextChatInputItem : ChatInputItemProtocol {
public var presentationMode: ChatInputItemPresentationMode { public var presentationMode: ChatInputItemPresentationMode {
return .Keyboard return .keyboard
} }
public var showsSendButton: Bool { public var showsSendButton: Bool {
@ -71,7 +71,7 @@ extension TextChatInputItem : ChatInputItemProtocol {
return self.internalTabView return self.internalTabView
} }
public func handleInput(input: AnyObject) { public func handleInput(_ input: AnyObject) {
if let text = input as? String { if let text = input as? String {
self.textInputHandler?(text) self.textInputHandler?(text)
} }

View File

@ -25,7 +25,8 @@
import Foundation import Foundation
// Be aware this is not thread safe! // Be aware this is not thread safe!
public struct Observable<T> { // Why class? https://lists.swift.org/pipermail/swift-users/Week-of-Mon-20160711/002580.html
public class Observable<T> {
public init(_ value: T) { public init(_ value: T) {
self.value = value self.value = value
@ -35,17 +36,17 @@ public struct Observable<T> {
didSet { didSet {
self.cleanDeadObservers() self.cleanDeadObservers()
for observer in self.observers { for observer in self.observers {
observer.closure(old: oldValue, new: self.value) observer.closure(oldValue, self.value)
} }
} }
} }
public mutating 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.observers.append(Observer(owner: observer, closure: closure))
self.cleanDeadObservers() self.cleanDeadObservers()
} }
private mutating func cleanDeadObservers() { private func cleanDeadObservers() {
self.observers = self.observers.filter { $0.owner != nil } self.observers = self.observers.filter { $0.owner != nil }
} }
@ -54,8 +55,8 @@ public struct Observable<T> {
private struct Observer<T> { private struct Observer<T> {
weak var owner: AnyObject? weak var owner: AnyObject?
let closure: (old: T, new: T) -> () let closure: (_ old: T, _ new: T) -> ()
init (owner: AnyObject, closure: (old: T, new: T) -> ()) { init (owner: AnyObject, closure: @escaping (_ old: T, _ new: T) -> ()) {
self.owner = owner self.owner = owner
self.closure = closure self.closure = closure
} }

View File

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

View File

@ -25,26 +25,26 @@
import Foundation import Foundation
import CoreGraphics import CoreGraphics
private let scale = UIScreen.mainScreen().scale private let scale = UIScreen.main.scale
public enum HorizontalAlignment { public enum HorizontalAlignment {
case Left case left
case Center case center
case Right case right
} }
public enum VerticalAlignment { public enum VerticalAlignment {
case Top case top
case Center case center
case Bottom case bottom
} }
public extension CGSize { 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) 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) return self.bma_insetBy(dx: -dx, dy: -dy)
} }
} }
@ -59,21 +59,21 @@ public extension CGSize {
// Horizontal alignment // Horizontal alignment
switch xAlignament { switch xAlignament {
case .Left: case .left:
originX = 0 originX = 0
case .Center: case .center:
originX = containerRect.midX - self.width / 2.0 originX = containerRect.midX - self.width / 2.0
case .Right: case .right:
originX = containerRect.maxY - self.width originX = containerRect.maxY - self.width
} }
// Vertical alignment // Vertical alignment
switch yAlignment { switch yAlignment {
case .Top: case .top:
originY = 0 originY = 0
case .Center: case .center:
originY = containerRect.midY - self.height / 2.0 originY = containerRect.midY - self.height / 2.0
case .Bottom: case .bottom:
originY = containerRect.maxY - self.height originY = containerRect.maxY - self.height
} }
@ -105,9 +105,8 @@ public extension CGRect {
} }
} }
public extension CGPoint { 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) return CGPoint(x: self.x + dx, y: self.y + dy)
} }
} }
@ -147,39 +146,38 @@ public extension UIEdgeInsets {
} }
public extension UIImage { 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) let rect = CGRect(origin: CGPoint.zero, size: self.size)
UIGraphicsBeginImageContextWithOptions(rect.size, false, self.scale) UIGraphicsBeginImageContextWithOptions(rect.size, false, self.scale)
let context = UIGraphicsGetCurrentContext()! let context = UIGraphicsGetCurrentContext()!
color.setFill() color.setFill()
CGContextFillRect(context, rect) context.fill(rect)
self.drawInRect(rect, blendMode: .DestinationIn, alpha: 1) self.draw(in: rect, blendMode: .destinationIn, alpha: 1)
let image = UIGraphicsGetImageFromCurrentImageContext()! let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext() 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) 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()! let context = UIGraphicsGetCurrentContext()!
CGContextTranslateCTM(context, 0, rect.height) context.translateBy(x: 0, y: rect.height)
CGContextScaleCTM(context, 1.0, -1.0) context.scaleBy(x: 1.0, y: -1.0)
CGContextSetBlendMode(context, .Normal) context.setBlendMode(.normal)
CGContextDrawImage(context, rect, self.CGImage!) context.draw(self.cgImage!, in: rect)
CGContextClipToMask(context, rect, self.CGImage!) context.clip(to: rect, mask: self.cgImage!)
color.setFill() color.setFill()
CGContextAddRect(context, rect) context.addRect(rect)
CGContextDrawPath(context, .Fill) context.drawPath(using: .fill)
let image = UIGraphicsGetImageFromCurrentImageContext()! let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext() 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) let rect = CGRect(origin: CGPoint.zero, size: size)
UIGraphicsBeginImageContextWithOptions(size, false, 0) UIGraphicsBeginImageContextWithOptions(size, false, 0)
color.setFill() color.setFill()
@ -191,11 +189,11 @@ public extension UIImage {
} }
public extension UIColor { 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) 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 r1: CGFloat = 0, r2: CGFloat = 0
var g1: CGFloat = 0, g2: CGFloat = 0 var g1: CGFloat = 0, g2: CGFloat = 0
var b1: CGFloat = 0, b2: 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) let decorationAttributes = ChatItemDecorationAttributes(bottomMargin: 0, showsTail: false, canShowAvatar: false)
var interactionHandler: PhotoMessageTestHandler! var interactionHandler: PhotoMessageTestHandler!
override func setUp() { override func setUp() {
super.setUp()
let viewModelBuilder = PhotoMessageViewModelDefaultBuilder<PhotoMessageModel<MessageModel>>() let viewModelBuilder = PhotoMessageViewModelDefaultBuilder<PhotoMessageModel<MessageModel>>()
let sizingCell = PhotoMessageCollectionViewCell.sizingCell() let sizingCell = PhotoMessageCollectionViewCell.sizingCell()
let photoStyle = PhotoMessageCollectionViewCellDefaultStyle() let photoStyle = PhotoMessageCollectionViewCellDefaultStyle()
let baseStyle = BaseMessageCollectionViewCellDefaultStyle() 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()) let photoMessageModel = PhotoMessageModel(messageModel: messageModel, imageSize: CGSize(width: 30, height: 30), image: UIImage())
self.interactionHandler = PhotoMessageTestHandler() self.interactionHandler = PhotoMessageTestHandler()
self.presenter = PhotoMessagePresenter(messageModel: photoMessageModel, viewModelBuilder: viewModelBuilder, interactionHandler: self.interactionHandler, sizingCell: sizingCell, baseCellStyle: baseStyle, photoCellStyle: photoStyle) 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() { func testThat_WhenCellIsBeginLongPressOnBubble_ThenInteractionHandlerHandlesEvent() {
let cell = PhotoMessageCollectionViewCell(frame: CGRect.zero) let cell = PhotoMessageCollectionViewCell(frame: CGRect.zero)
self.presenter.configureCell(cell, decorationAttributes: self.decorationAttributes) self.presenter.configureCell(cell, decorationAttributes: self.decorationAttributes)
cell.onBubbleLongPressBegan?(cell: cell) cell.onBubbleLongPressBegan?(cell)
XCTAssertTrue(self.interactionHandler.didHandleBeginLongPressOnBubble) XCTAssertTrue(self.interactionHandler.didHandleBeginLongPressOnBubble)
} }
func testThat_WhenCellIsEndLongPressOnBubble_ThenInteractionHandlerHandlesEvent() { func testThat_WhenCellIsEndLongPressOnBubble_ThenInteractionHandlerHandlesEvent() {
let cell = PhotoMessageCollectionViewCell(frame: CGRect.zero) let cell = PhotoMessageCollectionViewCell(frame: CGRect.zero)
self.presenter.configureCell(cell, decorationAttributes: self.decorationAttributes) self.presenter.configureCell(cell, decorationAttributes: self.decorationAttributes)
cell.onBubbleLongPressEnded?(cell: cell) cell.onBubbleLongPressEnded?(cell)
XCTAssertTrue(self.interactionHandler.didHandleEndLongPressOnBubble) XCTAssertTrue(self.interactionHandler.didHandleEndLongPressOnBubble)
} }
} }

View File

@ -28,7 +28,7 @@ import XCTest
class PhotoMessagePresenterBuilderTests: XCTestCase { class PhotoMessagePresenterBuilderTests: XCTestCase {
func testThat_CreatesPresenter() { 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 photoMessageModel = PhotoMessageModel(messageModel: messageModel, imageSize: CGSize(width: 30, height: 30), image: UIImage())
let builder = PhotoMessagePresenterBuilder(viewModelBuilder: PhotoMessageViewModelDefaultBuilder<PhotoMessageModel<MessageModel>>(), interactionHandler: PhotoMessageTestHandler()) let builder = PhotoMessagePresenterBuilder(viewModelBuilder: PhotoMessageViewModelDefaultBuilder<PhotoMessageModel<MessageModel>>(), interactionHandler: PhotoMessageTestHandler())
XCTAssertNotNil(builder.createPresenterWithChatItem(photoMessageModel)) XCTAssertNotNil(builder.createPresenterWithChatItem(photoMessageModel))

View File

@ -31,11 +31,12 @@ class PhotoMessagePresenterTests: XCTestCase, UICollectionViewDataSource {
let decorationAttributes = ChatItemDecorationAttributes(bottomMargin: 0, showsTail: false, canShowAvatar: false) let decorationAttributes = ChatItemDecorationAttributes(bottomMargin: 0, showsTail: false, canShowAvatar: false)
let testImage = UIImage() let testImage = UIImage()
override func setUp() { override func setUp() {
super.setUp()
let viewModelBuilder = PhotoMessageViewModelDefaultBuilder<PhotoMessageModel<MessageModel>>() let viewModelBuilder = PhotoMessageViewModelDefaultBuilder<PhotoMessageModel<MessageModel>>()
let sizingCell = PhotoMessageCollectionViewCell.sizingCell() let sizingCell = PhotoMessageCollectionViewCell.sizingCell()
let photoStyle = PhotoMessageCollectionViewCellDefaultStyle() let photoStyle = PhotoMessageCollectionViewCellDefaultStyle()
let baseStyle = BaseMessageCollectionViewCellDefaultStyle() 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) 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) 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) PhotoMessagePresenter<PhotoMessageViewModelDefaultBuilder<PhotoMessageModel<MessageModel>>, PhotoMessageTestHandler>.registerCells(collectionView)
collectionView.dataSource = self collectionView.dataSource = self
collectionView.reloadData() 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 collectionView.dataSource = nil
} }
// MARK: Helpers // MARK: Helpers
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1 return 1
} }
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
return self.presenter.dequeueCell(collectionView: collectionView, indexPath: indexPath) return self.presenter.dequeueCell(collectionView: collectionView, indexPath: indexPath as IndexPath)
} }
} }
@ -79,27 +80,27 @@ class PhotoMessageTestHandler: BaseMessageInteractionHandlerProtocol {
typealias ViewModelT = PhotoMessageViewModel<PhotoMessageModel<MessageModel>> typealias ViewModelT = PhotoMessageViewModel<PhotoMessageModel<MessageModel>>
var didHandleTapOnFailIcon = false var didHandleTapOnFailIcon = false
func userDidTapOnFailIcon(viewModel viewModel: ViewModelT, failIconView: UIView) { func userDidTapOnFailIcon(viewModel: ViewModelT, failIconView: UIView) {
self.didHandleTapOnFailIcon = true self.didHandleTapOnFailIcon = true
} }
var didHandleTapOnAvatar = false var didHandleTapOnAvatar = false
func userDidTapOnAvatar(viewModel viewModel: ViewModelT) { func userDidTapOnAvatar(viewModel: ViewModelT) {
self.didHandleTapOnAvatar = true self.didHandleTapOnAvatar = true
} }
var didHandleTapOnBubble = false var didHandleTapOnBubble = false
func userDidTapOnBubble(viewModel viewModel: ViewModelT) { func userDidTapOnBubble(viewModel: ViewModelT) {
self.didHandleTapOnBubble = true self.didHandleTapOnBubble = true
} }
var didHandleBeginLongPressOnBubble = false var didHandleBeginLongPressOnBubble = false
func userDidBeginLongPressOnBubble(viewModel viewModel: ViewModelT) { func userDidBeginLongPressOnBubble(viewModel: ViewModelT) {
self.didHandleBeginLongPressOnBubble = true self.didHandleBeginLongPressOnBubble = true
} }
var didHandleEndLongPressOnBubble = false var didHandleEndLongPressOnBubble = false
func userDidEndLongPressOnBubble(viewModel viewModel: ViewModelT) { func userDidEndLongPressOnBubble(viewModel: ViewModelT) {
self.didHandleEndLongPressOnBubble = true self.didHandleEndLongPressOnBubble = true
} }
} }

View File

@ -28,7 +28,7 @@ import XCTest
class TextMessagePresenterBuilderTests: XCTestCase { class TextMessagePresenterBuilderTests: XCTestCase {
func testThat_CreatesPresenter() { 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 textMessageModel = TextMessageModel(messageModel: messageModel, text: "Some text")
let builder = TextMessagePresenterBuilder(viewModelBuilder: TextMessageViewModelDefaultBuilder<TextMessageModel<MessageModel>>(), interactionHandler: TextMessageTestHandler()) let builder = TextMessagePresenterBuilder(viewModelBuilder: TextMessageViewModelDefaultBuilder<TextMessageModel<MessageModel>>(), interactionHandler: TextMessageTestHandler())
XCTAssertNotNil(builder.createPresenterWithChatItem(textMessageModel)) XCTAssertNotNil(builder.createPresenterWithChatItem(textMessageModel))

View File

@ -31,11 +31,12 @@ class TextMessagePresenterTests: XCTestCase, UICollectionViewDataSource {
var presenter: TextMessagePresenter<TextMessageViewModelDefaultBuilder<TextMessageModel<MessageModel>>, TextMessageTestHandler>! var presenter: TextMessagePresenter<TextMessageViewModelDefaultBuilder<TextMessageModel<MessageModel>>, TextMessageTestHandler>!
let decorationAttributes = ChatItemDecorationAttributes(bottomMargin: 0, showsTail: false, canShowAvatar: false) let decorationAttributes = ChatItemDecorationAttributes(bottomMargin: 0, showsTail: false, canShowAvatar: false)
override func setUp() { override func setUp() {
super.setUp()
let viewModelBuilder = TextMessageViewModelDefaultBuilder<TextMessageModel<MessageModel>>() let viewModelBuilder = TextMessageViewModelDefaultBuilder<TextMessageModel<MessageModel>>()
let sizingCell = TextMessageCollectionViewCell.sizingCell() let sizingCell = TextMessageCollectionViewCell.sizingCell()
let textStyle = TextMessageCollectionViewCellDefaultStyle() let textStyle = TextMessageCollectionViewCellDefaultStyle()
let baseStyle = BaseMessageCollectionViewCellDefaultStyle() 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") 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()) 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) TextMessagePresenter<TextMessageViewModelDefaultBuilder<TextMessageModel<MessageModel>>, TextMessageTestHandler>.registerCells(collectionView)
collectionView.dataSource = self collectionView.dataSource = self
collectionView.reloadData() 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 collectionView.dataSource = nil
} }
@ -72,40 +73,44 @@ class TextMessagePresenterTests: XCTestCase, UICollectionViewDataSource {
} }
func testThat_CanPerformCopyAction() { func testThat_CanPerformCopyAction() {
#if swift(>=2.3)
XCTAssertTrue(self.presenter.canPerformMenuControllerAction(#selector(UIResponderStandardEditActions.copy(_:))))
#else
XCTAssertTrue(self.presenter.canPerformMenuControllerAction(#selector(NSObject.copy(_:)))) XCTAssertTrue(self.presenter.canPerformMenuControllerAction(#selector(NSObject.copy(_:))))
#endif
} }
// MARK: Helpers // MARK: Helpers
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1 return 1
} }
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
return self.presenter.dequeueCell(collectionView: collectionView, indexPath: indexPath) return self.presenter.dequeueCell(collectionView: collectionView, indexPath: indexPath as IndexPath)
} }
} }
class TextMessageTestHandler: BaseMessageInteractionHandlerProtocol { class TextMessageTestHandler: BaseMessageInteractionHandlerProtocol {
typealias ViewModelT = TextMessageViewModel<TextMessageModel<MessageModel>> 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> <key>CFBundlePackageType</key>
<string>BNDL</string> <string>BNDL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0</string> <string>3.0.1</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>

View File

@ -49,7 +49,7 @@ class ChatInputBarTests: XCTestCase {
return itemView return itemView
} }
private func simulateTapOnTextViewForDelegate(textViewDelegate: UITextViewDelegate) { private func simulateTapOnTextViewForDelegate(_ textViewDelegate: UITextViewDelegate) {
let dummyTextView = UITextView() let dummyTextView = UITextView()
let shouldBeginEditing = textViewDelegate.textViewShouldBeginEditing?(dummyTextView) ?? true let shouldBeginEditing = textViewDelegate.textViewShouldBeginEditing?(dummyTextView) ?? true
guard shouldBeginEditing else { return } guard shouldBeginEditing else { return }
@ -57,22 +57,22 @@ class ChatInputBarTests: XCTestCase {
} }
func testThat_WhenInputTextChanged_BarEnablesSendButton() { func testThat_WhenInputTextChanged_BarEnablesSendButton() {
self.bar.sendButton.enabled = false self.bar.sendButton.isEnabled = false
self.bar.inputText = "!" self.bar.inputText = "!"
XCTAssertTrue(self.bar.sendButton.enabled) XCTAssertTrue(self.bar.sendButton.isEnabled)
} }
func testThat_WhenInputTextBecomesEmpty_BarDisablesSendButton() { func testThat_WhenInputTextBecomesEmpty_BarDisablesSendButton() {
self.bar.sendButton.enabled = true self.bar.sendButton.isEnabled = true
self.bar.inputText = "" self.bar.inputText = ""
XCTAssertFalse(self.bar.sendButton.enabled) XCTAssertFalse(self.bar.sendButton.isEnabled)
} }
// MARK: - Presenter tests // MARK: - Presenter tests
func testThat_WhenItemViewTapped_ItNotifiesPresenterThatNewItemReceivedFocus() { func testThat_WhenItemViewTapped_ItNotifiesPresenterThatNewItemReceivedFocus() {
self.setupPresenter() self.setupPresenter()
let item = MockInputItem() let item = MockInputItem()
self.bar.inputItemViewTapped(createItemView(item)) self.bar.inputItemViewTapped(createItemView(inputItem: item))
XCTAssertTrue(self.presenter.onDidReceiveFocusOnItemCalled) XCTAssertTrue(self.presenter.onDidReceiveFocusOnItemCalled)
XCTAssertTrue(self.presenter.itemThatReceivedFocus === item) XCTAssertTrue(self.presenter.itemThatReceivedFocus === item)
@ -91,21 +91,21 @@ class ChatInputBarTests: XCTestCase {
} }
func testThat_GivenTextViewHasNoText_WhenTextViewDidChange_ItDisablesSendButton() { func testThat_GivenTextViewHasNoText_WhenTextViewDidChange_ItDisablesSendButton() {
self.bar.sendButton.enabled = true self.bar.sendButton.isEnabled = true
self.bar.textView.text = "" self.bar.textView.text = ""
self.bar.textViewDidChange(self.bar.textView) self.bar.textViewDidChange(self.bar.textView)
XCTAssertFalse(self.bar.sendButton.enabled) XCTAssertFalse(self.bar.sendButton.isEnabled)
} }
func testThat_WhenTextViewDidChange_ItEnablesSendButton() { func testThat_WhenTextViewDidChange_ItEnablesSendButton() {
self.bar.sendButton.enabled = false self.bar.sendButton.isEnabled = false
self.bar.textView.text = "!" self.bar.textView.text = "!"
self.bar.textViewDidChange(self.bar.textView) self.bar.textViewDidChange(self.bar.textView)
XCTAssertTrue(self.bar.sendButton.enabled) XCTAssertTrue(self.bar.sendButton.isEnabled)
} }
func testThat_WhenSendButtonTapped_ItNotifiesPresenter() { func testThat_WhenSendButtonTapped_ItNotifiesPresenter() {
@ -118,7 +118,7 @@ class ChatInputBarTests: XCTestCase {
func testThat_WhenItemViewTapped_ItNotifiesDelegateThatNewItemReceivedFocus() { func testThat_WhenItemViewTapped_ItNotifiesDelegateThatNewItemReceivedFocus() {
self.setupDelegate() self.setupDelegate()
let item = MockInputItem() let item = MockInputItem()
self.bar.inputItemViewTapped(createItemView(item)) self.bar.inputItemViewTapped(createItemView(inputItem: item))
XCTAssertTrue(self.delegate.inputBarDidReceiveFocusOnItemCalled) XCTAssertTrue(self.delegate.inputBarDidReceiveFocusOnItemCalled)
XCTAssertTrue(self.delegate.focusedItem === item) XCTAssertTrue(self.delegate.focusedItem === item)
@ -158,14 +158,14 @@ class ChatInputBarTests: XCTestCase {
self.bar.inputText = " " self.bar.inputText = " "
self.bar.textViewDidChange(self.bar.textView) self.bar.textViewDidChange(self.bar.textView)
XCTAssertTrue(closureCalled) XCTAssertTrue(closureCalled)
XCTAssertFalse(self.bar.sendButton.enabled) XCTAssertFalse(self.bar.sendButton.isEnabled)
} }
func testThat_WhenItemViewTapped_ItReceivesFocuesByDefault() { func testThat_WhenItemViewTapped_ItReceivesFocuesByDefault() {
self.setupPresenter() self.setupPresenter()
let item = MockInputItem() let item = MockInputItem()
self.bar.inputItemViewTapped(createItemView(item)) self.bar.inputItemViewTapped(createItemView(inputItem: item))
XCTAssertTrue(self.presenter.onDidReceiveFocusOnItemCalled) XCTAssertTrue(self.presenter.onDidReceiveFocusOnItemCalled)
XCTAssertTrue(self.presenter.itemThatReceivedFocus === item) XCTAssertTrue(self.presenter.itemThatReceivedFocus === item)
@ -176,7 +176,7 @@ class ChatInputBarTests: XCTestCase {
self.delegate.inputBarShouldFocusOnItemResult = true self.delegate.inputBarShouldFocusOnItemResult = true
let item = MockInputItem() let item = MockInputItem()
self.bar.inputItemViewTapped(createItemView(item)) self.bar.inputItemViewTapped(createItemView(inputItem: item))
XCTAssertTrue(self.delegate.inputBarShouldFocusOnItemCalled) XCTAssertTrue(self.delegate.inputBarShouldFocusOnItemCalled)
XCTAssertTrue(self.delegate.inputBarDidReceiveFocusOnItemCalled) XCTAssertTrue(self.delegate.inputBarDidReceiveFocusOnItemCalled)
@ -188,7 +188,7 @@ class ChatInputBarTests: XCTestCase {
self.delegate.inputBarShouldFocusOnItemResult = false self.delegate.inputBarShouldFocusOnItemResult = false
let item = MockInputItem() let item = MockInputItem()
self.bar.inputItemViewTapped(createItemView(item)) self.bar.inputItemViewTapped(createItemView(inputItem: item))
XCTAssertTrue(self.delegate.inputBarShouldFocusOnItemCalled) XCTAssertTrue(self.delegate.inputBarShouldFocusOnItemCalled)
XCTAssertFalse(self.delegate.inputBarDidReceiveFocusOnItemCalled) XCTAssertFalse(self.delegate.inputBarDidReceiveFocusOnItemCalled)
@ -241,7 +241,7 @@ class FakeChatInputBarPresenter: ChatInputBarPresenter {
var onDidReceiveFocusOnItemCalled = false var onDidReceiveFocusOnItemCalled = false
var itemThatReceivedFocus: ChatInputItemProtocol? var itemThatReceivedFocus: ChatInputItemProtocol?
func onDidReceiveFocusOnItem(item: ChatInputItemProtocol) { func onDidReceiveFocusOnItem(_ item: ChatInputItemProtocol) {
self.onDidReceiveFocusOnItemCalled = true self.onDidReceiveFocusOnItemCalled = true
self.itemThatReceivedFocus = item self.itemThatReceivedFocus = item
} }
@ -250,41 +250,41 @@ class FakeChatInputBarPresenter: ChatInputBarPresenter {
class FakeChatInputBarDelegate: ChatInputBarDelegate { class FakeChatInputBarDelegate: ChatInputBarDelegate {
var inputBarShouldBeginTextEditingCalled = false var inputBarShouldBeginTextEditingCalled = false
var inputBarShouldBeginTextEditingResult = true var inputBarShouldBeginTextEditingResult = true
func inputBarShouldBeginTextEditing(inputBar: ChatInputBar) -> Bool { func inputBarShouldBeginTextEditing(_ inputBar: ChatInputBar) -> Bool {
self.inputBarShouldBeginTextEditingCalled = true self.inputBarShouldBeginTextEditingCalled = true
return self.inputBarShouldBeginTextEditingResult return self.inputBarShouldBeginTextEditingResult
} }
var inputBarDidBeginEditingCalled = false var inputBarDidBeginEditingCalled = false
func inputBarDidBeginEditing(inputBar: ChatInputBar) { func inputBarDidBeginEditing(_ inputBar: ChatInputBar) {
self.inputBarDidBeginEditingCalled = true self.inputBarDidBeginEditingCalled = true
} }
var inputBarDidEndEditingCalled = false var inputBarDidEndEditingCalled = false
func inputBarDidEndEditing(inputBar: ChatInputBar) { func inputBarDidEndEditing(_ inputBar: ChatInputBar) {
self.inputBarDidEndEditingCalled = true self.inputBarDidEndEditingCalled = true
} }
var inputBarDidChangeTextCalled = false var inputBarDidChangeTextCalled = false
func inputBarDidChangeText(inputBar: ChatInputBar) { func inputBarDidChangeText(_ inputBar: ChatInputBar) {
self.inputBarDidChangeTextCalled = true self.inputBarDidChangeTextCalled = true
} }
var inputBarSendButtonPressedCalled = false var inputBarSendButtonPressedCalled = false
func inputBarSendButtonPressed(inputBar: ChatInputBar) { func inputBarSendButtonPressed(_ inputBar: ChatInputBar) {
self.inputBarSendButtonPressedCalled = true self.inputBarSendButtonPressedCalled = true
} }
var inputBarShouldFocusOnItemCalled = false var inputBarShouldFocusOnItemCalled = false
var inputBarShouldFocusOnItemResult = true var inputBarShouldFocusOnItemResult = true
func inputBar(inputBar: ChatInputBar, shouldFocusOnItem item: ChatInputItemProtocol) -> Bool { func inputBar(_ inputBar: ChatInputBar, shouldFocusOnItem item: ChatInputItemProtocol) -> Bool {
self.inputBarShouldFocusOnItemCalled = true self.inputBarShouldFocusOnItemCalled = true
return self.inputBarShouldFocusOnItemResult return self.inputBarShouldFocusOnItemResult
} }
var inputBarDidReceiveFocusOnItemCalled = false var inputBarDidReceiveFocusOnItemCalled = false
var focusedItem: ChatInputItemProtocol? var focusedItem: ChatInputItemProtocol?
func inputBar(inputBar: ChatInputBar, didReceiveFocusOnItem item: ChatInputItemProtocol) { func inputBar(_ inputBar: ChatInputBar, didReceiveFocusOnItem item: ChatInputItemProtocol) {
self.inputBarDidReceiveFocusOnItemCalled = true self.inputBarDidReceiveFocusOnItemCalled = true
self.focusedItem = item self.focusedItem = item
} }

View File

@ -31,13 +31,13 @@ class ChatInputItemTests: XCTestCase {
@objc @objc
class MockInputItem: NSObject, ChatInputItemProtocol { class MockInputItem: NSObject, ChatInputItemProtocol {
var selected = false var selected = false
var presentationMode: ChatInputItemPresentationMode = .Keyboard var presentationMode: ChatInputItemPresentationMode = .keyboard
var showsSendButton = false var showsSendButton = false
var inputView: UIView? = nil var inputView: UIView? = nil
let tabView = UIView() let tabView = UIView()
private(set) var handledInput: AnyObject? private(set) var handledInput: AnyObject?
func handleInput(input: AnyObject) { func handleInput(_ input: AnyObject) {
self.handledInput = input self.handledInput = input
} }
} }

View File

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

View File

@ -58,7 +58,7 @@ class ChatInputPresenterTests: XCTestCase {
func testThat_GivenItemHasNonePresentationMode_WhenItemReceivesFocus_ItDoesntBecomeFocused() { func testThat_GivenItemHasNonePresentationMode_WhenItemReceivesFocus_ItDoesntBecomeFocused() {
let item = MockInputItem() let item = MockInputItem()
item.presentationMode = .None item.presentationMode = .none
self.presenter.onDidReceiveFocusOnItem(item) self.presenter.onDidReceiveFocusOnItem(item)
XCTAssertNil(self.presenter.focusedItem) XCTAssertNil(self.presenter.focusedItem)
} }
@ -95,7 +95,7 @@ class ChatInputPresenterTests: XCTestCase {
func testThat_GivenItemHasKeyboardPresentationMode_WhenItemReceivesFocus_PresenterShowsTextView() { func testThat_GivenItemHasKeyboardPresentationMode_WhenItemReceivesFocus_PresenterShowsTextView() {
self.bar.showsTextView = false self.bar.showsTextView = false
let item = MockInputItem() let item = MockInputItem()
item.presentationMode = .Keyboard item.presentationMode = .keyboard
self.presenter.onDidReceiveFocusOnItem(item) self.presenter.onDidReceiveFocusOnItem(item)
XCTAssertTrue(self.bar.showsTextView) XCTAssertTrue(self.bar.showsTextView)
} }
@ -103,7 +103,7 @@ class ChatInputPresenterTests: XCTestCase {
func testThat_GivenItemHasCustomViewPresentationMode_WhenItemReceivesFocus_PresenterHidesTextView() { func testThat_GivenItemHasCustomViewPresentationMode_WhenItemReceivesFocus_PresenterHidesTextView() {
self.bar.showsTextView = true self.bar.showsTextView = true
let item = MockInputItem() let item = MockInputItem()
item.presentationMode = .CustomView item.presentationMode = .customView
self.presenter.onDidReceiveFocusOnItem(item) self.presenter.onDidReceiveFocusOnItem(item)
XCTAssertFalse(self.bar.showsTextView) XCTAssertFalse(self.bar.showsTextView)
} }
@ -111,7 +111,7 @@ class ChatInputPresenterTests: XCTestCase {
func testThat_GivenItemHasNonePresentationMode_WhenItemReceivesFocus_PresenterDoesntHideTextView() { func testThat_GivenItemHasNonePresentationMode_WhenItemReceivesFocus_PresenterDoesntHideTextView() {
self.bar.showsTextView = true self.bar.showsTextView = true
let item = MockInputItem() let item = MockInputItem()
item.presentationMode = .None item.presentationMode = .none
self.presenter.onDidReceiveFocusOnItem(item) self.presenter.onDidReceiveFocusOnItem(item)
XCTAssertTrue(self.bar.showsTextView) XCTAssertTrue(self.bar.showsTextView)

View File

@ -30,16 +30,22 @@ class LiveCameraCellPresenterTests: XCTestCase {
var presenter: LiveCameraCellPresenter! var presenter: LiveCameraCellPresenter!
var cell: LiveCameraCell! var cell: LiveCameraCell!
var cameraAuthorizationStatus: AVAuthorizationStatus = .notDetermined
var cameraAuthorizationStatusProvider: LiveCameraCellPresenter.AVAuthorizationStatusProvider!
override func setUp() { override func setUp() {
super.setUp() super.setUp()
self.presenter = LiveCameraCellPresenter() self.cameraAuthorizationStatusProvider = { [unowned self] in
return self.cameraAuthorizationStatus
}
self.presenter = LiveCameraCellPresenter(authorizationStatusProvider: self.cameraAuthorizationStatusProvider)
self.cell = LiveCameraCell() self.cell = LiveCameraCell()
} }
override func tearDown() { override func tearDown() {
self.presenter = nil self.presenter = nil
self.cell = nil self.cell = nil
self.cameraAuthorizationStatusProvider = nil
super.tearDown() super.tearDown()
} }
@ -47,7 +53,7 @@ class LiveCameraCellPresenterTests: XCTestCase {
func testThat_WhenAuthorizationStatusIsNotDetermined_CaptureDoesntStart() { func testThat_WhenAuthorizationStatusIsNotDetermined_CaptureDoesntStart() {
let mockCaptureSession = MockLiveCameraCaptureSession() let mockCaptureSession = MockLiveCameraCaptureSession()
self.presenter.captureSession = mockCaptureSession self.presenter.captureSession = mockCaptureSession
self.presenter.cameraAuthorizationStatus = .NotDetermined self.cameraAuthorizationStatus = .notDetermined
self.presenter.cellWillBeShown(self.cell) self.presenter.cellWillBeShown(self.cell)
@ -57,7 +63,7 @@ class LiveCameraCellPresenterTests: XCTestCase {
func testThat_WhenAuthorizationStatusIsRestricted_CaptureDoesntStart() { func testThat_WhenAuthorizationStatusIsRestricted_CaptureDoesntStart() {
let mockCaptureSession = MockLiveCameraCaptureSession() let mockCaptureSession = MockLiveCameraCaptureSession()
self.presenter.captureSession = mockCaptureSession self.presenter.captureSession = mockCaptureSession
self.presenter.cameraAuthorizationStatus = .Restricted self.cameraAuthorizationStatus = .restricted
self.presenter.cellWillBeShown(self.cell) self.presenter.cellWillBeShown(self.cell)
@ -67,7 +73,7 @@ class LiveCameraCellPresenterTests: XCTestCase {
func testThat_WhenAuthorizationStatusIsDenied_CaptureDoesntStart() { func testThat_WhenAuthorizationStatusIsDenied_CaptureDoesntStart() {
let mockCaptureSession = MockLiveCameraCaptureSession() let mockCaptureSession = MockLiveCameraCaptureSession()
self.presenter.captureSession = mockCaptureSession self.presenter.captureSession = mockCaptureSession
self.presenter.cameraAuthorizationStatus = .Denied self.cameraAuthorizationStatus = .denied
self.presenter.cellWillBeShown(self.cell) self.presenter.cellWillBeShown(self.cell)
@ -77,7 +83,7 @@ class LiveCameraCellPresenterTests: XCTestCase {
func testThat_WhenAuthorizationStatusIsAuthorized_CaptureStarts() { func testThat_WhenAuthorizationStatusIsAuthorized_CaptureStarts() {
let mockCaptureSession = MockLiveCameraCaptureSession() let mockCaptureSession = MockLiveCameraCaptureSession()
self.presenter.captureSession = mockCaptureSession self.presenter.captureSession = mockCaptureSession
self.presenter.cameraAuthorizationStatus = .Authorized self.cameraAuthorizationStatus = .authorized
self.presenter.cellWillBeShown(self.cell) self.presenter.cellWillBeShown(self.cell)
@ -88,7 +94,7 @@ class LiveCameraCellPresenterTests: XCTestCase {
let mockCaptureSession = MockLiveCameraCaptureSession() let mockCaptureSession = MockLiveCameraCaptureSession()
mockCaptureSession.isCapturing = true mockCaptureSession.isCapturing = true
self.presenter.captureSession = mockCaptureSession self.presenter.captureSession = mockCaptureSession
self.presenter.cameraAuthorizationStatus = .Authorized self.cameraAuthorizationStatus = .authorized
self.presenter.cellWillBeShown(self.cell) self.presenter.cellWillBeShown(self.cell)
self.presenter.cellWasHidden(self.cell) self.presenter.cellWasHidden(self.cell)
@ -102,10 +108,10 @@ class LiveCameraCellPresenterTests: XCTestCase {
mockCaptureSession.isCapturing = true mockCaptureSession.isCapturing = true
self.presenter.captureSession = mockCaptureSession self.presenter.captureSession = mockCaptureSession
self.presenter.cameraAuthorizationStatus = .Authorized self.cameraAuthorizationStatus = .authorized
self.presenter.cellWillBeShown(self.cell) 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) XCTAssertFalse(mockCaptureSession.isCapturing)
} }
@ -115,11 +121,11 @@ class LiveCameraCellPresenterTests: XCTestCase {
mockCaptureSession.isCapturing = true mockCaptureSession.isCapturing = true
self.presenter.captureSession = mockCaptureSession self.presenter.captureSession = mockCaptureSession
self.presenter.cameraAuthorizationStatus = .Authorized self.cameraAuthorizationStatus = .authorized
self.presenter.cellWillBeShown(self.cell) self.presenter.cellWillBeShown(self.cell)
self.presenter.notificationCenter.postNotificationName(UIApplicationWillResignActiveNotification, object: nil) self.presenter.notificationCenter.post(name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
self.presenter.notificationCenter.postNotificationName(UIApplicationDidBecomeActiveNotification, object: nil) self.presenter.notificationCenter.post(name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)
XCTAssertTrue(mockCaptureSession.isCapturing) XCTAssertTrue(mockCaptureSession.isCapturing)
} }
@ -129,12 +135,12 @@ class LiveCameraCellPresenterTests: XCTestCase {
mockCaptureSession.isCapturing = false mockCaptureSession.isCapturing = false
self.presenter.captureSession = mockCaptureSession self.presenter.captureSession = mockCaptureSession
self.presenter.cameraAuthorizationStatus = .Authorized self.cameraAuthorizationStatus = .authorized
self.presenter.cellWillBeShown(self.cell) self.presenter.cellWillBeShown(self.cell)
self.presenter.cellWasHidden(self.cell) self.presenter.cellWasHidden(self.cell)
self.presenter.notificationCenter.postNotificationName(UIApplicationWillResignActiveNotification, object: nil) self.presenter.notificationCenter.post(name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
self.presenter.notificationCenter.postNotificationName(UIApplicationDidBecomeActiveNotification, object: nil) self.presenter.notificationCenter.post(name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)
XCTAssertFalse(mockCaptureSession.isCapturing) XCTAssertFalse(mockCaptureSession.isCapturing)
} }
@ -142,7 +148,7 @@ class LiveCameraCellPresenterTests: XCTestCase {
func testThat_WhenCellIsRemovedFromWindow_ThenCaptureIsStopped() { func testThat_WhenCellIsRemovedFromWindow_ThenCaptureIsStopped() {
let mockCaptureSession = MockLiveCameraCaptureSession() let mockCaptureSession = MockLiveCameraCaptureSession()
self.presenter.captureSession = mockCaptureSession self.presenter.captureSession = mockCaptureSession
self.presenter.cameraAuthorizationStatus = .Authorized self.cameraAuthorizationStatus = .authorized
self.presenter.cellWillBeShown(self.cell) self.presenter.cellWillBeShown(self.cell)
@ -153,7 +159,7 @@ class LiveCameraCellPresenterTests: XCTestCase {
func testThat_WhenReusedCellIsRemovedFromWindow_ThenCaptureIsNotStopped() { func testThat_WhenReusedCellIsRemovedFromWindow_ThenCaptureIsNotStopped() {
let mockCaptureSession = MockLiveCameraCaptureSession() let mockCaptureSession = MockLiveCameraCaptureSession()
self.presenter.captureSession = mockCaptureSession self.presenter.captureSession = mockCaptureSession
self.presenter.cameraAuthorizationStatus = .Authorized self.cameraAuthorizationStatus = .authorized
let firstCell = LiveCameraCell() let firstCell = LiveCameraCell()
self.presenter.cellWillBeShown(firstCell) self.presenter.cellWillBeShown(firstCell)
@ -166,7 +172,7 @@ class LiveCameraCellPresenterTests: XCTestCase {
func testThat_WhenCellIsReaddedToWindow_ThenCaputreIsRestarted() { func testThat_WhenCellIsReaddedToWindow_ThenCaputreIsRestarted() {
let mockCaptureSession = MockLiveCameraCaptureSession() let mockCaptureSession = MockLiveCameraCaptureSession()
self.presenter.captureSession = mockCaptureSession self.presenter.captureSession = mockCaptureSession
self.presenter.cameraAuthorizationStatus = .Authorized self.cameraAuthorizationStatus = .authorized
self.presenter.cellWillBeShown(self.cell) self.presenter.cellWillBeShown(self.cell)
self.cell.didMoveToWindow() self.cell.didMoveToWindow()
@ -180,14 +186,14 @@ class LiveCameraCellPresenterTests: XCTestCase {
func testThat_WhenReusedCellIsReaddedToWindow_ThenCaptureIsNotRestarted() { func testThat_WhenReusedCellIsReaddedToWindow_ThenCaptureIsNotRestarted() {
let mockCaptureSession = MockLiveCameraCaptureSession() let mockCaptureSession = MockLiveCameraCaptureSession()
self.presenter.captureSession = mockCaptureSession self.presenter.captureSession = mockCaptureSession
self.presenter.cameraAuthorizationStatus = .Authorized self.cameraAuthorizationStatus = .authorized
let firstCell = LiveCameraCell() let firstCell = LiveCameraCell()
self.presenter.cellWillBeShown(firstCell) self.presenter.cellWillBeShown(firstCell)
self.presenter.cellWillBeShown(self.cell) self.presenter.cellWillBeShown(self.cell)
self.cell.didMoveToWindow() self.cell.didMoveToWindow()
firstCell.willMoveToWindow(UIWindow()) firstCell.willMove(toWindow: UIWindow())
XCTAssertFalse(mockCaptureSession.isCapturing) XCTAssertFalse(mockCaptureSession.isCapturing)
} }
} }
@ -203,7 +209,7 @@ private class MockLiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
var isInitialized: Bool = false var isInitialized: Bool = false
var isCapturing: Bool = false var isCapturing: Bool = false
func startCapturing(completion: () -> Void) { func startCapturing(_ completion: @escaping () -> Void) {
guard !self.isCapturing else { return } guard !self.isCapturing else { return }
self.isInitialized = true self.isInitialized = true
@ -211,7 +217,7 @@ private class MockLiveCameraCaptureSession: LiveCameraCaptureSessionProtocol {
completion() completion()
} }
func stopCapturing(completion: () -> Void) { func stopCapturing(_ completion: @escaping () -> Void) {
guard self.isCapturing else { return } guard self.isCapturing else { return }
self.isInitialized = true self.isInitialized = true

View File

@ -33,7 +33,7 @@ class PhotosChatInputItemTests: XCTestCase {
} }
func testThat_PresentationModeIsCustomView() { func testThat_PresentationModeIsCustomView() {
XCTAssertEqual(self.inputItem.presentationMode, ChatInputItemPresentationMode.CustomView) XCTAssertEqual(self.inputItem.presentationMode, ChatInputItemPresentationMode.customView)
} }
func testThat_SendButtonDisabledForPhotosInputItem() { func testThat_SendButtonDisabledForPhotosInputItem() {
@ -54,7 +54,7 @@ class PhotosChatInputItemTests: XCTestCase {
self.inputItem.photoInputHandler = { image in self.inputItem.photoInputHandler = { image in
handled = true handled = true
} }
self.inputItem.handleInput(5) self.inputItem.handleInput(5 as AnyObject)
XCTAssertFalse(handled) XCTAssertFalse(handled)
} }

View File

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

View File

@ -28,7 +28,7 @@ import XCTest
class ObservableTests: XCTestCase { class ObservableTests: XCTestCase {
func testThatObserverClosureIsExecuted() { func testThatObserverClosureIsExecuted() {
var subject = Observable<Int>(0) let subject = Observable<Int>(0)
var executed = false var executed = false
subject.observe(self) { (old, new) -> () in subject.observe(self) { (old, new) -> () in
executed = true executed = true
@ -38,7 +38,7 @@ class ObservableTests: XCTestCase {
} }
func testThatObserverClosuresAreExecuted() { func testThatObserverClosuresAreExecuted() {
var subject = Observable<Int>(0) let subject = Observable<Int>(0)
var executed1 = false, executed2 = false var executed1 = false, executed2 = false
subject.observe(self) { (old, new) -> () in subject.observe(self) { (old, new) -> () in
executed1 = true executed1 = true
@ -52,7 +52,7 @@ class ObservableTests: XCTestCase {
} }
func testThatObserverClosureIsNotExecutedIfObserverWasDeallocated() { func testThatObserverClosureIsNotExecutedIfObserverWasDeallocated() {
var subject = Observable<Int>(0) let subject = Observable<Int>(0)
var observer: NSObject? = NSObject() var observer: NSObject? = NSObject()
var executed = false var executed = false
subject.observe(observer!) { (old, new) -> () in subject.observe(observer!) { (old, new) -> () in
@ -64,7 +64,7 @@ class ObservableTests: XCTestCase {
} }
func testNothingHappensIfNoObserversHaveBeenAdded() { func testNothingHappensIfNoObserversHaveBeenAdded() {
var subject = Observable<Int>(0) let subject = Observable<Int>(0)
subject.value = 1 subject.value = 1
XCTAssertEqual(1, subject.value) XCTAssertEqual(1, subject.value)
} }

View File

@ -10,7 +10,6 @@
C33FBFAE1BDE441C008E3545 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C33FBFAC1BDE441C008E3545 /* Main.storyboard */; }; C33FBFAE1BDE441C008E3545 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C33FBFAC1BDE441C008E3545 /* Main.storyboard */; };
C33FBFB01BDE441C008E3545 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C33FBFAF1BDE441C008E3545 /* Assets.xcassets */; }; C33FBFB01BDE441C008E3545 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C33FBFAF1BDE441C008E3545 /* Assets.xcassets */; };
C33FBFB31BDE441C008E3545 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C33FBFB11BDE441C008E3545 /* LaunchScreen.storyboard */; }; 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 */; }; C341D42E1C9635DF00FD3463 /* TimeSeparatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C341D42B1C9635DF00FD3463 /* TimeSeparatorModel.swift */; };
C341D42F1C9635DF00FD3463 /* TimeSeparatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C341D42C1C9635DF00FD3463 /* TimeSeparatorPresenter.swift */; }; C341D42F1C9635DF00FD3463 /* TimeSeparatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C341D42C1C9635DF00FD3463 /* TimeSeparatorPresenter.swift */; };
C341D4301C9635DF00FD3463 /* TimeSeparatorCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C341D42D1C9635DF00FD3463 /* TimeSeparatorCollectionViewCell.swift */; }; C341D4301C9635DF00FD3463 /* TimeSeparatorCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C341D42D1C9635DF00FD3463 /* TimeSeparatorCollectionViewCell.swift */; };
@ -45,13 +44,6 @@
remoteGlobalIDString = C33FBFA41BDE441C008E3545; remoteGlobalIDString = C33FBFA41BDE441C008E3545;
remoteInfo = ChattoApp; remoteInfo = ChattoApp;
}; };
C33FBFC51BDE441C008E3545 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = C33FBF9D1BDE441C008E3545 /* Project object */;
proxyType = 1;
remoteGlobalIDString = C33FBFA41BDE441C008E3545;
remoteInfo = ChattoApp;
};
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */ /* Begin PBXCopyFilesBuildPhase section */
@ -79,9 +71,6 @@
C33FBFB41BDE441C008E3545 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 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; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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; runOnlyForDeploymentPostprocessing = 0;
}; };
C33FBFC11BDE441C008E3545 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
@ -156,7 +138,6 @@
children = ( children = (
C33FBFA71BDE441C008E3545 /* ChattoApp */, C33FBFA71BDE441C008E3545 /* ChattoApp */,
C33FBFBC1BDE441C008E3545 /* ChattoAppTests */, C33FBFBC1BDE441C008E3545 /* ChattoAppTests */,
C33FBFC71BDE441C008E3545 /* ChattoAppUITests */,
C33FBFA61BDE441C008E3545 /* Products */, C33FBFA61BDE441C008E3545 /* Products */,
0852C8B139C7CFA0A1C22090 /* Frameworks */, 0852C8B139C7CFA0A1C22090 /* Frameworks */,
B616EDF620454A787C7E7D84 /* Pods */, B616EDF620454A787C7E7D84 /* Pods */,
@ -168,7 +149,6 @@
children = ( children = (
C33FBFA51BDE441C008E3545 /* ChattoApp.app */, C33FBFA51BDE441C008E3545 /* ChattoApp.app */,
C33FBFB91BDE441C008E3545 /* ChattoAppTests.xctest */, C33FBFB91BDE441C008E3545 /* ChattoAppTests.xctest */,
C33FBFC41BDE441C008E3545 /* ChattoAppUITests.xctest */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@ -194,15 +174,6 @@
path = ChattoAppTests; path = ChattoAppTests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
C33FBFC71BDE441C008E3545 /* ChattoAppUITests */ = {
isa = PBXGroup;
children = (
C33FBFC81BDE441C008E3545 /* ChattoAppUITests.swift */,
C33FBFCA1BDE441C008E3545 /* Info.plist */,
);
path = ChattoAppUITests;
sourceTree = "<group>";
};
C341D42A1C96359000FD3463 /* Time Separator */ = { C341D42A1C96359000FD3463 /* Time Separator */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -307,24 +278,6 @@
productReference = C33FBFB91BDE441C008E3545 /* ChattoAppTests.xctest */; productReference = C33FBFB91BDE441C008E3545 /* ChattoAppTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test"; 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 */ /* End PBXNativeTarget section */
/* Begin PBXProject section */ /* Begin PBXProject section */
@ -332,18 +285,16 @@
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastSwiftUpdateCheck = 0710; LastSwiftUpdateCheck = 0710;
LastUpgradeCheck = 0710; LastUpgradeCheck = 0800;
ORGANIZATIONNAME = Badoo; ORGANIZATIONNAME = Badoo;
TargetAttributes = { TargetAttributes = {
C33FBFA41BDE441C008E3545 = { C33FBFA41BDE441C008E3545 = {
CreatedOnToolsVersion = 7.1; CreatedOnToolsVersion = 7.1;
LastSwiftMigration = 0800;
}; };
C33FBFB81BDE441C008E3545 = { C33FBFB81BDE441C008E3545 = {
CreatedOnToolsVersion = 7.1; CreatedOnToolsVersion = 7.1;
TestTargetID = C33FBFA41BDE441C008E3545; LastSwiftMigration = 0800;
};
C33FBFC31BDE441C008E3545 = {
CreatedOnToolsVersion = 7.1;
TestTargetID = C33FBFA41BDE441C008E3545; TestTargetID = C33FBFA41BDE441C008E3545;
}; };
}; };
@ -363,7 +314,6 @@
targets = ( targets = (
C33FBFA41BDE441C008E3545 /* ChattoApp */, C33FBFA41BDE441C008E3545 /* ChattoApp */,
C33FBFB81BDE441C008E3545 /* ChattoAppTests */, C33FBFB81BDE441C008E3545 /* ChattoAppTests */,
C33FBFC31BDE441C008E3545 /* ChattoAppUITests */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
@ -387,13 +337,6 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
C33FBFC21BDE441C008E3545 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */
@ -437,7 +380,7 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; 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; showEnvVarsInLog = 0;
}; };
F8D7533B1E7B2E137B143EBD /* [CP] Copy Pods Resources */ = { F8D7533B1E7B2E137B143EBD /* [CP] Copy Pods Resources */ = {
@ -494,14 +437,6 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
C33FBFC01BDE441C008E3545 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C33FBFC91BDE441C008E3545 /* ChattoAppUITests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */ /* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */ /* Begin PBXTargetDependency section */
@ -510,11 +445,6 @@
target = C33FBFA41BDE441C008E3545 /* ChattoApp */; target = C33FBFA41BDE441C008E3545 /* ChattoApp */;
targetProxy = C33FBFBA1BDE441C008E3545 /* PBXContainerItemProxy */; targetProxy = C33FBFBA1BDE441C008E3545 /* PBXContainerItemProxy */;
}; };
C33FBFC61BDE441C008E3545 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = C33FBFA41BDE441C008E3545 /* ChattoApp */;
targetProxy = C33FBFC51BDE441C008E3545 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */ /* Begin PBXVariantGroup section */
@ -540,6 +470,7 @@
C33FBFCB1BDE441C008E3545 /* Debug */ = { C33FBFCB1BDE441C008E3545 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";
@ -550,8 +481,10 @@
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
@ -578,7 +511,7 @@
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 2.3; SWIFT_VERSION = 3.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };
name = Debug; name = Debug;
@ -586,6 +519,7 @@
C33FBFCC1BDE441C008E3545 /* Release */ = { C33FBFCC1BDE441C008E3545 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";
@ -596,8 +530,10 @@
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
@ -617,7 +553,7 @@
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 2.3; SWIFT_VERSION = 3.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES; VALIDATE_PRODUCT = YES;
}; };
@ -673,30 +609,6 @@
}; };
name = Release; 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 */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */ /* Begin XCConfigurationList section */
@ -727,15 +639,6 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
C33FBFD31BDE441C008E3545 /* Build configuration list for PBXNativeTarget "ChattoAppUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C33FBFD41BDE441C008E3545 /* Debug */,
C33FBFD51BDE441C008E3545 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */ /* End XCConfigurationList section */
}; };
rootObject = C33FBF9D1BDE441C008E3545 /* Project object */; rootObject = C33FBF9D1BDE441C008E3545 /* Project object */;

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "0720" LastUpgradeVersion = "0800"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "NO" parallelizeBuildables = "NO"
@ -39,16 +39,6 @@
ReferencedContainer = "container:ChattoApp.xcodeproj"> ReferencedContainer = "container:ChattoApp.xcodeproj">
</BuildableReference> </BuildableReference>
</TestableReference> </TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C33FBFC31BDE441C008E3545"
BuildableName = "ChattoAppUITests.xctest"
BlueprintName = "ChattoAppUITests"
ReferencedContainer = "container:ChattoApp.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables> </Testables>
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference

View File

@ -29,33 +29,31 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch. // Override point for customization after application launch.
return true 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. // 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. // 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. // 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. // 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. // 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. // 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:. // 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 import ChattoAdditions
class BaseMessageCollectionViewCellAvatarStyle: BaseMessageCollectionViewCellDefaultStyle { 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 // Display avatar for both incoming and outgoing messages for demo purpose
return CGSize(width: 35, height: 35) return CGSize(width: 35, height: 35)
} }

View File

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

Some files were not shown because too many files have changed in this diff Show More