diff --git a/.circleci/config.yml b/.circleci/config.yml index 07dbf76b2..e4c9ab212 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,11 +8,11 @@ jobs: build-and-test: environment: - - DESTINATION: "platform=iOS Simulator,name=iPhone 8" + - DESTINATION: "platform=iOS Simulator,name=iPhone XS" # Specify the Xcode version to use. macos: - xcode: "9.2.0" + xcode: "10.0.0" # Define the steps required to build the project. steps: @@ -22,17 +22,14 @@ jobs: - run: name: Update Homebrew command: brew update - - run: - name: Update carthage - command: brew upgrade carthage - run: name: Bootstrap Carthage command: carthage bootstrap --platform ios - run: - name: Biuld framework + name: Build framework command: xcodebuild build -project MessageKit.xcodeproj -scheme MessageKit -destination "$DESTINATION" CODE_SIGNING_REQUIRED=NO | xcpretty -c - run: - name: Biuld and run tests + name: Build and run tests command: xcodebuild test -project MessageKit.xcodeproj -scheme MessageKitTests -destination "$DESTINATION" CODE_SIGNING_REQUIRED=NO | xcpretty -c - run: name: Fetch CocoaPods Specs @@ -41,7 +38,7 @@ jobs: name: Update Pods command: cd Example && pod install - run: - name: Biuld and analyze Example + name: Build and analyze Example command: xcodebuild build analyze -workspace Example/ChatExample.xcworkspace -scheme ChatExample -destination "$DESTINATION" ONLY_ACTIVE_ARCH=NO CODE_SIGNING_REQUIRED=NO | xcpretty -c # Run tests. diff --git a/.gitignore b/.gitignore index 7cc39ae50..21eacaff4 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ Pods/ # Carthage Carthage +Carthage/Build +Carthage/Checkouts diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..9e3333c2e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "Carthage/Checkouts/Nimble"] + path = Carthage/Checkouts/Nimble + url = https://github.com/Quick/Nimble.git +[submodule "Carthage/Checkouts/Quick"] + path = Carthage/Checkouts/Quick + url = https://github.com/Quick/Quick.git +[submodule "Carthage/Checkouts/MessageInputBar"] + path = Carthage/Checkouts/MessageInputBar + url = https://github.com/ZkHaider/MessageInputBar.git diff --git a/.swift-version b/.swift-version index 5186d0706..bf77d5496 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -4.0 +4.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cd7961a2..d39ed49b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,63 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa -------------------------------------- -## Upcoming release +## [2.0.0](https://github.com/MessageKit/MessageKit/releases/tag/2.0.0) + +### Added + +- **Breaking Change** Added new methods to simplify using of custom messages: `customCellSizeCalculator(for:at:in:)` for `MessagesLayoutDelegate` and `customCell(for:at:in:)` for `MessagesDataSource`. +[#879](https://github.com/MessageKit/MessageKit/pull/879) by [@realbonus](https://github.com/RealBonus) + +### Changed + +- Change acl of `handleGesture(touchLocation:)` in `MessageLabel` from internal to open. +[#912](https://github.com/MessageKit/MessageKit/pull/912) by [@julienkode](https://github.com/JulienKode) + +## [2.0.0-beta.1](https://github.com/MessageKit/MessageKit/releases/tag/2.0.0-beta.1) ### Changed -- The `MessageData.emoji` case once again uses a default font of 2x the `messageLabelFont` size. +- **Breaking Change** Updated codebase to Swift 4.2 [#883](https://github.com/MessageKit/MessageKit/pull/883) by [@nathantannar4](https://github.com/nathantannar4) + +- Fixed the way that the Strings and UIImages are parsed in the `InputTextView` to prevent crashes in `parseForComponents()`. +[#791](https://github.com/MessageKit/MessageKit/pull/791) by [@nathantannar4](https://github.com/nathantannar4) + +### Added + +- Added support for detection and handling of `NSLink`s inside of messages. +[#815](https://github.com/MessageKit/MessageKit/pull/815) by [@jnic](https://github.com/jnic) + +- Added customizable `accessoryView`, with a new `MessagesDisplayDelegate` function `configureAccessoryView`, and corresponding size & padding properties in `MessageSizeCalculator`. The `accessoryView` is aligned to the center of the `messageContainerView`. +[#710](https://github.com/MessageKit/MessageKit/pull/710) by [@hyouuu](https://github.com/hyouuu) + +- Added a tap gesture recognition to the `accessoryView` which calls the `MessageCellDelagate` function `didTapAccessoryView(in:)`. +[#834](https://github.com/MessageKit/MessageKit/pull/834) by [@nathantannar4](https://github.com/nathantannar4) + +- Added `additionalBottomInset` property that allows to adjust the bottom content inset automatically set on the messages collection view by the view controller. +[#787](https://github.com/MessageKit/MessageKit/pull/787) by [@andreyvit](https://github.com/andreyvit) + +### Fixed + +- **Breaking Change** Fixed typo of `scrollsToBottomOnKeybordBeginsEditing` to `scrollsToBottomOnKeyboardBeginsEditing`. +[#856](https://github.com/MessageKit/MessageKit/pull/856) by [@p-petrenko](https://github.com/p-petrenko) + +- Fixed a bug that prevented `MessageLabel` from laying out properly when contained by superviews using autolayout. +[#889](https://github.com/MessageKit/MessageKit/pull/889) by [@marius-serban](https://github.com/marius-serban). + +- Fixed bottom content inset adjustment when using an undocked keyboard on iPad, or when `edgesForExtendedLayout` does not include `.top`, or when a parent container view controller adds extra views at the top of the screen. +[#787](https://github.com/MessageKit/MessageKit/pull/787) by [@andreyvit](https://github.com/andreyvit) + +- Fixed the `MessageData.emoji` case to use 2x the `messageLabelFont` size by default. [#795](https://github.com/MessageKit/MessageKit/pull/795) by [@Vortec4800](https://github.com/vortec4800). +### Fixed + +- Fixed `MessagesCollectionView` to allow to use nibs with `MessageReusableView`. +[#832](https://github.com/MessageKit/MessageKit/pull/832) by [@maxxx777](https://github.com/maxxx777). + +- Fixed multiple crashes at views, when views are being called from another XIB. +[#905](https://github.com/MessageKit/MessageKit/pull/905) by [@talanov](https://github.com/talanov). + ## [1.0.0](https://github.com/MessageKit/MessageKit/releases/tag/1.0.0) - First major release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6b37d5aec..eb2c5b40a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ ### Code of Conduct -Please read our [Code of Conduct](https://github.com/MessageKit/MessageKit/blob/master/Code_of_Conduct.md). +Please read our [Code of Conduct](https://github.com/MessageKit/MessageKit/blob/master/CODE_OF_CONDUCT.md). The MessageKit maintainers take this Code of Conduct very seriously. Intolerance, disrespect, harassment, and any form of negativity will not be tolerated. ### Ways to Contribute diff --git a/Cartfile b/Cartfile new file mode 100644 index 000000000..edf9a96f0 --- /dev/null +++ b/Cartfile @@ -0,0 +1 @@ +github "ZkHaider/MessageInputBar" "static" diff --git a/Cartfile.resolved b/Cartfile.resolved index afbb73e24..3dc68e4ea 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,2 +1,3 @@ -github "Quick/Nimble" "v7.0.3" -github "Quick/Quick" "v1.2.0" +github "Quick/Nimble" "v7.3.3" +github "Quick/Quick" "v1.3.4" +github "ZkHaider/MessageInputBar" "83e91fe05b75582fabce16cabf9ffa5646379906" diff --git a/Documentation/FAQs.md b/Documentation/FAQs.md index a112794d0..5ed0760fe 100644 --- a/Documentation/FAQs.md +++ b/Documentation/FAQs.md @@ -1,6 +1,13 @@ # MessageKit Frequently Asked Questions -**Why doesn't the `MessageInputBar` appear in my controller?** +- [Why doesn't the `MessageInputBar` appear in my controller?](#why-doesnt-the-messageinputbar-appear-in-my-controller) +- [How can I remove the `AvatarView` from the cell?](#how-can-i-remove-the-avatarview-from-the-cell) +- [How can I move the `AvatarView` to prevent it from overlapping text in the `MessageBottomLabel` or `CellTopLabel`?](#how-can-i-move-the-avatarview-to-prevent-it-from-overlapping-text-in-the-messagebottomlabel-or-celltoplabel) +- [How can I dismiss the keyboard?](#how-can-i-dismiss-the-keyboard) +- [How can I get a reference to the `MessageType` in the `MessageCellDelegate` methods?](#how-can-i-get-a-reference-to-the-messagetype-in-the-messagecelldelegate-methods) + + +## Why doesn't the `MessageInputBar` appear in my controller? If you're using the `MessagesViewController` as a child view controller then you have to call `becomeFirstResponder()` on your view controller. The @@ -21,7 +28,7 @@ class ParentVC: UIViewController { } ``` -**How can I remove the `AvatarView` from the cell?** +## How can I remove the `AvatarView` from the cell? You can set the `AvatarView` to hidden through the `configureAvatarView(_:AvatarView,for:MessageType,at:IndexPath,in:MessagesCollectionView)` method of `MessagesDisplayDelegate`. @@ -52,7 +59,7 @@ if let layout = messagesCollectionView.collectionViewLayout as? MessagesCollecti } ``` -**How can I move the `AvatarView` to prevent it from overlapping text in the `MessageBottomLabel` or `CellTopLabel`?** +## How can I move the `AvatarView` to prevent it from overlapping text in the `MessageBottomLabel` or `CellTopLabel`? If you have resized the `AvatarView` to be larger than the default size in MessageKit then you may notice that the `AvatarView` overlaps text either in the `MessageBottomLabel` or `CallTopLabel`. MessageKit allows the `AvatarView` @@ -75,7 +82,7 @@ if let layout = messagesCollectionView.collectionViewLayout as? MessagesCollecti There are other options provided as well so take a look at the `AvatarPosition` struct to see what they are. -**How can I dismiss the keyboard?** +## How can I dismiss the keyboard? The `MessagesViewController` needs to resign the first responder. @@ -84,7 +91,7 @@ let controller = MessagesViewController() controller.resignFirstResponder() ``` -**How can I get a reference to the `MessageType` in the `MessageCellDelegate` methods?** +## How can I get a reference to the `MessageType` in the `MessageCellDelegate` methods? You can use `UICollectionView`'s method `indexPath(for: UICollectionViewCell)` to get the `IndexPath` for the `MessageCollectionViewCell` argument. Using this `IndexPath`, you can diff --git a/Example/ChatExample.xcodeproj/project.pbxproj b/Example/ChatExample.xcodeproj/project.pbxproj index 85cef9321..2b7dc8da8 100644 --- a/Example/ChatExample.xcodeproj/project.pbxproj +++ b/Example/ChatExample.xcodeproj/project.pbxproj @@ -7,21 +7,31 @@ objects = { /* Begin PBXBuildFile section */ - 37D3EAC41F390E5F00DD6A55 /* SampleData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D3EAC31F390E5F00DD6A55 /* SampleData.swift */; }; + 385C2922211FF32E0010B4BA /* CustomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C2920211FF32E0010B4BA /* CustomCell.swift */; }; + 385C2923211FF32E0010B4BA /* TableViewCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C2921211FF32E0010B4BA /* TableViewCells.swift */; }; + 385C2927211FF33B0010B4BA /* MockSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C2925211FF33A0010B4BA /* MockSocket.swift */; }; + 385C2928211FF33B0010B4BA /* MockMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C2926211FF33B0010B4BA /* MockMessage.swift */; }; + 385C292B211FF3450010B4BA /* Settings+UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C292A211FF3450010B4BA /* Settings+UserDefaults.swift */; }; + 385C292D211FF3520010B4BA /* CustomMessageFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C292C211FF3520010B4BA /* CustomMessageFlowLayout.swift */; }; + 385C2931211FF3630010B4BA /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C292F211FF3630010B4BA /* UIColor+Extensions.swift */; }; + 385C2932211FF3630010B4BA /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C2930211FF3630010B4BA /* UIViewController+Extensions.swift */; }; + 385C2937211FF37B0010B4BA /* SampleData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C2934211FF37A0010B4BA /* SampleData.swift */; }; + 385C2939211FF37B0010B4BA /* Lorem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C2936211FF37B0010B4BA /* Lorem.swift */; }; + 385C2942211FF38F0010B4BA /* AdvancedExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C293B211FF38E0010B4BA /* AdvancedExampleViewController.swift */; }; + 385C2943211FF38F0010B4BA /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C293C211FF38E0010B4BA /* SettingsViewController.swift */; }; + 385C2944211FF38F0010B4BA /* MessageContainerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C293D211FF38F0010B4BA /* MessageContainerController.swift */; }; + 385C2945211FF38F0010B4BA /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C293E211FF38F0010B4BA /* NavigationController.swift */; }; + 385C2946211FF38F0010B4BA /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C293F211FF38F0010B4BA /* LaunchViewController.swift */; }; + 385C2947211FF38F0010B4BA /* BasicExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C2940211FF38F0010B4BA /* BasicExampleViewController.swift */; }; + 385C2948211FF38F0010B4BA /* ChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C2941211FF38F0010B4BA /* ChatViewController.swift */; }; 882B5E811CF7D53600B6E160 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882B5E781CF7D53600B6E160 /* AppDelegate.swift */; }; 882B5E821CF7D53600B6E160 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 882B5E791CF7D53600B6E160 /* Assets.xcassets */; }; 882B5E831CF7D53600B6E160 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 882B5E7A1CF7D53600B6E160 /* LaunchScreen.storyboard */; }; - 882B5E851CF7D53600B6E160 /* InboxViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882B5E7E1CF7D53600B6E160 /* InboxViewController.swift */; }; - 882B5E871CF7D53600B6E160 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882B5E801CF7D53600B6E160 /* SettingsViewController.swift */; }; 882B5E901CF7D56000B6E160 /* ChatExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882B5E8E1CF7D56000B6E160 /* ChatExampleUITests.swift */; }; 882B5E951CF7D56E00B6E160 /* ChatExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882B5E931CF7D56E00B6E160 /* ChatExampleTests.swift */; }; A444625E4F1C8CB1B33A17F8 /* Pods_ChatExampleUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2AC6E3F5C11E39F57598DBE6 /* Pods_ChatExampleUITests.framework */; }; - B0655A331F23E90800542A83 /* ConversationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0655A321F23E90800542A83 /* ConversationViewController.swift */; }; - B096438B1F288D47004D0129 /* MockMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B096438A1F288D47004D0129 /* MockMessage.swift */; }; C1DF6DF39F66906000EC76CF /* Pods_ChatExampleTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56F0AC85B38034EC92CCBC7D /* Pods_ChatExampleTests.framework */; }; C7CA53A1B85256A5097E7DC7 /* Pods_ChatExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B316705C4717C3B4C916D62 /* Pods_ChatExample.framework */; }; - CAB36EA12007A573009995ED /* TableViewCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB36EA02007A573009995ED /* TableViewCells.swift */; }; - CAB36EA32007B1B7009995ED /* Settings+UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB36EA22007B1B7009995ED /* Settings+UserDefaults.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -57,7 +67,23 @@ /* Begin PBXFileReference section */ 0364943D08CDBE656E6F6DF8 /* Pods-ChatExampleTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatExampleTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ChatExampleTests/Pods-ChatExampleTests.debug.xcconfig"; sourceTree = ""; }; 2AC6E3F5C11E39F57598DBE6 /* Pods_ChatExampleUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ChatExampleUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 37D3EAC31F390E5F00DD6A55 /* SampleData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleData.swift; sourceTree = ""; }; + 385C2920211FF32E0010B4BA /* CustomCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomCell.swift; sourceTree = ""; }; + 385C2921211FF32E0010B4BA /* TableViewCells.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewCells.swift; sourceTree = ""; }; + 385C2925211FF33A0010B4BA /* MockSocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockSocket.swift; sourceTree = ""; }; + 385C2926211FF33B0010B4BA /* MockMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockMessage.swift; sourceTree = ""; }; + 385C292A211FF3450010B4BA /* Settings+UserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Settings+UserDefaults.swift"; sourceTree = ""; }; + 385C292C211FF3520010B4BA /* CustomMessageFlowLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomMessageFlowLayout.swift; sourceTree = ""; }; + 385C292F211FF3630010B4BA /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; + 385C2930211FF3630010B4BA /* UIViewController+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extensions.swift"; sourceTree = ""; }; + 385C2934211FF37A0010B4BA /* SampleData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleData.swift; sourceTree = ""; }; + 385C2936211FF37B0010B4BA /* Lorem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Lorem.swift; sourceTree = ""; }; + 385C293B211FF38E0010B4BA /* AdvancedExampleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdvancedExampleViewController.swift; sourceTree = ""; }; + 385C293C211FF38E0010B4BA /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; + 385C293D211FF38F0010B4BA /* MessageContainerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageContainerController.swift; sourceTree = ""; }; + 385C293E211FF38F0010B4BA /* NavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = ""; }; + 385C293F211FF38F0010B4BA /* LaunchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = ""; }; + 385C2940211FF38F0010B4BA /* BasicExampleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicExampleViewController.swift; sourceTree = ""; }; + 385C2941211FF38F0010B4BA /* ChatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatViewController.swift; sourceTree = ""; }; 3B316705C4717C3B4C916D62 /* Pods_ChatExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ChatExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 56F0AC85B38034EC92CCBC7D /* Pods_ChatExampleTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ChatExampleTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 882B5E331CF7D4B900B6E160 /* ChatExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ChatExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -66,22 +92,16 @@ 882B5E781CF7D53600B6E160 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 882B5E791CF7D53600B6E160 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 882B5E7B1CF7D53600B6E160 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 882B5E7E1CF7D53600B6E160 /* InboxViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InboxViewController.swift; sourceTree = ""; }; 882B5E7F1CF7D53600B6E160 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 882B5E801CF7D53600B6E160 /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; 882B5E8E1CF7D56000B6E160 /* ChatExampleUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatExampleUITests.swift; sourceTree = ""; }; 882B5E8F1CF7D56000B6E160 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 882B5E931CF7D56E00B6E160 /* ChatExampleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatExampleTests.swift; sourceTree = ""; }; 882B5E941CF7D56E00B6E160 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9E0D67CD75BA7EB323FD391B /* Pods-ChatExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatExample.release.xcconfig"; path = "Pods/Target Support Files/Pods-ChatExample/Pods-ChatExample.release.xcconfig"; sourceTree = ""; }; A830E27DBE0B66B89C5D2EB8 /* Pods-ChatExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatExample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ChatExample/Pods-ChatExample.debug.xcconfig"; sourceTree = ""; }; - B0655A321F23E90800542A83 /* ConversationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationViewController.swift; sourceTree = ""; }; - B096438A1F288D47004D0129 /* MockMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockMessage.swift; sourceTree = ""; }; B0DD3C951C9D064B5E6D6644 /* Pods-ChatExampleUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatExampleUITests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ChatExampleUITests/Pods-ChatExampleUITests.debug.xcconfig"; sourceTree = ""; }; B2F1C412A96DE613A0AC31F8 /* Pods-ChatExampleUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatExampleUITests.release.xcconfig"; path = "Pods/Target Support Files/Pods-ChatExampleUITests/Pods-ChatExampleUITests.release.xcconfig"; sourceTree = ""; }; BFE5859D088A740A7D43E1B1 /* Pods-ChatExampleTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatExampleTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-ChatExampleTests/Pods-ChatExampleTests.release.xcconfig"; sourceTree = ""; }; - CAB36EA02007A573009995ED /* TableViewCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewCells.swift; sourceTree = ""; }; - CAB36EA22007B1B7009995ED /* Settings+UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Settings+UserDefaults.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -112,6 +132,64 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 385C2924211FF3310010B4BA /* Views */ = { + isa = PBXGroup; + children = ( + 385C2920211FF32E0010B4BA /* CustomCell.swift */, + 385C2921211FF32E0010B4BA /* TableViewCells.swift */, + ); + path = Views; + sourceTree = ""; + }; + 385C2929211FF33D0010B4BA /* Models */ = { + isa = PBXGroup; + children = ( + 385C2926211FF33B0010B4BA /* MockMessage.swift */, + 385C2925211FF33A0010B4BA /* MockSocket.swift */, + ); + path = Models; + sourceTree = ""; + }; + 385C292E211FF3540010B4BA /* Layout */ = { + isa = PBXGroup; + children = ( + 385C292C211FF3520010B4BA /* CustomMessageFlowLayout.swift */, + ); + path = Layout; + sourceTree = ""; + }; + 385C2933211FF3670010B4BA /* Extensions */ = { + isa = PBXGroup; + children = ( + 385C292F211FF3630010B4BA /* UIColor+Extensions.swift */, + 385C2930211FF3630010B4BA /* UIViewController+Extensions.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 385C293A211FF3800010B4BA /* Data Generation */ = { + isa = PBXGroup; + children = ( + 385C2936211FF37B0010B4BA /* Lorem.swift */, + 385C2934211FF37A0010B4BA /* SampleData.swift */, + ); + path = "Data Generation"; + sourceTree = ""; + }; + 385C2949211FF3930010B4BA /* View Controllers */ = { + isa = PBXGroup; + children = ( + 385C293B211FF38E0010B4BA /* AdvancedExampleViewController.swift */, + 385C2940211FF38F0010B4BA /* BasicExampleViewController.swift */, + 385C2941211FF38F0010B4BA /* ChatViewController.swift */, + 385C293F211FF38F0010B4BA /* LaunchViewController.swift */, + 385C293D211FF38F0010B4BA /* MessageContainerController.swift */, + 385C293E211FF38F0010B4BA /* NavigationController.swift */, + 385C293C211FF38E0010B4BA /* SettingsViewController.swift */, + ); + path = "View Controllers"; + sourceTree = ""; + }; 7523553CCB4B87460CE4DED6 /* Pods */ = { isa = PBXGroup; children = ( @@ -151,16 +229,16 @@ isa = PBXGroup; children = ( 882B5E781CF7D53600B6E160 /* AppDelegate.swift */, - B0655A321F23E90800542A83 /* ConversationViewController.swift */, - 37D3EAC31F390E5F00DD6A55 /* SampleData.swift */, - 882B5E7E1CF7D53600B6E160 /* InboxViewController.swift */, - B096438A1F288D47004D0129 /* MockMessage.swift */, - 882B5E801CF7D53600B6E160 /* SettingsViewController.swift */, + 385C2949211FF3930010B4BA /* View Controllers */, + 385C293A211FF3800010B4BA /* Data Generation */, + 385C292E211FF3540010B4BA /* Layout */, + 385C2929211FF33D0010B4BA /* Models */, + 385C2924211FF3310010B4BA /* Views */, 882B5E791CF7D53600B6E160 /* Assets.xcassets */, 882B5E7F1CF7D53600B6E160 /* Info.plist */, 882B5E7A1CF7D53600B6E160 /* LaunchScreen.storyboard */, - CAB36EA02007A573009995ED /* TableViewCells.swift */, - CAB36EA22007B1B7009995ED /* Settings+UserDefaults.swift */, + 385C292A211FF3450010B4BA /* Settings+UserDefaults.swift */, + 385C2933211FF3670010B4BA /* Extensions */, ); path = Sources; sourceTree = ""; @@ -267,7 +345,6 @@ TargetAttributes = { 882B5E321CF7D4B900B6E160 = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = T26M4385KK; LastSwiftMigration = 0800; }; 882B5E481CF7D4B900B6E160 = { @@ -371,10 +448,12 @@ ); inputPaths = ( "${SRCROOT}/Pods/Target Support Files/Pods-ChatExample/Pods-ChatExample-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/MessageInputBar/MessageInputBar.framework", "${BUILT_PRODUCTS_DIR}/MessageKit/MessageKit.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MessageInputBar.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MessageKit.framework", ); runOnlyForDeploymentPostprocessing = 0; @@ -420,14 +499,24 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - CAB36EA12007A573009995ED /* TableViewCells.swift in Sources */, - CAB36EA32007B1B7009995ED /* Settings+UserDefaults.swift in Sources */, - 882B5E871CF7D53600B6E160 /* SettingsViewController.swift in Sources */, - 37D3EAC41F390E5F00DD6A55 /* SampleData.swift in Sources */, - B096438B1F288D47004D0129 /* MockMessage.swift in Sources */, + 385C2922211FF32E0010B4BA /* CustomCell.swift in Sources */, + 385C2946211FF38F0010B4BA /* LaunchViewController.swift in Sources */, + 385C2939211FF37B0010B4BA /* Lorem.swift in Sources */, + 385C2928211FF33B0010B4BA /* MockMessage.swift in Sources */, + 385C2923211FF32E0010B4BA /* TableViewCells.swift in Sources */, + 385C2942211FF38F0010B4BA /* AdvancedExampleViewController.swift in Sources */, + 385C2937211FF37B0010B4BA /* SampleData.swift in Sources */, + 385C2931211FF3630010B4BA /* UIColor+Extensions.swift in Sources */, + 385C2932211FF3630010B4BA /* UIViewController+Extensions.swift in Sources */, + 385C292B211FF3450010B4BA /* Settings+UserDefaults.swift in Sources */, + 385C2945211FF38F0010B4BA /* NavigationController.swift in Sources */, + 385C2948211FF38F0010B4BA /* ChatViewController.swift in Sources */, + 385C2944211FF38F0010B4BA /* MessageContainerController.swift in Sources */, + 385C2943211FF38F0010B4BA /* SettingsViewController.swift in Sources */, + 385C2927211FF33B0010B4BA /* MockSocket.swift in Sources */, + 385C2947211FF38F0010B4BA /* BasicExampleViewController.swift in Sources */, 882B5E811CF7D53600B6E160 /* AppDelegate.swift in Sources */, - B0655A331F23E90800542A83 /* ConversationViewController.swift in Sources */, - 882B5E851CF7D53600B6E160 /* InboxViewController.swift in Sources */, + 385C292D211FF3520010B4BA /* CustomMessageFlowLayout.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -587,12 +676,12 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = T26M4385KK; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.messagekit.ChatExample; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; }; name = Debug; }; @@ -602,13 +691,13 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = T26M4385KK; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.messagekit.ChatExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; }; name = Release; }; @@ -621,7 +710,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.ChatExampleTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChatExample.app/ChatExample"; }; name = Debug; @@ -636,7 +725,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.ChatExampleTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChatExample.app/ChatExample"; }; name = Release; @@ -649,7 +738,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.ChatExampleUITests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TEST_TARGET_NAME = ChatExample; }; name = Debug; @@ -663,7 +752,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.ChatExampleUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TEST_TARGET_NAME = ChatExample; }; name = Release; diff --git a/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExample.xcscheme b/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExample.xcscheme index cebf89c06..0fa1ed4fa 100644 --- a/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExample.xcscheme +++ b/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExample.xcscheme @@ -1,6 +1,6 @@ '../' + pod 'MessageInputBar', :git => 'https://github.com/MessageKit/MessageInputBar.git', :branch => 'master' target 'ChatExampleTests' do inherit! :search_paths diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 008857df0..6d8fef549 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,16 +1,30 @@ PODS: - - MessageKit (0.13.4) + - MessageInputBar (0.4.1): + - MessageInputBar/Core (= 0.4.1) + - MessageInputBar/Core (0.4.1) + - MessageKit (1.0.0): + - MessageInputBar/Core DEPENDENCIES: + - MessageInputBar (from `https://github.com/MessageKit/MessageInputBar.git`, branch `master`) - MessageKit (from `../`) EXTERNAL SOURCES: + MessageInputBar: + :branch: master + :git: https://github.com/MessageKit/MessageInputBar.git MessageKit: :path: "../" +CHECKOUT OPTIONS: + MessageInputBar: + :commit: faebe27f2dd8f39ea145e75b7296ef48133c099a + :git: https://github.com/MessageKit/MessageInputBar.git + SPEC CHECKSUMS: - MessageKit: f51122543e9ba8b521e5eab58b0385bd7ce47a10 + MessageInputBar: e81c7535347f1f7b923de7080409a535a004b6e4 + MessageKit: 2bbd13dd6a7c06f42f2d13ed8871d1fe5383b477 -PODFILE CHECKSUM: cecdb7bc8129cf99f66de9f68eea3256fec30c3d +PODFILE CHECKSUM: 04c1a805e1997e83bacab1a34787e71e9fe4432b -COCOAPODS: 1.5.0 +COCOAPODS: 1.5.3 diff --git a/Example/Sources/AppDelegate.swift b/Example/Sources/AppDelegate.swift index dabbec32a..ea28caaae 100644 --- a/Example/Sources/AppDelegate.swift +++ b/Example/Sources/AppDelegate.swift @@ -29,13 +29,17 @@ final internal class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? = nil) -> Bool { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - UIApplication.shared.statusBarStyle = .lightContent window = UIWindow(frame: UIScreen.main.bounds) - window?.backgroundColor = .white - window?.rootViewController = UINavigationController(rootViewController: InboxViewController()) + window?.rootViewController = NavigationController(rootViewController: LaunchViewController()) window?.makeKeyAndVisible() + + if UserDefaults.isFirstLaunch() { + // Enable Text Messages + UserDefaults.standard.set(true, forKey: "Text Messages") + } + return true } diff --git a/Example/Sources/Assets.xcassets/Nathan-Tannar.imageset/Contents.json b/Example/Sources/Assets.xcassets/Nathan-Tannar.imageset/Contents.json new file mode 100644 index 000000000..353f2b0f3 --- /dev/null +++ b/Example/Sources/Assets.xcassets/Nathan-Tannar.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Nathan.jpg", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Sources/Assets.xcassets/Nathan-Tannar.imageset/Nathan.jpg b/Example/Sources/Assets.xcassets/Nathan-Tannar.imageset/Nathan.jpg new file mode 100644 index 000000000..db542b890 Binary files /dev/null and b/Example/Sources/Assets.xcassets/Nathan-Tannar.imageset/Nathan.jpg differ diff --git a/Example/Sources/Assets.xcassets/Steven-Deutsch.imageset/7445580.jpeg b/Example/Sources/Assets.xcassets/Steven-Deutsch.imageset/7445580.jpeg new file mode 100644 index 000000000..23d0ea92f Binary files /dev/null and b/Example/Sources/Assets.xcassets/Steven-Deutsch.imageset/7445580.jpeg differ diff --git a/Example/Sources/Assets.xcassets/Steven-Deutsch.imageset/Contents.json b/Example/Sources/Assets.xcassets/Steven-Deutsch.imageset/Contents.json new file mode 100644 index 000000000..b654b6600 --- /dev/null +++ b/Example/Sources/Assets.xcassets/Steven-Deutsch.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "7445580.jpeg", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Sources/Assets.xcassets/Wu-Zhong.imageset/5061845.png b/Example/Sources/Assets.xcassets/Wu-Zhong.imageset/5061845.png new file mode 100644 index 000000000..cf189e466 Binary files /dev/null and b/Example/Sources/Assets.xcassets/Wu-Zhong.imageset/5061845.png differ diff --git a/Example/Sources/Assets.xcassets/Wu-Zhong.imageset/Contents.json b/Example/Sources/Assets.xcassets/Wu-Zhong.imageset/Contents.json new file mode 100644 index 000000000..09237a158 --- /dev/null +++ b/Example/Sources/Assets.xcassets/Wu-Zhong.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "5061845.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Sources/Assets.xcassets/ic_appstore.imageset/Contents.json b/Example/Sources/Assets.xcassets/ic_appstore.imageset/Contents.json new file mode 100644 index 000000000..16f766d27 --- /dev/null +++ b/Example/Sources/Assets.xcassets/ic_appstore.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "icons8-apple_app_store_filled.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Sources/Assets.xcassets/ic_appstore.imageset/icons8-apple_app_store_filled.png b/Example/Sources/Assets.xcassets/ic_appstore.imageset/icons8-apple_app_store_filled.png new file mode 100644 index 000000000..0abc0e2be Binary files /dev/null and b/Example/Sources/Assets.xcassets/ic_appstore.imageset/icons8-apple_app_store_filled.png differ diff --git a/Example/Sources/Assets.xcassets/ic_at.imageset/Contents.json b/Example/Sources/Assets.xcassets/ic_at.imageset/Contents.json index 0c1081dcc..edee1feee 100644 --- a/Example/Sources/Assets.xcassets/ic_at.imageset/Contents.json +++ b/Example/Sources/Assets.xcassets/ic_at.imageset/Contents.json @@ -2,11 +2,11 @@ "images" : [ { "idiom" : "universal", - "filename" : "icons8-email_filled.png", "scale" : "1x" }, { "idiom" : "universal", + "filename" : "icons8-email.png", "scale" : "2x" }, { diff --git a/Example/Sources/Assets.xcassets/ic_at.imageset/icons8-email.png b/Example/Sources/Assets.xcassets/ic_at.imageset/icons8-email.png new file mode 100644 index 000000000..de6e9812c Binary files /dev/null and b/Example/Sources/Assets.xcassets/ic_at.imageset/icons8-email.png differ diff --git a/Example/Sources/Assets.xcassets/ic_at.imageset/icons8-email_filled.png b/Example/Sources/Assets.xcassets/ic_at.imageset/icons8-email_filled.png deleted file mode 100644 index 47190eeea..000000000 Binary files a/Example/Sources/Assets.xcassets/ic_at.imageset/icons8-email_filled.png and /dev/null differ diff --git a/Example/Sources/Assets.xcassets/ic_camera.imageset/Contents.json b/Example/Sources/Assets.xcassets/ic_camera.imageset/Contents.json index d1d32d39b..c556046fa 100644 --- a/Example/Sources/Assets.xcassets/ic_camera.imageset/Contents.json +++ b/Example/Sources/Assets.xcassets/ic_camera.imageset/Contents.json @@ -2,11 +2,11 @@ "images" : [ { "idiom" : "universal", - "filename" : "icons8-compact_camera_filled.png", "scale" : "1x" }, { "idiom" : "universal", + "filename" : "icons8-camera.png", "scale" : "2x" }, { diff --git a/Example/Sources/Assets.xcassets/ic_camera.imageset/icons8-camera.png b/Example/Sources/Assets.xcassets/ic_camera.imageset/icons8-camera.png new file mode 100644 index 000000000..9b609a178 Binary files /dev/null and b/Example/Sources/Assets.xcassets/ic_camera.imageset/icons8-camera.png differ diff --git a/Example/Sources/Assets.xcassets/ic_camera.imageset/icons8-compact_camera_filled.png b/Example/Sources/Assets.xcassets/ic_camera.imageset/icons8-compact_camera_filled.png deleted file mode 100644 index ad358bf2c..000000000 Binary files a/Example/Sources/Assets.xcassets/ic_camera.imageset/icons8-compact_camera_filled.png and /dev/null differ diff --git a/Example/Sources/Assets.xcassets/ic_hashtag.imageset/Contents.json b/Example/Sources/Assets.xcassets/ic_hashtag.imageset/Contents.json index ea5b62c8d..d2a53e432 100644 --- a/Example/Sources/Assets.xcassets/ic_hashtag.imageset/Contents.json +++ b/Example/Sources/Assets.xcassets/ic_hashtag.imageset/Contents.json @@ -2,11 +2,11 @@ "images" : [ { "idiom" : "universal", - "filename" : "icons8-hashtag_filled.png", "scale" : "1x" }, { "idiom" : "universal", + "filename" : "icons8-hashtag.png", "scale" : "2x" }, { diff --git a/Example/Sources/Assets.xcassets/ic_hashtag.imageset/icons8-hashtag.png b/Example/Sources/Assets.xcassets/ic_hashtag.imageset/icons8-hashtag.png new file mode 100644 index 000000000..cd1157559 Binary files /dev/null and b/Example/Sources/Assets.xcassets/ic_hashtag.imageset/icons8-hashtag.png differ diff --git a/Example/Sources/Assets.xcassets/ic_hashtag.imageset/icons8-hashtag_filled.png b/Example/Sources/Assets.xcassets/ic_hashtag.imageset/icons8-hashtag_filled.png deleted file mode 100644 index 30430f013..000000000 Binary files a/Example/Sources/Assets.xcassets/ic_hashtag.imageset/icons8-hashtag_filled.png and /dev/null differ diff --git a/Example/Sources/Assets.xcassets/ic_info.imageset/Contents.json b/Example/Sources/Assets.xcassets/ic_info.imageset/Contents.json new file mode 100644 index 000000000..2382af9e0 --- /dev/null +++ b/Example/Sources/Assets.xcassets/ic_info.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "icons8-info.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Sources/Assets.xcassets/ic_info.imageset/icons8-info.png b/Example/Sources/Assets.xcassets/ic_info.imageset/icons8-info.png new file mode 100644 index 000000000..f5ea857fe Binary files /dev/null and b/Example/Sources/Assets.xcassets/ic_info.imageset/icons8-info.png differ diff --git a/Example/Sources/Assets.xcassets/ic_library.imageset/Contents.json b/Example/Sources/Assets.xcassets/ic_library.imageset/Contents.json index 1687bc7a1..204bf39f4 100644 --- a/Example/Sources/Assets.xcassets/ic_library.imageset/Contents.json +++ b/Example/Sources/Assets.xcassets/ic_library.imageset/Contents.json @@ -2,11 +2,11 @@ "images" : [ { "idiom" : "universal", - "filename" : "icons8-stack_of_photos_filled.png", "scale" : "1x" }, { "idiom" : "universal", + "filename" : "icons8-medium_icons.png", "scale" : "2x" }, { diff --git a/Example/Sources/Assets.xcassets/ic_library.imageset/icons8-medium_icons.png b/Example/Sources/Assets.xcassets/ic_library.imageset/icons8-medium_icons.png new file mode 100644 index 000000000..392c9d2d8 Binary files /dev/null and b/Example/Sources/Assets.xcassets/ic_library.imageset/icons8-medium_icons.png differ diff --git a/Example/Sources/Assets.xcassets/ic_library.imageset/icons8-stack_of_photos_filled.png b/Example/Sources/Assets.xcassets/ic_library.imageset/icons8-stack_of_photos_filled.png deleted file mode 100644 index acf958072..000000000 Binary files a/Example/Sources/Assets.xcassets/ic_library.imageset/icons8-stack_of_photos_filled.png and /dev/null differ diff --git a/Example/Sources/Assets.xcassets/ic_like.imageset/Contents.json b/Example/Sources/Assets.xcassets/ic_like.imageset/Contents.json new file mode 100644 index 000000000..6332ba232 --- /dev/null +++ b/Example/Sources/Assets.xcassets/ic_like.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "icons8-like_it.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Sources/Assets.xcassets/ic_like.imageset/icons8-like_it.png b/Example/Sources/Assets.xcassets/ic_like.imageset/icons8-like_it.png new file mode 100644 index 000000000..d2aa2f99b Binary files /dev/null and b/Example/Sources/Assets.xcassets/ic_like.imageset/icons8-like_it.png differ diff --git a/Example/Sources/Assets.xcassets/ic_map_marker.imageset/Contents.json b/Example/Sources/Assets.xcassets/ic_map_marker.imageset/Contents.json new file mode 100644 index 000000000..87bc913a7 --- /dev/null +++ b/Example/Sources/Assets.xcassets/ic_map_marker.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "icons8-map_pin-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Sources/Assets.xcassets/ic_map_marker.imageset/icons8-map_pin-1.png b/Example/Sources/Assets.xcassets/ic_map_marker.imageset/icons8-map_pin-1.png new file mode 100644 index 000000000..b62d10c48 Binary files /dev/null and b/Example/Sources/Assets.xcassets/ic_map_marker.imageset/icons8-map_pin-1.png differ diff --git a/Example/Sources/Assets.xcassets/ic_mic.imageset/Contents.json b/Example/Sources/Assets.xcassets/ic_mic.imageset/Contents.json new file mode 100644 index 000000000..f17544f33 --- /dev/null +++ b/Example/Sources/Assets.xcassets/ic_mic.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "icons8-microphone.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Sources/Assets.xcassets/ic_mic.imageset/icons8-microphone.png b/Example/Sources/Assets.xcassets/ic_mic.imageset/icons8-microphone.png new file mode 100644 index 000000000..bda90ff4b Binary files /dev/null and b/Example/Sources/Assets.xcassets/ic_mic.imageset/icons8-microphone.png differ diff --git a/Example/Sources/Assets.xcassets/ic_send.imageset/Contents.json b/Example/Sources/Assets.xcassets/ic_send.imageset/Contents.json new file mode 100644 index 000000000..129c162ec --- /dev/null +++ b/Example/Sources/Assets.xcassets/ic_send.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "icons8-sent.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Sources/Assets.xcassets/ic_send.imageset/icons8-sent.png b/Example/Sources/Assets.xcassets/ic_send.imageset/icons8-sent.png new file mode 100644 index 000000000..81e7d3ecf Binary files /dev/null and b/Example/Sources/Assets.xcassets/ic_send.imageset/icons8-sent.png differ diff --git a/Example/Sources/Assets.xcassets/img1.imageset/Contents.json b/Example/Sources/Assets.xcassets/img1.imageset/Contents.json new file mode 100644 index 000000000..5c29b7d1a --- /dev/null +++ b/Example/Sources/Assets.xcassets/img1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "grey-crowned-crane-bird-crane-animal-45853.jpeg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Sources/Assets.xcassets/img1.imageset/grey-crowned-crane-bird-crane-animal-45853.jpeg b/Example/Sources/Assets.xcassets/img1.imageset/grey-crowned-crane-bird-crane-animal-45853.jpeg new file mode 100644 index 000000000..526e2e548 Binary files /dev/null and b/Example/Sources/Assets.xcassets/img1.imageset/grey-crowned-crane-bird-crane-animal-45853.jpeg differ diff --git a/Example/Sources/Assets.xcassets/img2.imageset/Contents.json b/Example/Sources/Assets.xcassets/img2.imageset/Contents.json new file mode 100644 index 000000000..e2a2d16ae --- /dev/null +++ b/Example/Sources/Assets.xcassets/img2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "pexels-photo-145939.jpeg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Sources/Assets.xcassets/img2.imageset/pexels-photo-145939.jpeg b/Example/Sources/Assets.xcassets/img2.imageset/pexels-photo-145939.jpeg new file mode 100644 index 000000000..3edeaf18e Binary files /dev/null and b/Example/Sources/Assets.xcassets/img2.imageset/pexels-photo-145939.jpeg differ diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift deleted file mode 100644 index a11c5f8d0..000000000 --- a/Example/Sources/ConversationViewController.swift +++ /dev/null @@ -1,461 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import UIKit -import MessageKit -import MapKit - -internal class ConversationViewController: MessagesViewController { - - let refreshControl = UIRefreshControl() - - var messageList: [MockMessage] = [] - - var isTyping = false - - lazy var formatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateStyle = .medium - return formatter - }() - - override func viewDidLoad() { - super.viewDidLoad() - - let messagesToFetch = UserDefaults.standard.mockMessagesCount() - - DispatchQueue.global(qos: .userInitiated).async { - SampleData.shared.getMessages(count: messagesToFetch) { messages in - DispatchQueue.main.async { - self.messageList = messages - self.messagesCollectionView.reloadData() - self.messagesCollectionView.scrollToBottom() - } - } - } - - messagesCollectionView.messagesDataSource = self - messagesCollectionView.messagesLayoutDelegate = self - messagesCollectionView.messagesDisplayDelegate = self - messagesCollectionView.messageCellDelegate = self - messageInputBar.delegate = self - - messageInputBar.sendButton.tintColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) - scrollsToBottomOnKeybordBeginsEditing = true // default false - maintainPositionOnKeyboardFrameChanged = true // default false - - messagesCollectionView.addSubview(refreshControl) - refreshControl.addTarget(self, action: #selector(ConversationViewController.loadMoreMessages), for: .valueChanged) - - navigationItem.rightBarButtonItems = [ - UIBarButtonItem(image: UIImage(named: "ic_keyboard"), - style: .plain, - target: self, - action: #selector(ConversationViewController.handleKeyboardButton)), - UIBarButtonItem(image: UIImage(named: "ic_typing"), - style: .plain, - target: self, - action: #selector(ConversationViewController.handleTyping)) - ] - } - - @objc func handleTyping() { - - defer { - isTyping = !isTyping - } - - if isTyping { - - messageInputBar.topStackView.arrangedSubviews.first?.removeFromSuperview() - messageInputBar.topStackViewPadding = .zero - - } else { - - let label = UILabel() - label.text = "nathan.tannar is typing..." - label.font = UIFont.boldSystemFont(ofSize: 16) - messageInputBar.topStackView.addArrangedSubview(label) - messageInputBar.topStackViewPadding.top = 6 - messageInputBar.topStackViewPadding.left = 12 - - // The backgroundView doesn't include the topStackView. This is so things in the topStackView can have transparent backgrounds if you need it that way or another color all together - messageInputBar.backgroundColor = messageInputBar.backgroundView.backgroundColor - - } - - } - - @objc func loadMoreMessages() { - DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: DispatchTime.now() + 4) { - SampleData.shared.getMessages(count: 10) { messages in - DispatchQueue.main.async { - self.messageList.insert(contentsOf: messages, at: 0) - self.messagesCollectionView.reloadDataAndKeepOffset() - self.refreshControl.endRefreshing() - } - } - } - } - - @objc func handleKeyboardButton() { - - messageInputBar.inputTextView.resignFirstResponder() - let actionSheetController = UIAlertController(title: "Change Keyboard Style", message: nil, preferredStyle: .actionSheet) - let actions = [ - UIAlertAction(title: "Slack", style: .default, handler: { _ in - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1, execute: { - self.slack() - }) - }), - UIAlertAction(title: "iMessage", style: .default, handler: { _ in - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1, execute: { - self.iMessage() - }) - }), - UIAlertAction(title: "Default", style: .default, handler: { _ in - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1, execute: { - self.defaultStyle() - }) - }), - UIAlertAction(title: "Cancel", style: .cancel, handler: nil) - ] - actions.forEach { actionSheetController.addAction($0) } - actionSheetController.view.tintColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) - present(actionSheetController, animated: true, completion: nil) - } - - // MARK: - Keyboard Style - - func slack() { - defaultStyle() - messageInputBar.backgroundView.backgroundColor = .white - messageInputBar.isTranslucent = false - messageInputBar.inputTextView.backgroundColor = .clear - messageInputBar.inputTextView.layer.borderWidth = 0 - let items = [ - makeButton(named: "ic_camera").onTextViewDidChange { button, textView in - button.isEnabled = textView.text.isEmpty - }, - makeButton(named: "ic_at").onSelected { - $0.tintColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) - }, - makeButton(named: "ic_hashtag").onSelected { - $0.tintColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) - }, - .flexibleSpace, - makeButton(named: "ic_library").onTextViewDidChange { button, textView in - button.tintColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) - button.isEnabled = textView.text.isEmpty - }, - messageInputBar.sendButton - .configure { - $0.layer.cornerRadius = 8 - $0.layer.borderWidth = 1.5 - $0.layer.borderColor = $0.titleColor(for: .disabled)?.cgColor - $0.setTitleColor(.white, for: .normal) - $0.setTitleColor(.white, for: .highlighted) - $0.setSize(CGSize(width: 52, height: 30), animated: true) - }.onDisabled { - $0.layer.borderColor = $0.titleColor(for: .disabled)?.cgColor - $0.backgroundColor = .white - }.onEnabled { - $0.backgroundColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) - $0.layer.borderColor = UIColor.clear.cgColor - }.onSelected { - // We use a transform becuase changing the size would cause the other views to relayout - $0.transform = CGAffineTransform(scaleX: 1.2, y: 1.2) - }.onDeselected { - $0.transform = CGAffineTransform.identity - } - ] - items.forEach { $0.tintColor = .lightGray } - - // We can change the container insets if we want - messageInputBar.inputTextView.textContainerInset = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0) - messageInputBar.inputTextView.placeholderLabelInsets = UIEdgeInsets(top: 8, left: 5, bottom: 8, right: 5) - - // Since we moved the send button to the bottom stack lets set the right stack width to 0 - messageInputBar.setRightStackViewWidthConstant(to: 0, animated: true) - - // Finally set the items - messageInputBar.setStackViewItems(items, forStack: .bottom, animated: true) - } - - func iMessage() { - defaultStyle() - messageInputBar.isTranslucent = false - messageInputBar.backgroundView.backgroundColor = .white - messageInputBar.separatorLine.isHidden = true - messageInputBar.inputTextView.backgroundColor = UIColor(red: 245/255, green: 245/255, blue: 245/255, alpha: 1) - messageInputBar.inputTextView.placeholderTextColor = UIColor(red: 0.6, green: 0.6, blue: 0.6, alpha: 1) - messageInputBar.inputTextView.textContainerInset = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 36) - messageInputBar.inputTextView.placeholderLabelInsets = UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 36) - messageInputBar.inputTextView.layer.borderColor = UIColor(red: 200/255, green: 200/255, blue: 200/255, alpha: 1).cgColor - messageInputBar.inputTextView.layer.borderWidth = 1.0 - messageInputBar.inputTextView.layer.cornerRadius = 16.0 - messageInputBar.inputTextView.layer.masksToBounds = true - messageInputBar.inputTextView.scrollIndicatorInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0) - messageInputBar.setRightStackViewWidthConstant(to: 36, animated: true) - messageInputBar.setStackViewItems([messageInputBar.sendButton], forStack: .right, animated: true) - messageInputBar.sendButton.imageView?.backgroundColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) - messageInputBar.sendButton.contentEdgeInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) - messageInputBar.sendButton.setSize(CGSize(width: 36, height: 36), animated: true) - messageInputBar.sendButton.image = #imageLiteral(resourceName: "ic_up") - messageInputBar.sendButton.title = nil - messageInputBar.sendButton.imageView?.layer.cornerRadius = 16 - messageInputBar.sendButton.backgroundColor = .clear - messageInputBar.textViewPadding.right = -38 - } - - func defaultStyle() { - let newMessageInputBar = MessageInputBar() - newMessageInputBar.sendButton.tintColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) - newMessageInputBar.delegate = self - messageInputBar = newMessageInputBar - reloadInputViews() - } - - // MARK: - Helpers - - func makeButton(named: String) -> InputBarButtonItem { - return InputBarButtonItem() - .configure { - $0.spacing = .fixed(10) - $0.image = UIImage(named: named)?.withRenderingMode(.alwaysTemplate) - $0.setSize(CGSize(width: 30, height: 30), animated: true) - }.onSelected { - $0.tintColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) - }.onDeselected { - $0.tintColor = UIColor.lightGray - }.onTouchUpInside { _ in - print("Item Tapped") - } - } -} - -// MARK: - MessagesDataSource - -extension ConversationViewController: MessagesDataSource { - - func currentSender() -> Sender { - return SampleData.shared.currentSender - } - - func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int { - return messageList.count - } - - func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType { - return messageList[indexPath.section] - } - - func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { - if indexPath.section % 3 == 0 { - return NSAttributedString(string: MessageKitDateFormatter.shared.string(from: message.sentDate), attributes: [NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 10), NSAttributedStringKey.foregroundColor: UIColor.darkGray]) - } - return nil - } - - func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { - let name = message.sender.displayName - return NSAttributedString(string: name, attributes: [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: .caption1)]) - } - - func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { - - let dateString = formatter.string(from: message.sentDate) - return NSAttributedString(string: dateString, attributes: [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: .caption2)]) - } - -} - -// MARK: - MessagesDisplayDelegate - -extension ConversationViewController: MessagesDisplayDelegate { - - // MARK: - Text Messages - - func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { - return isFromCurrentSender(message: message) ? .white : .darkText - } - - func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedStringKey: Any] { - return MessageLabel.defaultAttributes - } - - func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] { - return [.url, .address, .phoneNumber, .date, .transitInformation] - } - - // MARK: - All Messages - - func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { - return isFromCurrentSender(message: message) ? UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) : UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1) - } - - func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle { - let corner: MessageStyle.TailCorner = isFromCurrentSender(message: message) ? .bottomRight : .bottomLeft - return .bubbleTail(corner, .curved) -// let configurationClosure = { (view: MessageContainerView) in} -// return .custom(configurationClosure) - } - - func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { - let avatar = SampleData.shared.getAvatarFor(sender: message.sender) - avatarView.set(avatar: avatar) - } - - // MARK: - Location Messages - - func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? { - let annotationView = MKAnnotationView(annotation: nil, reuseIdentifier: nil) - let pinImage = #imageLiteral(resourceName: "pin") - annotationView.image = pinImage - annotationView.centerOffset = CGPoint(x: 0, y: -pinImage.size.height / 2) - return annotationView - } - - func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? { - return { view in - view.layer.transform = CATransform3DMakeScale(0, 0, 0) - view.alpha = 0.0 - UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0, options: [], animations: { - view.layer.transform = CATransform3DIdentity - view.alpha = 1.0 - }, completion: nil) - } - } - - func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions { - - return LocationMessageSnapshotOptions() - } -} - -// MARK: - MessagesLayoutDelegate - -extension ConversationViewController: MessagesLayoutDelegate { - - func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - if indexPath.section % 3 == 0 { - return 10 - } - return 0 - } - - func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return 16 - } - - func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return 16 - } - -} - -// MARK: - MessageCellDelegate - -extension ConversationViewController: MessageCellDelegate { - - func didTapAvatar(in cell: MessageCollectionViewCell) { - print("Avatar tapped") - } - - func didTapMessage(in cell: MessageCollectionViewCell) { - print("Message tapped") - } - - func didTapCellTopLabel(in cell: MessageCollectionViewCell) { - print("Top cell label tapped") - } - - func didTapMessageTopLabel(in cell: MessageCollectionViewCell) { - print("Top message label tapped") - } - - func didTapMessageBottomLabel(in cell: MessageCollectionViewCell) { - print("Bottom label tapped") - } - -} - -// MARK: - MessageLabelDelegate - -extension ConversationViewController: MessageLabelDelegate { - - func didSelectAddress(_ addressComponents: [String: String]) { - print("Address Selected: \(addressComponents)") - } - - func didSelectDate(_ date: Date) { - print("Date Selected: \(date)") - } - - func didSelectPhoneNumber(_ phoneNumber: String) { - print("Phone Number Selected: \(phoneNumber)") - } - - func didSelectURL(_ url: URL) { - print("URL Selected: \(url)") - } - - func didSelectTransitInformation(_ transitInformation: [String: String]) { - print("TransitInformation Selected: \(transitInformation)") - } - -} - -// MARK: - MessageInputBarDelegate - -extension ConversationViewController: MessageInputBarDelegate { - - func messageInputBar(_ inputBar: MessageInputBar, didPressSendButtonWith text: String) { - - // Each NSTextAttachment that contains an image will count as one empty character in the text: String - - for component in inputBar.inputTextView.components { - - if let image = component as? UIImage { - - let imageMessage = MockMessage(image: image, sender: currentSender(), messageId: UUID().uuidString, date: Date()) - messageList.append(imageMessage) - messagesCollectionView.insertSections([messageList.count - 1]) - - } else if let text = component as? String { - - let attributedText = NSAttributedString(string: text, attributes: [.font: UIFont.systemFont(ofSize: 15), .foregroundColor: UIColor.blue]) - - let message = MockMessage(attributedText: attributedText, sender: currentSender(), messageId: UUID().uuidString, date: Date()) - messageList.append(message) - messagesCollectionView.insertSections([messageList.count - 1]) - } - - } - - inputBar.inputTextView.text = String() - messagesCollectionView.scrollToBottom() - } - -} diff --git a/Example/Sources/Data Generation/Lorem.swift b/Example/Sources/Data Generation/Lorem.swift new file mode 100755 index 000000000..b36e22b41 --- /dev/null +++ b/Example/Sources/Data Generation/Lorem.swift @@ -0,0 +1,317 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + 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 + +public class Lorem { + private static let wordList = [ + "alias", "consequatur", "aut", "perferendis", "sit", "voluptatem", + "accusantium", "doloremque", "aperiam", "eaque", "ipsa", "quae", "ab", + "illo", "inventore", "veritatis", "et", "quasi", "architecto", + "beatae", "vitae", "dicta", "sunt", "explicabo", "aspernatur", "aut", + "odit", "aut", "fugit", "sed", "quia", "consequuntur", "magni", + "dolores", "eos", "qui", "ratione", "voluptatem", "sequi", "nesciunt", + "neque", "dolorem", "ipsum", "quia", "dolor", "sit", "amet", + "consectetur", "adipisci", "velit", "sed", "quia", "non", "numquam", + "eius", "modi", "tempora", "incidunt", "ut", "labore", "et", "dolore", + "magnam", "aliquam", "quaerat", "voluptatem", "ut", "enim", "ad", + "minima", "veniam", "quis", "nostrum", "exercitationem", "ullam", + "corporis", "nemo", "enim", "ipsam", "voluptatem", "quia", "voluptas", + "sit", "suscipit", "laboriosam", "nisi", "ut", "aliquid", "ex", "ea", + "commodi", "consequatur", "quis", "autem", "vel", "eum", "iure", + "reprehenderit", "qui", "in", "ea", "voluptate", "velit", "esse", + "quam", "nihil", "molestiae", "et", "iusto", "odio", "dignissimos", + "ducimus", "qui", "blanditiis", "praesentium", "laudantium", "totam", + "rem", "voluptatum", "deleniti", "atque", "corrupti", "quos", + "dolores", "et", "quas", "molestias", "excepturi", "sint", + "occaecati", "cupiditate", "non", "provident", "sed", "ut", + "perspiciatis", "unde", "omnis", "iste", "natus", "error", + "similique", "sunt", "in", "culpa", "qui", "officia", "deserunt", + "mollitia", "animi", "id", "est", "laborum", "et", "dolorum", "fuga", + "et", "harum", "quidem", "rerum", "facilis", "est", "et", "expedita", + "distinctio", "nam", "libero", "tempore", "cum", "soluta", "nobis", + "est", "eligendi", "optio", "cumque", "nihil", "impedit", "quo", + "porro", "quisquam", "est", "qui", "minus", "id", "quod", "maxime", + "placeat", "facere", "possimus", "omnis", "voluptas", "assumenda", + "est", "omnis", "dolor", "repellendus", "temporibus", "autem", + "quibusdam", "et", "aut", "consequatur", "vel", "illum", "qui", + "dolorem", "eum", "fugiat", "quo", "voluptas", "nulla", "pariatur", + "at", "vero", "eos", "et", "accusamus", "officiis", "debitis", "aut", + "rerum", "necessitatibus", "saepe", "eveniet", "ut", "et", + "voluptates", "repudiandae", "sint", "et", "molestiae", "non", + "recusandae", "itaque", "earum", "rerum", "hic", "tenetur", "a", + "sapiente", "delectus", "ut", "aut", "reiciendis", "voluptatibus", + "maiores", "doloribus", "asperiores", "repellat" + ] + + /** + Return a random word. + + - returns: Returns a random word. + */ + public class func word() -> String { + return wordList.random()! + } + + /** + Return an array of `count` words. + + - parameter count: The number of words to return. + + - returns: Returns an array of `count` words. + */ + public class func words(nbWords: Int = 3) -> [String] { + return wordList.random(nbWords) + } + + /** + Return a string of `count` words. + + - parameter count: The number of words the string should contain. + + - returns: Returns a string of `count` words. + */ + public class func words(nbWords: Int = 3) -> String { + return words(nbWords: nbWords).joined(separator: " ") + } + + /** + Generate a sentence of `nbWords` words. + - parameter nbWords: The number of words the sentence should contain. + - parameter variable: If `true`, the number of words will vary between + +/- 40% of `nbWords`. + - returns: + */ + public class func sentence(nbWords: Int = 6, variable: Bool = true) -> String { + if nbWords <= 0 { + return "" + } + + let result: String = self.words(nbWords: variable ? nbWords.randomize(variation: 40) : nbWords) + + return result.firstCapitalized + "." + } + + /** + Generate an array of sentences. + - parameter nbSentences: The number of sentences to generate. + + - returns: Returns an array of random sentences. + */ + public class func sentences(nbSentences: Int = 3) -> [String] { + return (0.. String { + if nbSentences <= 0 { + return "" + } + + return sentences(nbSentences: variable ? nbSentences.randomize(variation: 40) : nbSentences).joined(separator: " ") + } + + /** + Generate an array of random paragraphs. + - parameter nbParagraphs: The number of paragraphs to generate. + - returns: Returns an array of `nbParagraphs` paragraphs. + */ + public class func paragraphs(nbParagraphs: Int = 3) -> [String] { + return (0.. String { + return paragraphs(nbParagraphs: nbParagraphs).joined(separator: "\n\n") + } + + /** + Generate a string of at most `maxNbChars` characters. + - parameter maxNbChars: The maximum number of characters the string + should contain. + - returns: Returns a string of at most `maxNbChars` characters. + */ + public class func text(maxNbChars: Int = 200) -> String { + var result: [String] = [] + + if maxNbChars < 5 { + return "" + } else if maxNbChars < 25 { + while result.count == 0 { + var size = 0 + + while size < maxNbChars { + let w = (size != 0 ? " " : "") + word() + result.append(w) + size += w.count + } + + _ = result.popLast() + } + } else if maxNbChars < 100 { + while result.count == 0 { + var size = 0 + + while size < maxNbChars { + let s = (size != 0 ? " " : "") + sentence() + result.append(s) + size += s.count + } + + _ = result.popLast() + } + } else { + while result.count == 0 { + var size = 0 + + while size < maxNbChars { + let p = (size != 0 ? "\n" : "") + paragraph() + result.append(p) + size += p.count + } + + _ = result.popLast() + } + } + + return result.joined(separator: "") + } +} + +extension String { + var firstCapitalized: String { + var string = self + string.replaceSubrange(string.startIndex...string.startIndex, with: String(string[string.startIndex]).capitalized) + return string + } +} + +public extension Array { + /** + Shuffle the array in-place using the Fisher-Yates algorithm. + */ + public mutating func shuffle() { + for i in 0..<(count - 1) { + let j = Int(arc4random_uniform(UInt32(count - i))) + i + if j != i { + self.swapAt(i, j) + } + } + } + + /** + Return a shuffled version of the array using the Fisher-Yates + algorithm. + + - returns: Returns a shuffled version of the array. + */ + public func shuffled() -> [Element] { + var list = self + list.shuffle() + + return list + } + + /** + Return a random element from the array. + - returns: Returns a random element from the array or `nil` if the + array is empty. + */ + public func random() -> Element? { + return (count > 0) ? self.shuffled()[0] : nil + } + + /** + Return a random subset of `cnt` elements from the array. + - returns: Returns a random subset of `cnt` elements from the array. + */ + public func random(_ count: Int = 1) -> [Element] { + let result = shuffled() + + return (count > result.count) ? result : Array(result[0.. Int { + precondition(min <= max, "attempt to call random() with min > max") + + let diff = UInt(bitPattern: max &- min) + let result = UInt.random(min: 0, max: diff) + + return min + Int(bitPattern: result) + } + + public func randomize(variation: Int) -> Int { + let multiplier = Double(Int.random(min: 100 - variation, max: 100 + variation)) / 100 + let randomized = Double(self) * multiplier + + return Int(randomized) + 1 + } +} + +private extension UInt { + static func random(min: UInt, max: UInt) -> UInt { + precondition(min <= max, "attempt to call random() with min > max") + + if min == UInt.min && max == UInt.max { + var result: UInt = 0 + arc4random_buf(&result, MemoryLayout.size(ofValue: result)) + + return result + } else { + let range = max - min + 1 + let limit = UInt.max - UInt.max % range + var result: UInt = 0 + + repeat { + arc4random_buf(&result, MemoryLayout.size(ofValue: result)) + } while result >= limit + + result = result % range + + return min + result + } + } +} diff --git a/Example/Sources/Data Generation/SampleData.swift b/Example/Sources/Data Generation/SampleData.swift new file mode 100644 index 000000000..07b47218c --- /dev/null +++ b/Example/Sources/Data Generation/SampleData.swift @@ -0,0 +1,238 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + 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 MessageKit +import CoreLocation + +final internal class SampleData { + + static let shared = SampleData() + + private init() {} + + enum MessageTypes: UInt32, CaseIterable { + case Text = 0 + case AttributedText = 1 + case Photo = 2 + case Video = 3 + case Emoji = 4 + case Location = 5 + case Url = 6 + case Phone = 7 + case Custom = 8 + + static func random() -> MessageTypes { + // Update as new enumerations are added + let maxValue = Custom.rawValue + + let rand = arc4random_uniform(maxValue+1) + return MessageTypes(rawValue: rand)! + } + } + + let system = Sender(id: "000000", displayName: "System") + let nathan = Sender(id: "000001", displayName: "Nathan Tannar") + let steven = Sender(id: "000002", displayName: "Steven Deutsch") + let wu = Sender(id: "000003", displayName: "Wu Zhong") + + lazy var senders = [nathan, steven, wu] + + var currentSender: Sender { + return nathan + } + + var now = Date() + + let messageImages: [UIImage] = [#imageLiteral(resourceName: "img1"), #imageLiteral(resourceName: "img2")] + + let emojis = [ + "👍", + "😂😂😂", + "👋👋👋", + "😱😱😱", + "😃😃😃", + "❤️" + ] + + let attributes = ["Font1", "Font2", "Font3", "Font4", "Color", "Combo"] + + let locations: [CLLocation] = [ + CLLocation(latitude: 37.3118, longitude: -122.0312), + CLLocation(latitude: 33.6318, longitude: -100.0386), + CLLocation(latitude: 29.3358, longitude: -108.8311), + CLLocation(latitude: 39.3218, longitude: -127.4312), + CLLocation(latitude: 35.3218, longitude: -127.4314), + CLLocation(latitude: 39.3218, longitude: -113.3317) + ] + + func attributedString(with text: String) -> NSAttributedString { + let nsString = NSString(string: text) + var mutableAttributedString = NSMutableAttributedString(string: text) + let randomAttribute = Int(arc4random_uniform(UInt32(attributes.count))) + let range = NSRange(location: 0, length: nsString.length) + + switch attributes[randomAttribute] { + case "Font1": + mutableAttributedString.addAttribute(NSAttributedString.Key.font, value: UIFont.preferredFont(forTextStyle: .body), range: range) + case "Font2": + mutableAttributedString.addAttributes([NSAttributedString.Key.font: UIFont.monospacedDigitSystemFont(ofSize: UIFont.systemFontSize, weight: UIFont.Weight.bold)], range: range) + case "Font3": + mutableAttributedString.addAttributes([NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: UIFont.systemFontSize)], range: range) + case "Font4": + mutableAttributedString.addAttributes([NSAttributedString.Key.font: UIFont.italicSystemFont(ofSize: UIFont.systemFontSize)], range: range) + case "Color": + mutableAttributedString.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.red], range: range) + case "Combo": + let msg9String = "Use .attributedText() to add bold, italic, colored text and more..." + let msg9Text = NSString(string: msg9String) + let msg9AttributedText = NSMutableAttributedString(string: String(msg9Text)) + + msg9AttributedText.addAttribute(NSAttributedString.Key.font, value: UIFont.preferredFont(forTextStyle: .body), range: NSRange(location: 0, length: msg9Text.length)) + msg9AttributedText.addAttributes([NSAttributedString.Key.font: UIFont.monospacedDigitSystemFont(ofSize: UIFont.systemFontSize, weight: UIFont.Weight.bold)], range: msg9Text.range(of: ".attributedText()")) + msg9AttributedText.addAttributes([NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: UIFont.systemFontSize)], range: msg9Text.range(of: "bold")) + msg9AttributedText.addAttributes([NSAttributedString.Key.font: UIFont.italicSystemFont(ofSize: UIFont.systemFontSize)], range: msg9Text.range(of: "italic")) + msg9AttributedText.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.red], range: msg9Text.range(of: "colored")) + mutableAttributedString = msg9AttributedText + default: + fatalError("Unrecognized attribute for mock message") + } + + return NSAttributedString(attributedString: mutableAttributedString) + } + + func dateAddingRandomTime() -> Date { + let randomNumber = Int(arc4random_uniform(UInt32(10))) + if randomNumber % 2 == 0 { + let date = Calendar.current.date(byAdding: .hour, value: randomNumber, to: now)! + now = date + return date + } else { + let randomMinute = Int(arc4random_uniform(UInt32(59))) + let date = Calendar.current.date(byAdding: .minute, value: randomMinute, to: now)! + now = date + return date + } + } + + func randomMessageType() -> MessageTypes { + let messageType = MessageTypes.random() + + if !UserDefaults.standard.bool(forKey: "\(messageType)" + " Messages") { + return randomMessageType() + } + + return messageType + } + + func randomMessage(allowedSenders: [Sender]) -> MockMessage { + + let randomNumberSender = Int(arc4random_uniform(UInt32(allowedSenders.count))) + + let uniqueID = NSUUID().uuidString + let sender = allowedSenders[randomNumberSender] + let date = dateAddingRandomTime() + + switch randomMessageType() { + case .Text: + let randomSentence = Lorem.sentence() + return MockMessage(text: randomSentence, sender: sender, messageId: uniqueID, date: date) + case .AttributedText: + let randomSentence = Lorem.sentence() + let attributedText = attributedString(with: randomSentence) + return MockMessage(attributedText: attributedText, sender: senders[randomNumberSender], messageId: uniqueID, date: date) + case .Photo: + let randomNumberImage = Int(arc4random_uniform(UInt32(messageImages.count))) + let image = messageImages[randomNumberImage] + return MockMessage(image: image, sender: sender, messageId: uniqueID, date: date) + case .Video: + let randomNumberImage = Int(arc4random_uniform(UInt32(messageImages.count))) + let image = messageImages[randomNumberImage] + return MockMessage(thumbnail: image, sender: sender, messageId: uniqueID, date: date) + case .Emoji: + let randomNumberEmoji = Int(arc4random_uniform(UInt32(emojis.count))) + return MockMessage(emoji: emojis[randomNumberEmoji], sender: sender, messageId: uniqueID, date: date) + case .Location: + let randomNumberLocation = Int(arc4random_uniform(UInt32(locations.count))) + return MockMessage(location: locations[randomNumberLocation], sender: sender, messageId: uniqueID, date: date) + case .Url: + return MockMessage(text: "https://github.com/MessageKit", sender: sender, messageId: uniqueID, date: date) + case .Phone: + return MockMessage(text: "123-456-7890", sender: sender, messageId: uniqueID, date: date) + case .Custom: + return MockMessage(custom: "Someone left the conversation", sender: system, messageId: uniqueID, date: date) + } + } + + func getMessages(count: Int, completion: ([MockMessage]) -> Void) { + var messages: [MockMessage] = [] + // Disable Custom Messages + UserDefaults.standard.set(false, forKey: "Custom Messages") + for _ in 0.. Void) { + var messages: [MockMessage] = [] + // Enable Custom Messages + UserDefaults.standard.set(true, forKey: "Custom Messages") + for _ in 0.. Void) { + var messages: [MockMessage] = [] + // Disable Custom Messages + UserDefaults.standard.set(false, forKey: "Custom Messages") + for _ in 0.. Avatar { + let firstName = sender.displayName.components(separatedBy: " ").first + let lastName = sender.displayName.components(separatedBy: " ").first + let initials = "\(firstName?.first ?? "A")\(lastName?.first ?? "A")" + switch sender { + case nathan: + return Avatar(image: #imageLiteral(resourceName: "Nathan-Tannar"), initials: initials) + case steven: + return Avatar(image: #imageLiteral(resourceName: "Steven-Deutsch"), initials: initials) + case wu: + return Avatar(image: #imageLiteral(resourceName: "Wu-Zhong"), initials: initials) + case system: + return Avatar(image: nil, initials: "SS") + default: + return Avatar(image: nil, initials: initials) + } + } + +} diff --git a/Example/Sources/Extensions/UIColor+Extensions.swift b/Example/Sources/Extensions/UIColor+Extensions.swift new file mode 100644 index 000000000..f951c17c5 --- /dev/null +++ b/Example/Sources/Extensions/UIColor+Extensions.swift @@ -0,0 +1,29 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +extension UIColor { + static let primaryColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) +} diff --git a/Example/Sources/Extensions/UIViewController+Extensions.swift b/Example/Sources/Extensions/UIViewController+Extensions.swift new file mode 100644 index 000000000..2d7345498 --- /dev/null +++ b/Example/Sources/Extensions/UIViewController+Extensions.swift @@ -0,0 +1,67 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +extension UIViewController { + + func updateTitleView(title: String, subtitle: String?, baseColor: UIColor = .white) { + + let titleLabel = UILabel(frame: CGRect(x: 0, y: -2, width: 0, height: 0)) + titleLabel.backgroundColor = UIColor.clear + titleLabel.textColor = baseColor + titleLabel.font = UIFont.systemFont(ofSize: 15) + titleLabel.text = title + titleLabel.textAlignment = .center + titleLabel.adjustsFontSizeToFitWidth = true + titleLabel.sizeToFit() + + let subtitleLabel = UILabel(frame: CGRect(x: 0, y: 18, width: 0, height: 0)) + subtitleLabel.textColor = baseColor.withAlphaComponent(0.95) + subtitleLabel.font = UIFont.systemFont(ofSize: 12) + subtitleLabel.text = subtitle + subtitleLabel.textAlignment = .center + subtitleLabel.adjustsFontSizeToFitWidth = true + subtitleLabel.sizeToFit() + + let titleView = UIView(frame: CGRect(x: 0, y: 0, width: max(titleLabel.frame.size.width, subtitleLabel.frame.size.width), height: 30)) + titleView.addSubview(titleLabel) + if subtitle != nil { + titleView.addSubview(subtitleLabel) + } else { + titleLabel.frame = titleView.frame + } + let widthDiff = subtitleLabel.frame.size.width - titleLabel.frame.size.width + if widthDiff < 0 { + let newX = widthDiff / 2 + subtitleLabel.frame.origin.x = abs(newX) + } else { + let newX = widthDiff / 2 + titleLabel.frame.origin.x = newX + } + + navigationItem.titleView = titleView + } + +} diff --git a/Example/Sources/Info.plist b/Example/Sources/Info.plist index 27dad6468..aee82b188 100644 --- a/Example/Sources/Info.plist +++ b/Example/Sources/Info.plist @@ -52,6 +52,6 @@ UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance - + diff --git a/Example/Sources/Layout/CustomMessageFlowLayout.swift b/Example/Sources/Layout/CustomMessageFlowLayout.swift new file mode 100644 index 000000000..004ec7035 --- /dev/null +++ b/Example/Sources/Layout/CustomMessageFlowLayout.swift @@ -0,0 +1,67 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + 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 +import MessageKit + +open class CustomMessagesFlowLayout: MessagesCollectionViewFlowLayout { + + open lazy var customMessageSizeCalculator = CustomMessageSizeCalculator(layout: self) + + open override func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator { +// if isSectionReservedForTypingBubble(indexPath.section) { +// return typingMessageSizeCalculator +// } + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + if case .custom = message.kind { + return customMessageSizeCalculator + } + return super.cellSizeCalculatorForItem(at: indexPath) + } + + open override func messageSizeCalculators() -> [MessageSizeCalculator] { + var superCalculators = super.messageSizeCalculators() + // Append any of your custom `MessageSizeCalculator` if you wish for the convenience + // functions to work such as `setMessageIncoming...` or `setMessageOutgoing...` + superCalculators.append(customMessageSizeCalculator) + return superCalculators + } +} + +open class CustomMessageSizeCalculator: MessageSizeCalculator { + + public override init(layout: MessagesCollectionViewFlowLayout? = nil) { + super.init() + self.layout = layout + } + + open override func sizeForItem(at indexPath: IndexPath) -> CGSize { + guard let layout = layout else { return .zero } + let collectionViewWidth = layout.collectionView?.bounds.width ?? 0 + let contentInset = layout.collectionView?.contentInset ?? .zero + let inset = layout.sectionInset.left + layout.sectionInset.right + contentInset.left + contentInset.right + return CGSize(width: collectionViewWidth - inset, height: 44) + } + +} diff --git a/Example/Sources/MockMessage.swift b/Example/Sources/Models/MockMessage.swift similarity index 87% rename from Example/Sources/MockMessage.swift rename to Example/Sources/Models/MockMessage.swift index ef60a96d1..8a3203730 100644 --- a/Example/Sources/MockMessage.swift +++ b/Example/Sources/Models/MockMessage.swift @@ -26,7 +26,7 @@ import Foundation import CoreLocation import MessageKit -private struct MockLocationItem: LocationItem { +private struct CoordinateItem: LocationItem { var location: CLLocation var size: CGSize @@ -38,7 +38,7 @@ private struct MockLocationItem: LocationItem { } -private struct MockMediaItem: MediaItem { +private struct ImageMediaItem: MediaItem { var url: URL? var image: UIImage? @@ -66,6 +66,10 @@ internal struct MockMessage: MessageType { self.messageId = messageId self.sentDate = date } + + init(custom: Any?, sender: Sender, messageId: String, date: Date) { + self.init(kind: .custom(custom), sender: sender, messageId: messageId, date: date) + } init(text: String, sender: Sender, messageId: String, date: Date) { self.init(kind: .text(text), sender: sender, messageId: messageId, date: date) @@ -76,17 +80,17 @@ internal struct MockMessage: MessageType { } init(image: UIImage, sender: Sender, messageId: String, date: Date) { - let mediaItem = MockMediaItem(image: image) + let mediaItem = ImageMediaItem(image: image) self.init(kind: .photo(mediaItem), sender: sender, messageId: messageId, date: date) } init(thumbnail: UIImage, sender: Sender, messageId: String, date: Date) { - let mediaItem = MockMediaItem(image: thumbnail) + let mediaItem = ImageMediaItem(image: thumbnail) self.init(kind: .video(mediaItem), sender: sender, messageId: messageId, date: date) } init(location: CLLocation, sender: Sender, messageId: String, date: Date) { - let locationItem = MockLocationItem(location: location) + let locationItem = CoordinateItem(location: location) self.init(kind: .location(locationItem), sender: sender, messageId: messageId, date: date) } diff --git a/Example/Sources/Models/MockSocket.swift b/Example/Sources/Models/MockSocket.swift new file mode 100644 index 000000000..0585d9dc5 --- /dev/null +++ b/Example/Sources/Models/MockSocket.swift @@ -0,0 +1,87 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit +import MessageKit + +final class MockSocket { + + static var shared = MockSocket() + + private var timer: Timer? + + private var queuedMessage: MockMessage? + + private var onNewMessageCode: ((MockMessage) -> Void)? + + private var onTypingStatusCode: (() -> Void)? + + private var connectedUsers: [Sender] = [] + + private init() {} + + @discardableResult + func connect(with senders: [Sender]) -> Self { + disconnect() + connectedUsers = senders + timer = Timer.scheduledTimer(timeInterval: 2.5, target: self, selector: #selector(handleTimer), userInfo: nil, repeats: true) + return self + } + + @discardableResult + func disconnect() -> Self { + timer?.invalidate() + timer = nil + onTypingStatusCode = nil + onNewMessageCode = nil + return self + } + + @discardableResult + func onNewMessage(code: @escaping (MockMessage) -> Void) -> Self { + onNewMessageCode = code + return self + } + + @discardableResult + func onTypingStatus(code: @escaping () -> Void) -> Self { + onTypingStatusCode = code + return self + } + + @objc + private func handleTimer() { + if let message = queuedMessage { + onNewMessageCode?(message) + queuedMessage = nil + } else { + let sender = arc4random_uniform(1) % 2 == 0 ? connectedUsers.first! : connectedUsers.last! + SampleData.shared.getMessages(count: 1, allowedSenders: [sender]) { (message) in + queuedMessage = message.first + } + onTypingStatusCode?() + } + } + +} diff --git a/Example/Sources/SampleData.swift b/Example/Sources/SampleData.swift deleted file mode 100644 index 10619a8e1..000000000 --- a/Example/Sources/SampleData.swift +++ /dev/null @@ -1,200 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - 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 MessageKit -import CoreLocation - -final internal class SampleData { - - static let shared = SampleData() - - private init() {} - - let messageTextValues = [ - "Ok", - "k", - "lol", - "1-800-555-0000", - "One Infinite Loop Cupertino, CA 95014 This is some extra text that should not be detected.", - "This is an example of the date detector 11/11/2017. April 1st is April Fools Day. Next Friday is not Friday the 13th.", - "https://github.com/SD10", - "Check out this awesome UI library for Chat", - "My favorite things in life don’t cost any money. It’s really clear that the most precious resource we all have is time.", - """ - You know, this iPhone, as a matter of fact, the engine in here is made in America. - And not only are the engines in here made in America, but engines are made in America and are exported. - The glass on this phone is made in Kentucky. And so we've been working for years on doing more and more in the United States. - """, - """ - Remembering that I'll be dead soon is the most important tool I've ever encountered to help me make the big choices in life. - Because almost everything - all external expectations, all pride, all fear of embarrassment or failure - - these things just fall away in the face of death, leaving only what is truly important. - """, - "I think if you do something and it turns out pretty good, then you should go do something else wonderful, not dwell on it for too long. Just figure out what’s next.", - "Price is rarely the most important thing. A cheap product might sell some units. Somebody gets it home and they feel great when they pay the money, but then they get it home and use it and the joy is gone." - ] - - let dan = Sender(id: "123456", displayName: "Dan Leonard") - let steven = Sender(id: "654321", displayName: "Steven") - let jobs = Sender(id: "000001", displayName: "Steve Jobs") - let cook = Sender(id: "656361", displayName: "Tim Cook") - - lazy var senders = [dan, steven, jobs, cook] - - var currentSender: Sender { - return steven - } - - let messageImages: [UIImage] = [#imageLiteral(resourceName: "Dan-Leonard"), #imageLiteral(resourceName: "Tim-Cook"), #imageLiteral(resourceName: "Steve-Jobs")] - - var now = Date() - - let messageTypes = ["Text", "Text", "Text", "AttributedText", "Photo", "Video", "Location", "Emoji"] - - let attributes = ["Font1", "Font2", "Font3", "Font4", "Color", "Combo"] - - let locations: [CLLocation] = [ - CLLocation(latitude: 37.3118, longitude: -122.0312), - CLLocation(latitude: 33.6318, longitude: -100.0386), - CLLocation(latitude: 29.3358, longitude: -108.8311), - CLLocation(latitude: 39.3218, longitude: -127.4312), - CLLocation(latitude: 35.3218, longitude: -127.4314), - CLLocation(latitude: 39.3218, longitude: -113.3317) - ] - - let emojis = [ - "👍", - "👋", - "👋👋👋", - "😱😱", - "🎈", - "🇧🇷" - ] - - func attributedString(with text: String) -> NSAttributedString { - let nsString = NSString(string: text) - var mutableAttributedString = NSMutableAttributedString(string: text) - let randomAttribute = Int(arc4random_uniform(UInt32(attributes.count))) - let range = NSRange(location: 0, length: nsString.length) - - switch attributes[randomAttribute] { - case "Font1": - mutableAttributedString.addAttribute(NSAttributedStringKey.font, value: UIFont.preferredFont(forTextStyle: .body), range: range) - case "Font2": - mutableAttributedString.addAttributes([NSAttributedStringKey.font: UIFont.monospacedDigitSystemFont(ofSize: UIFont.systemFontSize, weight: UIFont.Weight.bold)], range: range) - case "Font3": - mutableAttributedString.addAttributes([NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: UIFont.systemFontSize)], range: range) - case "Font4": - mutableAttributedString.addAttributes([NSAttributedStringKey.font: UIFont.italicSystemFont(ofSize: UIFont.systemFontSize)], range: range) - case "Color": - mutableAttributedString.addAttributes([NSAttributedStringKey.foregroundColor: UIColor.red], range: range) - case "Combo": - let msg9String = "Use .attributedText() to add bold, italic, colored text and more..." - let msg9Text = NSString(string: msg9String) - let msg9AttributedText = NSMutableAttributedString(string: String(msg9Text)) - - msg9AttributedText.addAttribute(NSAttributedStringKey.font, value: UIFont.preferredFont(forTextStyle: .body), range: NSRange(location: 0, length: msg9Text.length)) - msg9AttributedText.addAttributes([NSAttributedStringKey.font: UIFont.monospacedDigitSystemFont(ofSize: UIFont.systemFontSize, weight: UIFont.Weight.bold)], range: msg9Text.range(of: ".attributedText()")) - msg9AttributedText.addAttributes([NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: UIFont.systemFontSize)], range: msg9Text.range(of: "bold")) - msg9AttributedText.addAttributes([NSAttributedStringKey.font: UIFont.italicSystemFont(ofSize: UIFont.systemFontSize)], range: msg9Text.range(of: "italic")) - msg9AttributedText.addAttributes([NSAttributedStringKey.foregroundColor: UIColor.red], range: msg9Text.range(of: "colored")) - mutableAttributedString = msg9AttributedText - default: - fatalError("Unrecognized attribute for mock message") - } - - return NSAttributedString(attributedString: mutableAttributedString) - } - - func dateAddingRandomTime() -> Date { - let randomNumber = Int(arc4random_uniform(UInt32(10))) - if randomNumber % 2 == 0 { - let date = Calendar.current.date(byAdding: .hour, value: randomNumber, to: now)! - now = date - return date - } else { - let randomMinute = Int(arc4random_uniform(UInt32(59))) - let date = Calendar.current.date(byAdding: .minute, value: randomMinute, to: now)! - now = date - return date - } - } - - func randomMessage() -> MockMessage { - - let randomNumberSender = Int(arc4random_uniform(UInt32(senders.count))) - let randomNumberText = Int(arc4random_uniform(UInt32(messageTextValues.count))) - let randomNumberImage = Int(arc4random_uniform(UInt32(messageImages.count))) - let randomMessageType = Int(arc4random_uniform(UInt32(messageTypes.count))) - let randomNumberLocation = Int(arc4random_uniform(UInt32(locations.count))) - let randomNumberEmoji = Int(arc4random_uniform(UInt32(emojis.count))) - let uniqueID = NSUUID().uuidString - let sender = senders[randomNumberSender] - let date = dateAddingRandomTime() - - switch messageTypes[randomMessageType] { - case "Text": - return MockMessage(text: messageTextValues[randomNumberText], sender: sender, messageId: uniqueID, date: date) - case "AttributedText": - let attributedText = attributedString(with: messageTextValues[randomNumberText]) - return MockMessage(attributedText: attributedText, sender: senders[randomNumberSender], messageId: uniqueID, date: date) - case "Photo": - let image = messageImages[randomNumberImage] - return MockMessage(image: image, sender: sender, messageId: uniqueID, date: date) - case "Video": - let image = messageImages[randomNumberImage] - return MockMessage(thumbnail: image, sender: sender, messageId: uniqueID, date: date) - case "Location": - return MockMessage(location: locations[randomNumberLocation], sender: sender, messageId: uniqueID, date: date) - case "Emoji": - return MockMessage(emoji: emojis[randomNumberEmoji], sender: sender, messageId: uniqueID, date: date) - default: - fatalError("Unrecognized mock message type") - } - } - - func getMessages(count: Int, completion: ([MockMessage]) -> Void) { - var messages: [MockMessage] = [] - for _ in 0.. Avatar { - switch sender { - case dan: - return Avatar(image: #imageLiteral(resourceName: "Dan-Leonard"), initials: "DL") - case steven: - return Avatar(initials: "S") - case jobs: - return Avatar(image: #imageLiteral(resourceName: "Steve-Jobs"), initials: "SJ") - case cook: - return Avatar(image: #imageLiteral(resourceName: "Tim-Cook")) - default: - return Avatar() - } - } - -} diff --git a/Example/Sources/Settings+UserDefaults.swift b/Example/Sources/Settings+UserDefaults.swift index 620a0241e..edddcc724 100644 --- a/Example/Sources/Settings+UserDefaults.swift +++ b/Example/Sources/Settings+UserDefaults.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017 MessageKit + Copyright (c) 2017-2018 MessageKit Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -41,4 +41,14 @@ extension UserDefaults { } return 20 } + + static func isFirstLaunch() -> Bool { + let hasBeenLaunchedBeforeFlag = "hasBeenLaunchedBeforeFlag" + let isFirstLaunch = !UserDefaults.standard.bool(forKey: hasBeenLaunchedBeforeFlag) + if isFirstLaunch { + UserDefaults.standard.set(true, forKey: hasBeenLaunchedBeforeFlag) + UserDefaults.standard.synchronize() + } + return isFirstLaunch + } } diff --git a/Example/Sources/View Controllers/AdvancedExampleViewController.swift b/Example/Sources/View Controllers/AdvancedExampleViewController.swift new file mode 100644 index 000000000..d55b07cd3 --- /dev/null +++ b/Example/Sources/View Controllers/AdvancedExampleViewController.swift @@ -0,0 +1,388 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit +import MapKit +import MessageKit +import MessageInputBar + +final class AdvancedExampleViewController: ChatViewController { + + let outgoingAvatarOverlap: CGFloat = 17.5 + + override func viewDidLoad() { + messagesCollectionView = MessagesCollectionView(frame: .zero, collectionViewLayout: CustomMessagesFlowLayout()) + messagesCollectionView.register(CustomCell.self) + super.viewDidLoad() + + updateTitleView(title: "MessageKit", subtitle: "2 Online") + + // Customize the typing bubble! These are the default values +// typingBubbleBackgroundColor = UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1) +// typingBubbleDotColor = .lightGray + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + MockSocket.shared.connect(with: [SampleData.shared.steven, SampleData.shared.wu]) + .onTypingStatus { [weak self] in + self?.setTypingIndicatorHidden(false) + }.onNewMessage { [weak self] message in + self?.setTypingIndicatorHidden(true, performUpdates: { +// self?.insertMessage(message) + }) + self?.insertMessage(message) + } + } + + override func loadFirstMessages() { + DispatchQueue.global(qos: .userInitiated).async { + let count = UserDefaults.standard.mockMessagesCount() + SampleData.shared.getAdvancedMessages(count: count) { messages in + DispatchQueue.main.async { + self.messageList = messages + self.messagesCollectionView.reloadData() + self.messagesCollectionView.scrollToBottom() + } + } + } + } + + override func loadMoreMessages() { + DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1) { + SampleData.shared.getAdvancedMessages(count: 20) { messages in + DispatchQueue.main.async { + self.messageList.insert(contentsOf: messages, at: 0) + self.messagesCollectionView.reloadDataAndKeepOffset() + self.refreshControl.endRefreshing() + } + } + } + } + + override func configureMessageCollectionView() { + super.configureMessageCollectionView() + + let layout = messagesCollectionView.collectionViewLayout as? MessagesCollectionViewFlowLayout + layout?.sectionInset = UIEdgeInsets(top: 1, left: 8, bottom: 1, right: 8) + + // Hide the outgoing avatar and adjust the label alignment to line up with the messages + layout?.setMessageOutgoingAvatarSize(.zero) + layout?.setMessageOutgoingMessageTopLabelAlignment(LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8))) + layout?.setMessageOutgoingMessageBottomLabelAlignment(LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8))) + + // Set outgoing avatar to overlap with the message bubble + layout?.setMessageIncomingMessageTopLabelAlignment(LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(top: 0, left: 18, bottom: outgoingAvatarOverlap, right: 0))) + layout?.setMessageIncomingAvatarSize(CGSize(width: 30, height: 30)) + layout?.setMessageIncomingMessagePadding(UIEdgeInsets(top: -outgoingAvatarOverlap, left: -18, bottom: outgoingAvatarOverlap, right: 18)) + + layout?.setMessageIncomingAccessoryViewSize(CGSize(width: 30, height: 30)) + layout?.setMessageIncomingAccessoryViewPadding(HorizontalEdgeInsets(left: 8, right: 0)) + layout?.setMessageOutgoingAccessoryViewSize(CGSize(width: 30, height: 30)) + layout?.setMessageOutgoingAccessoryViewPadding(HorizontalEdgeInsets(left: 0, right: 8)) + + messagesCollectionView.messagesLayoutDelegate = self + messagesCollectionView.messagesDisplayDelegate = self + } + + override func configureMessageInputBar() { + super.configureMessageInputBar() + + messageInputBar.isTranslucent = true + messageInputBar.separatorLine.isHidden = true + messageInputBar.inputTextView.tintColor = .primaryColor + messageInputBar.inputTextView.backgroundColor = UIColor(red: 245/255, green: 245/255, blue: 245/255, alpha: 1) + messageInputBar.inputTextView.placeholderTextColor = UIColor(red: 0.6, green: 0.6, blue: 0.6, alpha: 1) + messageInputBar.inputTextView.textContainerInset = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 36) + messageInputBar.inputTextView.placeholderLabelInsets = UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 36) + messageInputBar.inputTextView.layer.borderColor = UIColor(red: 200/255, green: 200/255, blue: 200/255, alpha: 1).cgColor + messageInputBar.inputTextView.layer.borderWidth = 1.0 + messageInputBar.inputTextView.layer.cornerRadius = 16.0 + messageInputBar.inputTextView.layer.masksToBounds = true + messageInputBar.inputTextView.scrollIndicatorInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0) + configureInputBarItems() + } + + private func configureInputBarItems() { + messageInputBar.setRightStackViewWidthConstant(to: 36, animated: false) + messageInputBar.sendButton.imageView?.backgroundColor = UIColor(white: 0.85, alpha: 1) + messageInputBar.sendButton.contentEdgeInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) + messageInputBar.sendButton.setSize(CGSize(width: 36, height: 36), animated: false) + messageInputBar.sendButton.image = #imageLiteral(resourceName: "ic_up") + messageInputBar.sendButton.title = nil + messageInputBar.sendButton.imageView?.layer.cornerRadius = 16 + messageInputBar.textViewPadding.right = -38 + let charCountButton = InputBarButtonItem() + .configure { + $0.title = "0/140" + $0.contentHorizontalAlignment = .right + $0.setTitleColor(UIColor(white: 0.6, alpha: 1), for: .normal) + $0.titleLabel?.font = UIFont.systemFont(ofSize: 10, weight: .bold) + $0.setSize(CGSize(width: 50, height: 25), animated: false) + }.onTextViewDidChange { (item, textView) in + item.title = "\(textView.text.count)/140" + let isOverLimit = textView.text.count > 140 + item.messageInputBar?.shouldManageSendButtonEnabledState = !isOverLimit // Disable automated management when over limit + if isOverLimit { + item.messageInputBar?.sendButton.isEnabled = false + } + let color = isOverLimit ? .red : UIColor(white: 0.6, alpha: 1) + item.setTitleColor(color, for: .normal) + } + let bottomItems = [makeButton(named: "ic_at"), makeButton(named: "ic_hashtag"), makeButton(named: "ic_library"), .flexibleSpace, charCountButton] + messageInputBar.textViewPadding.bottom = 8 + messageInputBar.setStackViewItems(bottomItems, forStack: .bottom, animated: false) + + // This just adds some more flare + messageInputBar.sendButton + .onEnabled { item in + UIView.animate(withDuration: 0.3, animations: { + item.imageView?.backgroundColor = .primaryColor + }) + }.onDisabled { item in + UIView.animate(withDuration: 0.3, animations: { + item.imageView?.backgroundColor = UIColor(white: 0.85, alpha: 1) + }) + } + } + + // MARK: - Helpers + + func isTimeLabelVisible(at indexPath: IndexPath) -> Bool { + return indexPath.section % 3 == 0 && !isPreviousMessageSameSender(at: indexPath) + } + + func isPreviousMessageSameSender(at indexPath: IndexPath) -> Bool { + guard indexPath.section - 1 >= 0 else { return false } + return messageList[indexPath.section].sender == messageList[indexPath.section - 1].sender + } + + func isNextMessageSameSender(at indexPath: IndexPath) -> Bool { + guard indexPath.section + 1 < messageList.count else { return false } + return messageList[indexPath.section].sender == messageList[indexPath.section + 1].sender + } + + func setTypingIndicatorHidden(_ isHidden: Bool, performUpdates updates: (() -> Void)? = nil) { + updateTitleView(title: "MessageKit", subtitle: isHidden ? "2 Online" : "Typing...") +// setTypingBubbleHidden(isHidden, animated: true, whilePerforming: updates) { [weak self] (_) in +// if self?.isLastSectionVisible() == true { +// self?.messagesCollectionView.scrollToBottom(animated: true) +// } +// } +// messagesCollectionView.scrollToBottom(animated: true) + } + + private func makeButton(named: String) -> InputBarButtonItem { + return InputBarButtonItem() + .configure { + $0.spacing = .fixed(10) + $0.image = UIImage(named: named)?.withRenderingMode(.alwaysTemplate) + $0.setSize(CGSize(width: 25, height: 25), animated: false) + $0.tintColor = UIColor(white: 0.8, alpha: 1) + }.onSelected { + $0.tintColor = .primaryColor + }.onDeselected { + $0.tintColor = UIColor(white: 0.8, alpha: 1) + }.onTouchUpInside { _ in + print("Item Tapped") + } + } + + // MARK: - UICollectionViewDataSource + + public override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { + fatalError("Ouch. nil data source for messages") + } + +// guard !isSectionReservedForTypingBubble(indexPath.section) else { +// return super.collectionView(collectionView, cellForItemAt: indexPath) +// } + + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + if case .custom = message.kind { + let cell = messagesCollectionView.dequeueReusableCell(CustomCell.self, for: indexPath) + cell.configure(with: message, at: indexPath, and: messagesCollectionView) + return cell + } + return super.collectionView(collectionView, cellForItemAt: indexPath) + } + + // MARK: - MessagesDataSource + + override func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + if isTimeLabelVisible(at: indexPath) { + return NSAttributedString(string: MessageKitDateFormatter.shared.string(from: message.sentDate), attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10), NSAttributedString.Key.foregroundColor: UIColor.darkGray]) + } + return nil + } + + override func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + if !isPreviousMessageSameSender(at: indexPath) { + let name = message.sender.displayName + return NSAttributedString(string: name, attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)]) + } + return nil + } + + override func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + + if !isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message) { + return NSAttributedString(string: "Delivered", attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)]) + } + return nil + } + +} + +// MARK: - MessagesDisplayDelegate + +extension AdvancedExampleViewController: MessagesDisplayDelegate { + + // MARK: - Text Messages + + func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { + return isFromCurrentSender(message: message) ? .white : .darkText + } + + func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any] { + return MessageLabel.defaultAttributes + } + + func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] { + return [.url, .address, .phoneNumber, .date, .transitInformation] + } + + // MARK: - All Messages + + func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { + return isFromCurrentSender(message: message) ? .primaryColor : UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1) + } + + func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle { + + var corners: UIRectCorner = [] + + if isFromCurrentSender(message: message) { + corners.formUnion(.topLeft) + corners.formUnion(.bottomLeft) + if !isPreviousMessageSameSender(at: indexPath) { + corners.formUnion(.topRight) + } + if !isNextMessageSameSender(at: indexPath) { + corners.formUnion(.bottomRight) + } + } else { + corners.formUnion(.topRight) + corners.formUnion(.bottomRight) + if !isPreviousMessageSameSender(at: indexPath) { + corners.formUnion(.topLeft) + } + if !isNextMessageSameSender(at: indexPath) { + corners.formUnion(.bottomLeft) + } + } + + return .custom { view in + let radius: CGFloat = 16 + let path = UIBezierPath(roundedRect: view.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) + let mask = CAShapeLayer() + mask.path = path.cgPath + view.layer.mask = mask + } + } + + func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { + let avatar = SampleData.shared.getAvatarFor(sender: message.sender) + avatarView.set(avatar: avatar) + avatarView.isHidden = isNextMessageSameSender(at: indexPath) + avatarView.layer.borderWidth = 2 + avatarView.layer.borderColor = UIColor.primaryColor.cgColor + } + + func configureAccessoryView(_ accessoryView: UIView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { + // Cells are reused, so only add a button here once. For real use you would need to + // ensure any subviews are removed if not needed + guard accessoryView.subviews.isEmpty else { return } + let button = UIButton(type: .infoLight) + button.tintColor = .primaryColor + accessoryView.addSubview(button) + button.frame = accessoryView.bounds + button.isUserInteractionEnabled = false // respond to accessoryView tap through `MessageCellDelegate` + accessoryView.layer.cornerRadius = accessoryView.frame.height / 2 + accessoryView.backgroundColor = UIColor.primaryColor.withAlphaComponent(0.3) + } + + // MARK: - Location Messages + + func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? { + let annotationView = MKAnnotationView(annotation: nil, reuseIdentifier: nil) + let pinImage = #imageLiteral(resourceName: "ic_map_marker") + annotationView.image = pinImage + annotationView.centerOffset = CGPoint(x: 0, y: -pinImage.size.height / 2) + return annotationView + } + + func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? { + return { view in + view.layer.transform = CATransform3DMakeScale(2, 2, 2) + UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0, options: [], animations: { + view.layer.transform = CATransform3DIdentity + }, completion: nil) + } + } + + func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions { + + return LocationMessageSnapshotOptions(showsBuildings: true, showsPointsOfInterest: true, span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)) + } + +} + +// MARK: - MessagesLayoutDelegate + +extension AdvancedExampleViewController: MessagesLayoutDelegate { + + func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + if isTimeLabelVisible(at: indexPath) { + return 18 + } + return 0 + } + + func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + if isFromCurrentSender(message: message) { + return !isPreviousMessageSameSender(at: indexPath) ? 20 : 0 + } else { + return !isPreviousMessageSameSender(at: indexPath) ? (20 + outgoingAvatarOverlap) : 0 + } + } + + func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + return (!isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message)) ? 16 : 0 + } + +} diff --git a/Example/Sources/View Controllers/BasicExampleViewController.swift b/Example/Sources/View Controllers/BasicExampleViewController.swift new file mode 100644 index 000000000..0d7fc717b --- /dev/null +++ b/Example/Sources/View Controllers/BasicExampleViewController.swift @@ -0,0 +1,118 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit +import MapKit +import MessageKit +import MessageInputBar + +final class BasicExampleViewController: ChatViewController { + + override func configureMessageCollectionView() { + super.configureMessageCollectionView() + + messagesCollectionView.messagesLayoutDelegate = self + messagesCollectionView.messagesDisplayDelegate = self + } + +} + +// MARK: - MessagesDisplayDelegate + +extension BasicExampleViewController: MessagesDisplayDelegate { + + // MARK: - Text Messages + + func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { + return isFromCurrentSender(message: message) ? .white : .darkText + } + + func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any] { + return MessageLabel.defaultAttributes + } + + func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] { + return [.url, .address, .phoneNumber, .date, .transitInformation] + } + + // MARK: - All Messages + + func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { + return isFromCurrentSender(message: message) ? .primaryColor : UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1) + } + + func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle { + + let tail: MessageStyle.TailCorner = isFromCurrentSender(message: message) ? .bottomRight : .bottomLeft + return .bubbleTail(tail, .curved) + } + + func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { + let avatar = SampleData.shared.getAvatarFor(sender: message.sender) + avatarView.set(avatar: avatar) + } + + // MARK: - Location Messages + + func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? { + let annotationView = MKAnnotationView(annotation: nil, reuseIdentifier: nil) + let pinImage = #imageLiteral(resourceName: "ic_map_marker") + annotationView.image = pinImage + annotationView.centerOffset = CGPoint(x: 0, y: -pinImage.size.height / 2) + return annotationView + } + + func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? { + return { view in + view.layer.transform = CATransform3DMakeScale(2, 2, 2) + UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0, options: [], animations: { + view.layer.transform = CATransform3DIdentity + }, completion: nil) + } + } + + func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions { + + return LocationMessageSnapshotOptions(showsBuildings: true, showsPointsOfInterest: true, span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)) + } + +} + +// MARK: - MessagesLayoutDelegate + +extension BasicExampleViewController: MessagesLayoutDelegate { + + func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + return 18 + } + + func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + return 20 + } + + func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + return 16 + } + +} diff --git a/Example/Sources/View Controllers/ChatViewController.swift b/Example/Sources/View Controllers/ChatViewController.swift new file mode 100644 index 000000000..a8de91665 --- /dev/null +++ b/Example/Sources/View Controllers/ChatViewController.swift @@ -0,0 +1,250 @@ +/* +MIT License + +Copyright (c) 2017-2018 MessageKit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import UIKit +import MessageKit +import MessageInputBar + +/// A base class for the example controllers +class ChatViewController: MessagesViewController, MessagesDataSource { + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + var messageList: [MockMessage] = [] + + let refreshControl = UIRefreshControl() + + let formatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + return formatter + }() + + override func viewDidLoad() { + super.viewDidLoad() + + configureMessageCollectionView() + configureMessageInputBar() + loadFirstMessages() + title = "MessageKit" + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + MockSocket.shared.connect(with: [SampleData.shared.steven, SampleData.shared.wu]) + .onNewMessage { [weak self] message in + self?.insertMessage(message) + } + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + MockSocket.shared.disconnect() + } + + func loadFirstMessages() { + DispatchQueue.global(qos: .userInitiated).async { + let count = UserDefaults.standard.mockMessagesCount() + SampleData.shared.getMessages(count: count) { messages in + DispatchQueue.main.async { + self.messageList = messages + self.messagesCollectionView.reloadData() + self.messagesCollectionView.scrollToBottom() + } + } + } + } + + @objc + func loadMoreMessages() { + DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1) { + SampleData.shared.getMessages(count: 20) { messages in + DispatchQueue.main.async { + self.messageList.insert(contentsOf: messages, at: 0) + self.messagesCollectionView.reloadDataAndKeepOffset() + self.refreshControl.endRefreshing() + } + } + } + } + + func configureMessageCollectionView() { + + messagesCollectionView.messagesDataSource = self + messagesCollectionView.messageCellDelegate = self + + scrollsToBottomOnKeyboardBeginsEditing = true // default false + maintainPositionOnKeyboardFrameChanged = true // default false + + messagesCollectionView.addSubview(refreshControl) + refreshControl.addTarget(self, action: #selector(loadMoreMessages), for: .valueChanged) + } + + func configureMessageInputBar() { + messageInputBar.delegate = self + messageInputBar.inputTextView.tintColor = .primaryColor + messageInputBar.sendButton.tintColor = .primaryColor + } + + // MARK: - Helpers + + func insertMessage(_ message: MockMessage) { + messageList.append(message) + // Reload last section to update header/footer labels and insert a new one + messagesCollectionView.performBatchUpdates({ + messagesCollectionView.insertSections([messageList.count - 1]) + if messageList.count >= 2 { + messagesCollectionView.reloadSections([messageList.count - 2]) + } + }, completion: { [weak self] _ in + if self?.isLastSectionVisible() == true { + self?.messagesCollectionView.scrollToBottom(animated: true) + } + }) + } + + func isLastSectionVisible() -> Bool { + + guard !messageList.isEmpty else { return false } + + let lastIndexPath = IndexPath(item: 0, section: messageList.count - 1) + + return messagesCollectionView.indexPathsForVisibleItems.contains(lastIndexPath) + } + + // MARK: - MessagesDataSource + + func currentSender() -> Sender { + return SampleData.shared.currentSender + } + + func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int { + return messageList.count + } + + func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType { + return messageList[indexPath.section] + } + + func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + if indexPath.section % 3 == 0 { + return NSAttributedString(string: MessageKitDateFormatter.shared.string(from: message.sentDate), attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10), NSAttributedString.Key.foregroundColor: UIColor.darkGray]) + } + return nil + } + + func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + let name = message.sender.displayName + return NSAttributedString(string: name, attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)]) + } + + func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + + let dateString = formatter.string(from: message.sentDate) + return NSAttributedString(string: dateString, attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption2)]) + } + +} + +// MARK: - MessageCellDelegate + +extension ChatViewController: MessageCellDelegate { + + func didTapAvatar(in cell: MessageCollectionViewCell) { + print("Avatar tapped") + } + + func didTapMessage(in cell: MessageCollectionViewCell) { + print("Message tapped") + } + + func didTapCellTopLabel(in cell: MessageCollectionViewCell) { + print("Top cell label tapped") + } + + func didTapMessageTopLabel(in cell: MessageCollectionViewCell) { + print("Top message label tapped") + } + + func didTapMessageBottomLabel(in cell: MessageCollectionViewCell) { + print("Bottom label tapped") + } + + func didTapAccessoryView(in cell: MessageCollectionViewCell) { + print("Accessory view tapped") + } + +} + +// MARK: - MessageLabelDelegate + +extension ChatViewController: MessageLabelDelegate { + + func didSelectAddress(_ addressComponents: [String: String]) { + print("Address Selected: \(addressComponents)") + } + + func didSelectDate(_ date: Date) { + print("Date Selected: \(date)") + } + + func didSelectPhoneNumber(_ phoneNumber: String) { + print("Phone Number Selected: \(phoneNumber)") + } + + func didSelectURL(_ url: URL) { + print("URL Selected: \(url)") + } + + func didSelectTransitInformation(_ transitInformation: [String: String]) { + print("TransitInformation Selected: \(transitInformation)") + } + +} + +// MARK: - MessageInputBarDelegate + +extension ChatViewController: MessageInputBarDelegate { + + func messageInputBar(_ inputBar: MessageInputBar, didPressSendButtonWith text: String) { + + for component in inputBar.inputTextView.components { + + if let str = component as? String { + let message = MockMessage(text: str, sender: currentSender(), messageId: UUID().uuidString, date: Date()) + insertMessage(message) + } else if let img = component as? UIImage { + let message = MockMessage(image: img, sender: currentSender(), messageId: UUID().uuidString, date: Date()) + insertMessage(message) + } + + } + inputBar.inputTextView.text = String() + messagesCollectionView.scrollToBottom(animated: true) + } + +} diff --git a/Example/Sources/InboxViewController.swift b/Example/Sources/View Controllers/LaunchViewController.swift similarity index 72% rename from Example/Sources/InboxViewController.swift rename to Example/Sources/View Controllers/LaunchViewController.swift index 559437ad9..8340af1a8 100644 --- a/Example/Sources/InboxViewController.swift +++ b/Example/Sources/View Controllers/LaunchViewController.swift @@ -24,20 +24,23 @@ import UIKit import MessageKit +import MessageInputBar import SafariServices -final internal class InboxViewController: UITableViewController { +final internal class LaunchViewController: UITableViewController { + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } - let cells = ["Example", "Settings", "Source Code", "Contributors"] + let cells = ["Basic Example", "Advanced Example", "Embedded Example", "Settings", "Source Code", "Contributors"] // MARK: - View Life Cycle override func viewDidLoad() { super.viewDidLoad() title = "MessageKit" - navigationController?.navigationBar.tintColor = .white - navigationController?.navigationBar.barTintColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) - navigationController?.navigationBar.titleTextAttributes = [NSAttributedStringKey.foregroundColor: UIColor.white, NSAttributedStringKey.font: UIFont.systemFont(ofSize: 20, weight: UIFont.Weight.bold) ] + navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") tableView.tableFooterView = UIView() } @@ -46,7 +49,6 @@ final internal class InboxViewController: UITableViewController { super.viewWillAppear(animated) if #available(iOS 11.0, *) { navigationController?.navigationBar.prefersLargeTitles = true - navigationController?.navigationBar.largeTitleTextAttributes = [NSAttributedStringKey.foregroundColor: UIColor.white] } } @@ -75,21 +77,31 @@ final internal class InboxViewController: UITableViewController { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let cell = cells[indexPath.row] switch cell { - case "Example": - navigationController?.pushViewController(ConversationViewController(), animated: true) + case "Basic Example": + navigationController?.pushViewController(BasicExampleViewController(), animated: true) + case "Advanced Example": + navigationController?.pushViewController(AdvancedExampleViewController(), animated: true) + case "Embedded Example": + navigationController?.pushViewController(MessageContainerController(), animated: true) case "Settings": navigationController?.pushViewController(SettingsViewController(), animated: true) case "Source Code": guard let url = URL(string: "https://github.com/MessageKit/MessageKit") else { return } - let webViewController = SFSafariViewController(url: url) - present(webViewController, animated: true, completion: nil) + openURL(url) case "Contributors": guard let url = URL(string: "https://github.com/orgs/MessageKit/teams/contributors/members") else { return } - let webViewController = SFSafariViewController(url: url) - present(webViewController, animated: true, completion: nil) + openURL(url) default: assertionFailure("You need to impliment the action for this cell: \(cell)") return } } + + func openURL(_ url: URL) { + let webViewController = SFSafariViewController(url: url) + if #available(iOS 10.0, *) { + webViewController.preferredControlTintColor = .primaryColor + } + present(webViewController, animated: true, completion: nil) + } } diff --git a/Example/Sources/View Controllers/MessageContainerController.swift b/Example/Sources/View Controllers/MessageContainerController.swift new file mode 100644 index 000000000..ec1ea24b2 --- /dev/null +++ b/Example/Sources/View Controllers/MessageContainerController.swift @@ -0,0 +1,88 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit +import MapKit + +final class MessageContainerController: UIViewController { + + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + let mapView = MKMapView() + + let bannerView: UIView = { + let view = UIView() + view.backgroundColor = .primaryColor + view.alpha = 0.7 + return view + }() + + let conversationViewController = BasicExampleViewController() + + /// Required for the `MessageInputBar` to be visible + override var canBecomeFirstResponder: Bool { + return conversationViewController.canBecomeFirstResponder + } + + /// Required for the `MessageInputBar` to be visible + override var inputAccessoryView: UIView? { + return conversationViewController.inputAccessoryView + } + + override func viewDidLoad() { + super.viewDidLoad() + + /// Add the `ConversationViewController` as a child view controller + conversationViewController.willMove(toParent: self) + self.addChild(conversationViewController) + view.addSubview(conversationViewController.view) + conversationViewController.didMove(toParent: self) + + view.addSubview(mapView) + view.addSubview(bannerView) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.navigationBar.isTranslucent = true + navigationController?.navigationBar.barTintColor = .clear + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + navigationController?.navigationBar.isTranslucent = false + navigationController?.navigationBar.barTintColor = .primaryColor + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + let headerHeight: CGFloat = 200 + mapView.frame = CGRect(origin: .zero, size: CGSize(width: view.bounds.width, height: headerHeight)) + bannerView.frame = CGRect(origin: .zero, size: CGSize(width: view.bounds.width, height: headerHeight)) + conversationViewController.view.frame = CGRect(x: 0, y: headerHeight, width: view.bounds.width, height: view.bounds.height - headerHeight) + } + +} diff --git a/Example/Sources/View Controllers/NavigationController.swift b/Example/Sources/View Controllers/NavigationController.swift new file mode 100644 index 000000000..cb41b0cb5 --- /dev/null +++ b/Example/Sources/View Controllers/NavigationController.swift @@ -0,0 +1,67 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +final class NavigationController: UINavigationController { + + override var preferredStatusBarStyle: UIStatusBarStyle { + return viewControllers.last?.preferredStatusBarStyle ?? .lightContent + } + + override func viewDidLoad() { + super.viewDidLoad() + navigationBar.isTranslucent = false + navigationBar.tintColor = .white + navigationBar.barTintColor = .primaryColor + navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white] + if #available(iOS 11.0, *) { + navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.white] + } + navigationBar.shadowImage = UIImage() + navigationBar.setBackgroundImage(UIImage(), for: .default) + view.backgroundColor = .primaryColor + } + + func setAppearanceStyle(to style: UIStatusBarStyle) { + if style == .default { + navigationBar.shadowImage = UIImage() + navigationBar.barTintColor = .primaryColor + navigationBar.tintColor = .white + navigationBar.titleTextAttributes = [.foregroundColor: UIColor.white] + if #available(iOS 11.0, *) { + navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.white] + } + } else if style == .lightContent { + navigationBar.shadowImage = nil + navigationBar.barTintColor = .white + navigationBar.tintColor = UIColor(red: 0, green: 0.5, blue: 1, alpha: 1) + navigationBar.titleTextAttributes = [.foregroundColor: UIColor.black] + if #available(iOS 11.0, *) { + navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.black] + } + } + } + +} diff --git a/Example/Sources/SettingsViewController.swift b/Example/Sources/View Controllers/SettingsViewController.swift similarity index 77% rename from Example/Sources/SettingsViewController.swift rename to Example/Sources/View Controllers/SettingsViewController.swift index dae638b29..882e0f9e3 100644 --- a/Example/Sources/SettingsViewController.swift +++ b/Example/Sources/View Controllers/SettingsViewController.swift @@ -24,12 +24,17 @@ import UIKit import MessageKit +import MessageInputBar final internal class SettingsViewController: UITableViewController { // MARK: - Properties - var selectedMockMessagesCount: Int = 20 + override var preferredStatusBarStyle: UIStatusBarStyle { + return .lightContent + } + + let cells = ["Mock messages count", "Text Messages", "AttributedText Messages", "Photo Messages", "Video Messages", "Emoji Messages", "Location Messages", "Url Messages", "Phone Messages"] // MARK: - Picker @@ -50,6 +55,8 @@ final internal class SettingsViewController: UITableViewController { messagesPicker.dataSource = self messagesPicker.delegate = self messagesPicker.backgroundColor = .white + + messagesPicker.selectRow(UserDefaults.standard.mockMessagesCount(), inComponent: 0, animated: false) } // MARK: - Toolbar @@ -69,7 +76,7 @@ final internal class SettingsViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() tableView.register(TextFieldTableViewCell.self, forCellReuseIdentifier: TextFieldTableViewCell.identifier) - + tableView.tableFooterView = UIView() configurePickerView() configureToolbar() } @@ -77,12 +84,25 @@ final internal class SettingsViewController: UITableViewController { // MARK: - TableViewDelegate & TableViewDataSource override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 1 + return cells.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cellValue = cells[indexPath.row] + let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell() + cell.textLabel?.text = cells[indexPath.row] - return indexPath.row == 0 ? configureTextFieldTableViewCell(at: indexPath) : UITableViewCell() + switch cellValue { + case "Mock messages count": + return configureTextFieldTableViewCell(at: indexPath) + default: + let switchView = UISwitch(frame: .zero) + switchView.isOn = UserDefaults.standard.bool(forKey: cellValue) + switchView.tag = indexPath.row + switchView.addTarget(self, action: #selector(self.switchChanged(_:)), for: .valueChanged) + cell.accessoryView = switchView + } + return cell } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { @@ -113,6 +133,12 @@ final internal class SettingsViewController: UITableViewController { } return TextFieldTableViewCell() } + + @objc func switchChanged(_ sender: UISwitch!) { + let cell = cells[sender.tag] + + UserDefaults.standard.set(sender.isOn, forKey: cell) + } } // MARK: - UIPickerViewDelegate, UIPickerViewDataSource @@ -129,8 +155,4 @@ extension SettingsViewController: UIPickerViewDelegate, UIPickerViewDataSource { func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { return "\(row)" } - - func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { - selectedMockMessagesCount = row - } } diff --git a/Sources/Views/SeparatorLine.swift b/Example/Sources/Views/CustomCell.swift similarity index 59% rename from Sources/Views/SeparatorLine.swift rename to Example/Sources/Views/CustomCell.swift index 9ca582b66..c23b0205a 100644 --- a/Sources/Views/SeparatorLine.swift +++ b/Example/Sources/Views/CustomCell.swift @@ -23,46 +23,42 @@ */ import UIKit +import MessageKit -/** - A UIView thats intrinsicContentSize is overrided so an exact height can be specified - - ## Important Notes ## - 1. Default height is 1.0 - 2. Default backgroundColor is UIColor.lightGray - 3. Intended to be used in an `InputStackView` - */ -open class SeparatorLine: UIView { +open class CustomCell: UICollectionViewCell { - // MARK: - Properties + let label = UILabel() - /// The height of the line - open var height: CGFloat = 1.0 { - didSet { - invalidateIntrinsicContentSize() - } + public override init(frame: CGRect) { + super.init(frame: frame) + setupSubviews() } - open override var intrinsicContentSize: CGSize { - return CGSize(width: super.intrinsicContentSize.width, height: height) + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setupSubviews() } - // MARK: - Initialization - - public override init(frame: CGRect) { - super.init(frame: frame) - setup() + open func setupSubviews() { + contentView.addSubview(label) + label.textAlignment = .center + label.font = UIFont.italicSystemFont(ofSize: 13) } - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setup() + open override func layoutSubviews() { + super.layoutSubviews() + label.frame = contentView.bounds } - /// Sets up the default properties - open func setup() { - backgroundColor = .lightGray - translatesAutoresizingMaskIntoConstraints = false - setContentHuggingPriority(.defaultHigh, for: .vertical) + open func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) { + // Do stuff + switch message.kind { + case .custom(let data): + guard let systemMessage = data as? String else { return } + label.text = systemMessage + default: + break + } } + } diff --git a/Example/Sources/TableViewCells.swift b/Example/Sources/Views/TableViewCells.swift similarity index 95% rename from Example/Sources/TableViewCells.swift rename to Example/Sources/Views/TableViewCells.swift index 6b88a5237..615ffc13c 100644 --- a/Example/Sources/TableViewCells.swift +++ b/Example/Sources/Views/TableViewCells.swift @@ -1,7 +1,7 @@ /* MIT License - Copyright (c) 2017 MessageKit + Copyright (c) 2017-2018 MessageKit Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -33,7 +33,7 @@ internal class TextFieldTableViewCell: UITableViewCell { // MARK: - View lifecycle - override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) mainLabel.translatesAutoresizingMaskIntoConstraints = false diff --git a/MessageKit.podspec b/MessageKit.podspec index f7c448689..0d2e1b659 100644 --- a/MessageKit.podspec +++ b/MessageKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'MessageKit' - s.version = '1.0.0' + s.version = '2.0.0' s.license = { :type => "MIT", :file => "LICENSE.md" } s.summary = 'An elegant messages UI library for iOS.' @@ -19,4 +19,7 @@ Pod::Spec.new do |s| s.ios.resource_bundle = { 'MessageKitAssets' => 'Assets/MessageKitAssets.bundle/Images' } s.requires_arc = true + + s.dependency 'MessageInputBar/Core' + end diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index aecf30827..973d17922 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -14,12 +14,8 @@ 1F066E1D1FDA3C1700E11013 /* SenderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F066E1C1FDA3C1700E11013 /* SenderSpec.swift */; }; 1F066E211FDA3DEA00E11013 /* AvatarSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F066E201FDA3DEA00E11013 /* AvatarSpec.swift */; }; 1F066E231FDA3F0200E11013 /* DetectorTypeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F066E221FDA3F0200E11013 /* DetectorTypeSpec.swift */; }; - 1F087D451FD274FD00E95A45 /* Nimble.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 1F7FC8C71FD26F49006CC979 /* Nimble.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 1F087D461FD274FD00E95A45 /* Quick.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 1F7FC8C51FD26F33006CC979 /* Quick.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 1F6C040C206A2891007BDE44 /* MessageContentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6C040B206A2891007BDE44 /* MessageContentCell.swift */; }; 1F6C040E206A2AF4007BDE44 /* MessageReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6C040D206A2AF4007BDE44 /* MessageReusableView.swift */; }; - 1F7FC8C61FD26F33006CC979 /* Quick.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F7FC8C51FD26F33006CC979 /* Quick.framework */; }; - 1F7FC8C81FD26F49006CC979 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F7FC8C71FD26F49006CC979 /* Nimble.framework */; }; 1F82D1431FB1B75B00B81A88 /* AvatarPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */; }; 1FCA6D30201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */; }; 1FD589602064E08A004B5081 /* MediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD5895F2064E08A004B5081 /* MediaItem.swift */; }; @@ -34,15 +30,11 @@ 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A320087C82004FD648 /* MessageKitError.swift */; }; 1FF377AA20087D78004FD648 /* MessagesViewController+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */; }; 1FF377AC20087DA2004FD648 /* MessagesViewController+Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */; }; - 38C57C791F9AE3E50043CC03 /* SeparatorLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C57C781F9AE3E50043CC03 /* SeparatorLine.swift */; }; - 38C57C7C1F9AE4890043CC03 /* InputStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C57C7B1F9AE4870043CC03 /* InputStackView.swift */; }; + 382C794221705D2000F4FAF5 /* HorizontalEdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382C794121705D2000F4FAF5 /* HorizontalEdgeInsets.swift */; }; 88916B2D1CF0DF2F00469F91 /* MessageKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 88916B221CF0DF2F00469F91 /* MessageKit.framework */; }; 8962AC8A1F87AB7D0030B058 /* MessagesCollectionViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC831F87AB230030B058 /* MessagesCollectionViewTests.swift */; }; - 8962AC8B1F87AB7D0030B058 /* InputBarItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC841F87AB230030B058 /* InputBarItemTests.swift */; }; 8962AC8C1F87AB7D0030B058 /* AvatarViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC851F87AB230030B058 /* AvatarViewTests.swift */; }; 8962AC8D1F87AB7D0030B058 /* MessageCollectionViewCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC861F87AB230030B058 /* MessageCollectionViewCellTests.swift */; }; - 8962AC8E1F87AB7D0030B058 /* InputTextViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC871F87AB230030B058 /* InputTextViewTests.swift */; }; - 8962AC8F1F87AB7D0030B058 /* MessageInputBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC881F87AB230030B058 /* MessageInputBarTests.swift */; }; 8962AC911F87AB860030B058 /* MessagesViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC7D1F87AB230030B058 /* MessagesViewControllerTests.swift */; }; 8962AC941F87AB860030B058 /* MessageKitDateFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC741F87AB230030B058 /* MessageKitDateFormatterTests.swift */; }; 8962AC951F87AB860030B058 /* MessageStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC751F87AB230030B058 /* MessageStyleTests.swift */; }; @@ -64,9 +56,6 @@ B7A03F3D1F866946006AEF79 /* MediaMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F391F866946006AEF79 /* MediaMessageCell.swift */; }; B7A03F461F86694F006AEF79 /* AvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F3E1F86694F006AEF79 /* AvatarView.swift */; }; B7A03F471F86694F006AEF79 /* MessageLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F3F1F86694F006AEF79 /* MessageLabel.swift */; }; - B7A03F481F86694F006AEF79 /* InputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F401F86694F006AEF79 /* InputTextView.swift */; }; - B7A03F491F86694F006AEF79 /* InputBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F411F86694F006AEF79 /* InputBarItem.swift */; }; - B7A03F4A1F86694F006AEF79 /* MessageInputBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F421F86694F006AEF79 /* MessageInputBar.swift */; }; B7A03F4B1F86694F006AEF79 /* MessageContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F431F86694F006AEF79 /* MessageContainerView.swift */; }; B7A03F4C1F86694F006AEF79 /* PlayButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F441F86694F006AEF79 /* PlayButtonView.swift */; }; B7A03F4D1F86694F006AEF79 /* MessagesCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F451F86694F006AEF79 /* MessagesCollectionView.swift */; }; @@ -77,7 +66,6 @@ B7A03F5F1F8669CA006AEF79 /* MessageLabelDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F551F8669C9006AEF79 /* MessageLabelDelegate.swift */; }; B7A03F601F8669CA006AEF79 /* MessagesDisplayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F561F8669C9006AEF79 /* MessagesDisplayDelegate.swift */; }; B7A03F611F8669CA006AEF79 /* MessagesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F571F8669CA006AEF79 /* MessagesDataSource.swift */; }; - B7A03F621F8669CA006AEF79 /* MessageInputBarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F581F8669CA006AEF79 /* MessageInputBarDelegate.swift */; }; B7A03F6B1F8669EB006AEF79 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F651F8669EB006AEF79 /* UIColor+Extensions.swift */; }; B7A03F6C1F8669EB006AEF79 /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F661F8669EB006AEF79 /* UIView+Extensions.swift */; }; B7A03F6D1F8669EB006AEF79 /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F671F8669EB006AEF79 /* Bundle+Extensions.swift */; }; @@ -85,6 +73,7 @@ B7A03F731F866A06006AEF79 /* MessageKit+Availability.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F701F866A06006AEF79 /* MessageKit+Availability.swift */; }; B7A03F751F866A06006AEF79 /* MessageKit.h in Headers */ = {isa = PBXBuildFile; fileRef = B7A03F721F866A06006AEF79 /* MessageKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; B7A03F7B1F866B85006AEF79 /* MessageCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F7A1F866B85006AEF79 /* MessageCollectionViewCell.swift */; }; + C42FF512220E1F6E001F328D /* MessageKitAssets.bundle in Resources */ = {isa = PBXBuildFile; fileRef = B0C8D99A1F73076B000A86E4 /* MessageKitAssets.bundle */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -95,6 +84,167 @@ remoteGlobalIDString = 88916B211CF0DF2F00469F91; remoteInfo = MessageKit; }; + C42FF4C5220E1E32001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4C0220E1E32001F328D /* MessageInputBar.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 386FB17C20C496C1006A93BA; + remoteInfo = MessageInputBar; + }; + C42FF4C7220E1E32001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4C0220E1E32001F328D /* MessageInputBar.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 386FB18520C496C1006A93BA; + remoteInfo = MessageInputBarTests; + }; + C42FF4D3220E1E37001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4C9220E1E37001F328D /* Nimble.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 1F925EAD195C0D6300ED456B; + remoteInfo = "Nimble-macOS"; + }; + C42FF4D5220E1E37001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4C9220E1E37001F328D /* Nimble.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 1F925EB7195C0D6300ED456B; + remoteInfo = "Nimble-macOSTests"; + }; + C42FF4D7220E1E37001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4C9220E1E37001F328D /* Nimble.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 1F1A74291940169200FFFC47; + remoteInfo = "Nimble-iOS"; + }; + C42FF4D9220E1E37001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4C9220E1E37001F328D /* Nimble.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 1F1A74341940169200FFFC47; + remoteInfo = "Nimble-iOSTests"; + }; + C42FF4DB220E1E37001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4C9220E1E37001F328D /* Nimble.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 1F5DF1551BDCA0CE00C3A531; + remoteInfo = "Nimble-tvOS"; + }; + C42FF4DD220E1E37001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4C9220E1E37001F328D /* Nimble.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 1F5DF15E1BDCA0CE00C3A531; + remoteInfo = "Nimble-tvOSTests"; + }; + C42FF4EF220E1E3D001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4DF220E1E3D001F328D /* Quick.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = DAEB6B8E1943873100289F44; + remoteInfo = "Quick-macOS"; + }; + C42FF4F1220E1E3D001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4DF220E1E3D001F328D /* Quick.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = DAEB6B991943873100289F44; + remoteInfo = "Quick - macOSTests"; + }; + C42FF4F3220E1E3D001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4DF220E1E3D001F328D /* Quick.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = DA5663E81A4C8D8500193C88; + remoteInfo = "QuickFocused - macOSTests"; + }; + C42FF4F5220E1E3D001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4DF220E1E3D001F328D /* Quick.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 64076CF51D6D7C2000E2B499; + remoteInfo = "QuickAfterSuite - macOSTests"; + }; + C42FF4F7220E1E3D001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4DF220E1E3D001F328D /* Quick.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 5A5D117C19473F2100F6D13D; + remoteInfo = "Quick-iOS"; + }; + C42FF4F9220E1E3D001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4DF220E1E3D001F328D /* Quick.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 5A5D118619473F2100F6D13D; + remoteInfo = "Quick - iOSTests"; + }; + C42FF4FB220E1E3D001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4DF220E1E3D001F328D /* Quick.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = DA9876B21A4C70EB0004AA17; + remoteInfo = "QuickFocused - iOSTests"; + }; + C42FF4FD220E1E3D001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4DF220E1E3D001F328D /* Quick.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 64076D081D6D7CD600E2B499; + remoteInfo = "QuickAfterSuite - iOSTests"; + }; + C42FF4FF220E1E3D001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4DF220E1E3D001F328D /* Quick.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 1F118CD51BDCA4AB005013A2; + remoteInfo = "Quick-tvOS"; + }; + C42FF501220E1E3D001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4DF220E1E3D001F328D /* Quick.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 1F118CDE1BDCA4AB005013A2; + remoteInfo = "Quick - tvOSTests"; + }; + C42FF503220E1E3D001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4DF220E1E3D001F328D /* Quick.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 1F118CF01BDCA4BB005013A2; + remoteInfo = "QuickFocused - tvOSTests"; + }; + C42FF505220E1E3D001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4DF220E1E3D001F328D /* Quick.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 64076D1A1D6D7CEA00E2B499; + remoteInfo = "QuickAfterSuite - tvOSTests"; + }; + C42FF507220E1E67001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4C0220E1E32001F328D /* MessageInputBar.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 386FB17B20C496C1006A93BA; + remoteInfo = MessageInputBar; + }; + C42FF50D220E1EB5001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4DF220E1E3D001F328D /* Quick.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 5A5D117B19473F2100F6D13D; + remoteInfo = "Quick-iOS"; + }; + C42FF50F220E1EB5001F328D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C42FF4C9220E1E37001F328D /* Nimble.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 1F1A74281940169200FFFC47; + remoteInfo = "Nimble-iOS"; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -104,8 +254,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 1F087D451FD274FD00E95A45 /* Nimble.framework in CopyFiles */, - 1F087D461FD274FD00E95A45 /* Quick.framework in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -136,8 +284,8 @@ 1FF377A320087C82004FD648 /* MessageKitError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageKitError.swift; sourceTree = ""; }; 1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Menu.swift"; sourceTree = ""; }; 1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Keyboard.swift"; sourceTree = ""; }; - 38C57C781F9AE3E50043CC03 /* SeparatorLine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeparatorLine.swift; sourceTree = ""; }; - 38C57C7B1F9AE4870043CC03 /* InputStackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputStackView.swift; sourceTree = ""; }; + 382C794121705D2000F4FAF5 /* HorizontalEdgeInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalEdgeInsets.swift; sourceTree = ""; }; + 38C2AE7B20D4878D00F8079E /* MessageInputBar.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageInputBar.framework; path = Carthage/Build/iOS/MessageInputBar.framework; sourceTree = ""; }; 88916B221CF0DF2F00469F91 /* MessageKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MessageKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 88916B2C1CF0DF2F00469F91 /* MessageKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MessageKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 8962AC741F87AB230030B058 /* MessageKitDateFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageKitDateFormatterTests.swift; sourceTree = ""; }; @@ -148,11 +296,8 @@ 8962AC7F1F87AB230030B058 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 8962AC811F87AB230030B058 /* MessagesDisplayDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesDisplayDelegateTests.swift; sourceTree = ""; }; 8962AC831F87AB230030B058 /* MessagesCollectionViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesCollectionViewTests.swift; sourceTree = ""; }; - 8962AC841F87AB230030B058 /* InputBarItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputBarItemTests.swift; sourceTree = ""; }; 8962AC851F87AB230030B058 /* AvatarViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarViewTests.swift; sourceTree = ""; }; 8962AC861F87AB230030B058 /* MessageCollectionViewCellTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewCellTests.swift; sourceTree = ""; }; - 8962AC871F87AB230030B058 /* InputTextViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputTextViewTests.swift; sourceTree = ""; }; - 8962AC881F87AB230030B058 /* MessageInputBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageInputBarTests.swift; sourceTree = ""; }; B0C8D99A1F73076B000A86E4 /* MessageKitAssets.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = MessageKitAssets.bundle; sourceTree = ""; }; B7A03F161F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesCollectionViewFlowLayout.swift; sourceTree = ""; }; B7A03F171F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesCollectionViewLayoutAttributes.swift; sourceTree = ""; }; @@ -170,9 +315,6 @@ B7A03F391F866946006AEF79 /* MediaMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaMessageCell.swift; sourceTree = ""; }; B7A03F3E1F86694F006AEF79 /* AvatarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarView.swift; sourceTree = ""; }; B7A03F3F1F86694F006AEF79 /* MessageLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageLabel.swift; sourceTree = ""; }; - B7A03F401F86694F006AEF79 /* InputTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextView.swift; sourceTree = ""; }; - B7A03F411F86694F006AEF79 /* InputBarItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputBarItem.swift; sourceTree = ""; }; - B7A03F421F86694F006AEF79 /* MessageInputBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageInputBar.swift; sourceTree = ""; }; B7A03F431F86694F006AEF79 /* MessageContainerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageContainerView.swift; sourceTree = ""; }; B7A03F441F86694F006AEF79 /* PlayButtonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayButtonView.swift; sourceTree = ""; }; B7A03F451F86694F006AEF79 /* MessagesCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesCollectionView.swift; sourceTree = ""; }; @@ -183,7 +325,6 @@ B7A03F551F8669C9006AEF79 /* MessageLabelDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageLabelDelegate.swift; sourceTree = ""; }; B7A03F561F8669C9006AEF79 /* MessagesDisplayDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDisplayDelegate.swift; sourceTree = ""; }; B7A03F571F8669CA006AEF79 /* MessagesDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDataSource.swift; sourceTree = ""; }; - B7A03F581F8669CA006AEF79 /* MessageInputBarDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageInputBarDelegate.swift; sourceTree = ""; }; B7A03F651F8669EB006AEF79 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; B7A03F661F8669EB006AEF79 /* UIView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = ""; }; B7A03F671F8669EB006AEF79 /* Bundle+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+Extensions.swift"; sourceTree = ""; }; @@ -192,6 +333,9 @@ B7A03F711F866A06006AEF79 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B7A03F721F866A06006AEF79 /* MessageKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MessageKit.h; sourceTree = ""; }; B7A03F7A1F866B85006AEF79 /* MessageCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCollectionViewCell.swift; sourceTree = ""; }; + C42FF4C0220E1E32001F328D /* MessageInputBar.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = MessageInputBar.xcodeproj; path = Carthage/Checkouts/MessageInputBar/MessageInputBar.xcodeproj; sourceTree = SOURCE_ROOT; }; + C42FF4C9220E1E37001F328D /* Nimble.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Nimble.xcodeproj; path = Carthage/Checkouts/Nimble/Nimble.xcodeproj; sourceTree = SOURCE_ROOT; }; + C42FF4DF220E1E3D001F328D /* Quick.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Quick.xcodeproj; path = Carthage/Checkouts/Quick/Quick.xcodeproj; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -206,8 +350,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 1F7FC8C81FD26F49006CC979 /* Nimble.framework in Frameworks */, - 1F7FC8C61FD26F33006CC979 /* Quick.framework in Frameworks */, 88916B2D1CF0DF2F00469F91 /* MessageKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -218,6 +360,7 @@ 1F7FC8C21FD26F22006CC979 /* Frameworks */ = { isa = PBXGroup; children = ( + 38C2AE7B20D4878D00F8079E /* MessageInputBar.framework */, 1F7FC8C71FD26F49006CC979 /* Nimble.framework */, 1F7FC8C51FD26F33006CC979 /* Quick.framework */, 1F7FC8C31FD26F22006CC979 /* Carthage */, @@ -248,6 +391,7 @@ 88916B181CF0DF2F00469F91 = { isa = PBXGroup; children = ( + C42FF4BF220E1E21001F328D /* Dependencies */, 88916B3C1CF0DF5100469F91 /* Sources */, 88916B411CF0DF5900469F91 /* Tests */, 88916B231CF0DF2F00469F91 /* Products */, @@ -344,11 +488,8 @@ isa = PBXGroup; children = ( 8962AC831F87AB230030B058 /* MessagesCollectionViewTests.swift */, - 8962AC841F87AB230030B058 /* InputBarItemTests.swift */, 8962AC851F87AB230030B058 /* AvatarViewTests.swift */, 8962AC861F87AB230030B058 /* MessageCollectionViewCellTests.swift */, - 8962AC871F87AB230030B058 /* InputTextViewTests.swift */, - 8962AC881F87AB230030B058 /* MessageInputBarTests.swift */, ); path = ViewsTests; sourceTree = ""; @@ -381,11 +522,12 @@ B7A03F1C1F866895006AEF79 /* Avatar.swift */, 1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */, B7A03F211F866895006AEF79 /* DetectorType.swift */, + 382C794121705D2000F4FAF5 /* HorizontalEdgeInsets.swift */, B7A03F221F866895006AEF79 /* LabelAlignment.swift */, B7A03F1D1F866895006AEF79 /* LocationMessageSnapshotOptions.swift */, B7A03F231F866895006AEF79 /* MessageKind.swift */, - 1FF377A320087C82004FD648 /* MessageKitError.swift */, B7A03F1B1F866895006AEF79 /* MessageKitDateFormatter.swift */, + 1FF377A320087C82004FD648 /* MessageKitError.swift */, B7A03F1F1F866895006AEF79 /* MessageStyle.swift */, B7A03F1A1F866895006AEF79 /* NSConstraintLayoutSet.swift */, B7A03F1E1F866895006AEF79 /* Sender.swift */, @@ -399,16 +541,11 @@ 2EB618F01F84676A007FBA0E /* Cells */, 2EB618F11F846899007FBA0E /* Headers & Footers */, B7A03F3E1F86694F006AEF79 /* AvatarView.swift */, - B7A03F411F86694F006AEF79 /* InputBarItem.swift */, - B7A03F401F86694F006AEF79 /* InputTextView.swift */, - 38C57C7B1F9AE4870043CC03 /* InputStackView.swift */, 1FE783A7206633C0007FA024 /* InsetLabel.swift */, B7A03F431F86694F006AEF79 /* MessageContainerView.swift */, - B7A03F421F86694F006AEF79 /* MessageInputBar.swift */, B7A03F3F1F86694F006AEF79 /* MessageLabel.swift */, B7A03F451F86694F006AEF79 /* MessagesCollectionView.swift */, B7A03F441F86694F006AEF79 /* PlayButtonView.swift */, - 38C57C781F9AE3E50043CC03 /* SeparatorLine.swift */, ); path = Views; sourceTree = ""; @@ -417,7 +554,6 @@ isa = PBXGroup; children = ( B7A03F521F8669C9006AEF79 /* MessageCellDelegate.swift */, - B7A03F581F8669CA006AEF79 /* MessageInputBarDelegate.swift */, B7A03F551F8669C9006AEF79 /* MessageLabelDelegate.swift */, B7A03F571F8669CA006AEF79 /* MessagesDataSource.swift */, B7A03F561F8669C9006AEF79 /* MessagesDisplayDelegate.swift */, @@ -463,6 +599,57 @@ path = Supporting; sourceTree = ""; }; + C42FF4BF220E1E21001F328D /* Dependencies */ = { + isa = PBXGroup; + children = ( + C42FF4DF220E1E3D001F328D /* Quick.xcodeproj */, + C42FF4C9220E1E37001F328D /* Nimble.xcodeproj */, + C42FF4C0220E1E32001F328D /* MessageInputBar.xcodeproj */, + ); + path = Dependencies; + sourceTree = ""; + }; + C42FF4C1220E1E32001F328D /* Products */ = { + isa = PBXGroup; + children = ( + C42FF4C6220E1E32001F328D /* MessageInputBar.framework */, + C42FF4C8220E1E32001F328D /* MessageInputBarTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + C42FF4CA220E1E37001F328D /* Products */ = { + isa = PBXGroup; + children = ( + C42FF4D4220E1E37001F328D /* Nimble.framework */, + C42FF4D6220E1E37001F328D /* NimbleTests.xctest */, + C42FF4D8220E1E37001F328D /* Nimble.framework */, + C42FF4DA220E1E37001F328D /* NimbleTests.xctest */, + C42FF4DC220E1E37001F328D /* Nimble.framework */, + C42FF4DE220E1E37001F328D /* NimbleTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + C42FF4E0220E1E3D001F328D /* Products */ = { + isa = PBXGroup; + children = ( + C42FF4F0220E1E3D001F328D /* Quick.framework */, + C42FF4F2220E1E3D001F328D /* Quick - macOSTests.xctest */, + C42FF4F4220E1E3D001F328D /* QuickFocused - macOSTests.xctest */, + C42FF4F6220E1E3D001F328D /* QuickAfterSuite - macOSTests.xctest */, + C42FF4F8220E1E3D001F328D /* Quick.framework */, + C42FF4FA220E1E3D001F328D /* Quick - iOSTests.xctest */, + C42FF4FC220E1E3D001F328D /* QuickFocused - iOSTests.xctest */, + C42FF4FE220E1E3D001F328D /* QuickAfterSuite - iOSTests.xctest */, + C42FF500220E1E3D001F328D /* Quick.framework */, + C42FF502220E1E3D001F328D /* Quick - tvOSTests.xctest */, + C42FF504220E1E3D001F328D /* QuickFocused - tvOSTests.xctest */, + C42FF506220E1E3D001F328D /* QuickAfterSuite - tvOSTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -490,6 +677,7 @@ buildRules = ( ); dependencies = ( + C42FF508220E1E67001F328D /* PBXTargetDependency */, ); name = MessageKit; productName = MessageKit; @@ -508,6 +696,8 @@ buildRules = ( ); dependencies = ( + C42FF50E220E1EB5001F328D /* PBXTargetDependency */, + C42FF510220E1EB5001F328D /* PBXTargetDependency */, 88916B2F1CF0DF2F00469F91 /* PBXTargetDependency */, ); name = MessageKitTests; @@ -522,7 +712,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0900; + LastUpgradeCheck = 0940; ORGANIZATIONNAME = MessageKit; TargetAttributes = { 88916B211CF0DF2F00469F91 = { @@ -531,6 +721,7 @@ }; 88916B2B1CF0DF2F00469F91 = { CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = EVL2DEPTTR; }; }; }; @@ -544,6 +735,20 @@ mainGroup = 88916B181CF0DF2F00469F91; productRefGroup = 88916B231CF0DF2F00469F91 /* Products */; projectDirPath = ""; + projectReferences = ( + { + ProductGroup = C42FF4C1220E1E32001F328D /* Products */; + ProjectRef = C42FF4C0220E1E32001F328D /* MessageInputBar.xcodeproj */; + }, + { + ProductGroup = C42FF4CA220E1E37001F328D /* Products */; + ProjectRef = C42FF4C9220E1E37001F328D /* Nimble.xcodeproj */; + }, + { + ProductGroup = C42FF4E0220E1E3D001F328D /* Products */; + ProjectRef = C42FF4DF220E1E3D001F328D /* Quick.xcodeproj */; + }, + ); projectRoot = ""; targets = ( 88916B211CF0DF2F00469F91 /* MessageKit */, @@ -552,6 +757,149 @@ }; /* End PBXProject section */ +/* Begin PBXReferenceProxy section */ + C42FF4C6220E1E32001F328D /* MessageInputBar.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = MessageInputBar.framework; + remoteRef = C42FF4C5220E1E32001F328D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C42FF4C8220E1E32001F328D /* MessageInputBarTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = MessageInputBarTests.xctest; + remoteRef = C42FF4C7220E1E32001F328D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C42FF4D4220E1E37001F328D /* Nimble.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = Nimble.framework; + remoteRef = C42FF4D3220E1E37001F328D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C42FF4D6220E1E37001F328D /* NimbleTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = NimbleTests.xctest; + remoteRef = C42FF4D5220E1E37001F328D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C42FF4D8220E1E37001F328D /* Nimble.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = Nimble.framework; + remoteRef = C42FF4D7220E1E37001F328D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C42FF4DA220E1E37001F328D /* NimbleTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = NimbleTests.xctest; + remoteRef = C42FF4D9220E1E37001F328D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C42FF4DC220E1E37001F328D /* Nimble.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = Nimble.framework; + remoteRef = C42FF4DB220E1E37001F328D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C42FF4DE220E1E37001F328D /* NimbleTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = NimbleTests.xctest; + remoteRef = C42FF4DD220E1E37001F328D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C42FF4F0220E1E3D001F328D /* Quick.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = Quick.framework; + remoteRef = C42FF4EF220E1E3D001F328D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C42FF4F2220E1E3D001F328D /* Quick - macOSTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = "Quick - macOSTests.xctest"; + remoteRef = C42FF4F1220E1E3D001F328D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C42FF4F4220E1E3D001F328D /* QuickFocused - macOSTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = "QuickFocused - macOSTests.xctest"; + remoteRef = C42FF4F3220E1E3D001F328D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C42FF4F6220E1E3D001F328D /* QuickAfterSuite - macOSTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = "QuickAfterSuite - macOSTests.xctest"; + remoteRef = C42FF4F5220E1E3D001F328D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C42FF4F8220E1E3D001F328D /* Quick.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = Quick.framework; + remoteRef = C42FF4F7220E1E3D001F328D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C42FF4FA220E1E3D001F328D /* Quick - iOSTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = "Quick - iOSTests.xctest"; + remoteRef = C42FF4F9220E1E3D001F328D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C42FF4FC220E1E3D001F328D /* QuickFocused - iOSTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = "QuickFocused - iOSTests.xctest"; + remoteRef = C42FF4FB220E1E3D001F328D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C42FF4FE220E1E3D001F328D /* QuickAfterSuite - iOSTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = "QuickAfterSuite - iOSTests.xctest"; + remoteRef = C42FF4FD220E1E3D001F328D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C42FF500220E1E3D001F328D /* Quick.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = Quick.framework; + remoteRef = C42FF4FF220E1E3D001F328D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C42FF502220E1E3D001F328D /* Quick - tvOSTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = "Quick - tvOSTests.xctest"; + remoteRef = C42FF501220E1E3D001F328D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C42FF504220E1E3D001F328D /* QuickFocused - tvOSTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = "QuickFocused - tvOSTests.xctest"; + remoteRef = C42FF503220E1E3D001F328D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C42FF506220E1E3D001F328D /* QuickAfterSuite - tvOSTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = "QuickAfterSuite - tvOSTests.xctest"; + remoteRef = C42FF505220E1E3D001F328D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + /* Begin PBXResourcesBuildPhase section */ 88916B201CF0DF2F00469F91 /* Resources */ = { isa = PBXResourcesBuildPhase; @@ -565,6 +913,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + C42FF512220E1F6E001F328D /* MessageKitAssets.bundle in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -582,7 +931,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; + shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -591,20 +940,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 382C794221705D2000F4FAF5 /* HorizontalEdgeInsets.swift in Sources */, B7A03F3C1F866946006AEF79 /* LocationMessageCell.swift in Sources */, 1FF377AA20087D78004FD648 /* MessagesViewController+Menu.swift in Sources */, - 38C57C7C1F9AE4890043CC03 /* InputStackView.swift in Sources */, B7A03F5B1F8669CA006AEF79 /* MessageType.swift in Sources */, - B7A03F491F86694F006AEF79 /* InputBarItem.swift in Sources */, B7A03F601F8669CA006AEF79 /* MessagesDisplayDelegate.swift in Sources */, 1FE783A8206633C0007FA024 /* InsetLabel.swift in Sources */, B7A03F5C1F8669CA006AEF79 /* MessageCellDelegate.swift in Sources */, 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */, - B7A03F4A1F86694F006AEF79 /* MessageInputBar.swift in Sources */, 1F6C040E206A2AF4007BDE44 /* MessageReusableView.swift in Sources */, B7A03F4B1F86694F006AEF79 /* MessageContainerView.swift in Sources */, B7A03F281F866895006AEF79 /* LocationMessageSnapshotOptions.swift in Sources */, - B7A03F481F86694F006AEF79 /* InputTextView.swift in Sources */, B7A03F6C1F8669EB006AEF79 /* UIView+Extensions.swift in Sources */, B7A03F3A1F866946006AEF79 /* TextMessageCell.swift in Sources */, B7A03F191F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift in Sources */, @@ -628,7 +974,6 @@ B7A03F291F866895006AEF79 /* Sender.swift in Sources */, B7A03F4F1F86697C006AEF79 /* MessagesViewController.swift in Sources */, B7A03F251F866895006AEF79 /* NSConstraintLayoutSet.swift in Sources */, - B7A03F621F8669CA006AEF79 /* MessageInputBarDelegate.swift in Sources */, 0EE91E661FDEC888005420A2 /* CGRect+Extensions.swift in Sources */, B7A03F181F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift in Sources */, B7A03F2A1F866895006AEF79 /* MessageStyle.swift in Sources */, @@ -639,7 +984,6 @@ 1FE7839E20662835007FA024 /* MessageSizeCalculator.swift in Sources */, 1FE783A6206629C2007FA024 /* LocationMessageSizeCalculator.swift in Sources */, B7A03F2D1F866895006AEF79 /* LabelAlignment.swift in Sources */, - 38C57C791F9AE3E50043CC03 /* SeparatorLine.swift in Sources */, 1F6C040C206A2891007BDE44 /* MessageContentCell.swift in Sources */, B7A03F2C1F866895006AEF79 /* DetectorType.swift in Sources */, B7A03F271F866895006AEF79 /* Avatar.swift in Sources */, @@ -655,19 +999,16 @@ 8962AC8C1F87AB7D0030B058 /* AvatarViewTests.swift in Sources */, 1F066E141FD90BB700E11013 /* MessageLabelSpec.swift in Sources */, 8962AC941F87AB860030B058 /* MessageKitDateFormatterTests.swift in Sources */, - 8962AC8E1F87AB7D0030B058 /* InputTextViewTests.swift in Sources */, 8962AC911F87AB860030B058 /* MessagesViewControllerTests.swift in Sources */, 1F066E231FDA3F0200E11013 /* DetectorTypeSpec.swift in Sources */, 1F066E211FDA3DEA00E11013 /* AvatarSpec.swift in Sources */, 1FD589612064E1B5004B5081 /* MockMessagesDataSource.swift in Sources */, - 8962AC8F1F87AB7D0030B058 /* MessageInputBarTests.swift in Sources */, 1F066E131FD90BB600E11013 /* MessagesViewControllerSpec.swift in Sources */, 1F066E1D1FDA3C1700E11013 /* SenderSpec.swift in Sources */, 8962AC8A1F87AB7D0030B058 /* MessagesCollectionViewTests.swift in Sources */, 8962AC8D1F87AB7D0030B058 /* MessageCollectionViewCellTests.swift in Sources */, 8962AC991F87AB860030B058 /* MessagesDisplayDelegateTests.swift in Sources */, 1FD589622064E1B9004B5081 /* MockMessage.swift in Sources */, - 8962AC8B1F87AB7D0030B058 /* InputBarItemTests.swift in Sources */, 8962AC951F87AB860030B058 /* MessageStyleTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -680,6 +1021,21 @@ target = 88916B211CF0DF2F00469F91 /* MessageKit */; targetProxy = 88916B2E1CF0DF2F00469F91 /* PBXContainerItemProxy */; }; + C42FF508220E1E67001F328D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = MessageInputBar; + targetProxy = C42FF507220E1E67001F328D /* PBXContainerItemProxy */; + }; + C42FF50E220E1EB5001F328D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "Quick-iOS"; + targetProxy = C42FF50D220E1EB5001F328D /* PBXContainerItemProxy */; + }; + C42FF510220E1EB5001F328D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "Nimble-iOS"; + targetProxy = C42FF50F220E1EB5001F328D /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -696,12 +1052,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -734,7 +1092,7 @@ ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -754,12 +1112,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -784,7 +1144,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -801,15 +1161,20 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + ); INFOPLIST_FILE = "$(SRCROOT)/Sources/Supporting/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks $(FRAMEWORK_SEARCH_PATHS)"; + MACH_O_TYPE = staticlib; PRODUCT_BUNDLE_IDENTIFIER = com.messagekit.MessageKit; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; }; name = Debug; }; @@ -822,27 +1187,33 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + ); INFOPLIST_FILE = "$(SRCROOT)/Sources/Supporting/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks $(FRAMEWORK_SEARCH_PATHS)"; + MACH_O_TYPE = staticlib; PRODUCT_BUNDLE_IDENTIFIER = com.messagekit.MessageKit; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; }; name = Release; }; 88916B3A1CF0DF2F00469F91 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + DEVELOPMENT_TEAM = EVL2DEPTTR; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/iOS", ); INFOPLIST_FILE = "Tests/Supporting Files/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks $(FRAMEWORK_SEARCH_PATHS)"; PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.MessageKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; }; @@ -851,12 +1222,13 @@ 88916B3B1CF0DF2F00469F91 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + DEVELOPMENT_TEAM = EVL2DEPTTR; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/iOS", ); INFOPLIST_FILE = "Tests/Supporting Files/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks $(FRAMEWORK_SEARCH_PATHS)"; PRODUCT_BUNDLE_IDENTIFIER = com.hexedbits.MessageKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; diff --git a/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKit.xcscheme b/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKit.xcscheme index 34cab23ec..0bb9eb20c 100644 --- a/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKit.xcscheme +++ b/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKit.xcscheme @@ -1,6 +1,6 @@ + codeCoverageEnabled = "YES" + shouldUseLaunchSchemeArgsEnv = "YES"> @@ -57,7 +56,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKitTests.xcscheme b/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKitTests.xcscheme index 20ed3c6ab..1569e1b64 100644 --- a/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKitTests.xcscheme +++ b/MessageKit.xcodeproj/xcshareddata/xcschemes/MessageKitTests.xcscheme @@ -1,6 +1,6 @@ + codeCoverageEnabled = "YES" + shouldUseLaunchSchemeArgsEnv = "YES"> @@ -32,7 +31,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/README.md b/README.md index ceaec4553..491730b62 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,13 @@ To integrate MessageKit using Carthage, add the following to your `Cartfile`: github "MessageKit/MessageKit" ```` +## Getting Started + +Please have a look at the [Quick Start guide](https://github.com/MessageKit/MessageKit/blob/master/Documentation/QuickStart.md), the [FAQs](https://github.com/MessageKit/MessageKit/blob/master/Documentation/FAQs.md) and the [MessageInputBar docs](https://github.com/MessageKit/MessageKit/blob/master/Documentation/MessageInputBar.md). + +If you have any issues have a look at the [Example](https://github.com/MessageKit/MessageKit/tree/master/Example) project or write a question with the "messagekit" tag on [Stack Overflow](https://stackoverflow.com/questions/tagged/messagekit). + + ## Requirements - **iOS9** or later @@ -90,7 +97,7 @@ github "MessageKit/MessageKit" ## Contributing Great! Look over these things first. -- Please read our [Code of Conduct](https://github.com/MessageKit/MessageKit/blob/master/Code_of_Conduct.md) +- Please read our [Code of Conduct](https://github.com/MessageKit/MessageKit/blob/master/CODE_OF_CONDUCT.md) - Check the [Contributing Guide Lines](https://github.com/MessageKit/MessageKit/blob/master/CONTRIBUTING.md). - Come join us on [Slack](https://join.slack.com/t/messagekit/shared_invite/MjI4NzIzNzMyMzU0LTE1MDMwODIzMDUtYzllYzIyNTU4MA) and 🗣 don't be a stranger. - Check out the [current issues](https://github.com/MessageKit/MessageKit/issues) and see if you can tackle any of those. @@ -116,6 +123,8 @@ Interested in contributing to MessageKit? Click here to join our [Slack](https:/ Add your app to the list of apps using this library and make a pull request. - [MediQuo](https://www.mediquo.com) +- [RappresentaMe](https://itunes.apple.com/it/app/rappresentame/id1330914443) +- [WiseEyes](https://itunes.apple.com/us/app/wiseeyes/id1391408511?mt=8) *Please provide attribution, it is greatly appreciated.* diff --git a/Sources/Controllers/MessagesViewController+Keyboard.swift b/Sources/Controllers/MessagesViewController+Keyboard.swift index d09cb2887..881b6aaa0 100644 --- a/Sources/Controllers/MessagesViewController+Keyboard.swift +++ b/Sources/Controllers/MessagesViewController+Keyboard.swift @@ -23,28 +23,29 @@ */ import Foundation +import MessageInputBar extension MessagesViewController { // MARK: - Register / Unregister Observers internal func addKeyboardObservers() { - NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.handleKeyboardDidChangeState(_:)), name: .UIKeyboardWillChangeFrame, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.handleTextViewDidBeginEditing(_:)), name: .UITextViewTextDidBeginEditing, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.adjustScrollViewInset), name: .UIDeviceOrientationDidChange, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.handleKeyboardDidChangeState(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.handleTextViewDidBeginEditing(_:)), name: UITextView.textDidBeginEditingNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.adjustScrollViewTopInset), name: UIDevice.orientationDidChangeNotification, object: nil) } internal func removeKeyboardObservers() { - NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillChangeFrame, object: nil) - NotificationCenter.default.removeObserver(self, name: .UITextViewTextDidBeginEditing, object: nil) - NotificationCenter.default.removeObserver(self, name: .UIDeviceOrientationDidChange, object: nil) + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UITextView.textDidBeginEditingNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil) } // MARK: - Notification Handlers @objc private func handleTextViewDidBeginEditing(_ notification: Notification) { - if scrollsToBottomOnKeybordBeginsEditing { + if scrollsToBottomOnKeyboardBeginsEditing { guard let inputTextView = notification.object as? InputTextView, inputTextView === messageInputBar.inputTextView else { return } messagesCollectionView.scrollToBottom(animated: true) } @@ -52,12 +53,35 @@ extension MessagesViewController { @objc private func handleKeyboardDidChangeState(_ notification: Notification) { - guard let keyboardEndFrame = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? CGRect else { return } - guard !isMessagesControllerBeingDismissed else { return } + + guard let keyboardStartFrameInScreenCoords = notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? CGRect else { return } + guard !keyboardStartFrameInScreenCoords.isEmpty else { + // WORKAROUND for what seems to be a bug in iPad's keyboard handling in iOS 11: we receive an extra spurious frame change + // notification when undocking the keyboard, with a zero starting frame and an incorrect end frame. The workaround is to + // ignore this notification. + return + } - let newBottomInset = view.frame.height - keyboardEndFrame.minY - iPhoneXBottomInset + // Note that the check above does not exclude all notifications from an undocked keyboard, only the weird ones. + // + // We've tried following Apple's recommended approach of tracking UIKeyboardWillShow / UIKeyboardDidHide and ignoring frame + // change notifications while the keyboard is hidden or undocked (undocked keyboard is considered hidden by those events). + // Unfortunately, we do care about the difference between hidden and undocked, because we have an input bar which is at the + // bottom when the keyboard is hidden, and is tied to the keyboard when it's undocked. + // + // If we follow what Apple recommends and ignore notifications while the keyboard is hidden/undocked, we get an extra inset + // at the bottom when the undocked keyboard is visible (the inset that tries to compensate for the missing input bar). + // (Alternatives like setting newBottomInset to 0 or to the height of the input bar don't work either.) + // + // We could make it work by adding extra checks for the state of the keyboard and compensating accordingly, but it seems easier + // to simply check whether the current keyboard frame, whatever it is (even when undocked), covers the bottom of the collection + // view. + guard let keyboardEndFrameInScreenCoords = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } + let keyboardEndFrame = view.convert(keyboardEndFrameInScreenCoords, from: view.window) + + let newBottomInset = requiredScrollViewBottomInset(forKeyboardFrame: keyboardEndFrame) let differenceOfBottomInset = newBottomInset - messageCollectionViewBottomInset if maintainPositionOnKeyboardFrameChanged && differenceOfBottomInset != 0 { @@ -68,8 +92,10 @@ extension MessagesViewController { messageCollectionViewBottomInset = newBottomInset } + // MARK: - Inset Computation + @objc - internal func adjustScrollViewInset() { + internal func adjustScrollViewTopInset() { if #available(iOS 11.0, *) { // No need to add to the top contentInset } else { @@ -81,23 +107,35 @@ extension MessagesViewController { } } - // MARK: - Helpers + private func requiredScrollViewBottomInset(forKeyboardFrame keyboardFrame: CGRect) -> CGFloat { + // we only need to adjust for the part of the keyboard that covers (i.e. intersects) our collection view; + // see https://developer.apple.com/videos/play/wwdc2017/242/ for more details + let intersection = messagesCollectionView.frame.intersection(keyboardFrame) + + if intersection.isNull || intersection.maxY < messagesCollectionView.frame.maxY { + // The keyboard is hidden, is a hardware one, or is undocked and does not cover the bottom of the collection view. + // Note: intersection.maxY may be less than messagesCollectionView.frame.maxY when dealing with undocked keyboards. + return max(0, additionalBottomInset - automaticallyAddedBottomInset) + } else { + return max(0, intersection.height + additionalBottomInset - automaticallyAddedBottomInset) + } + } - internal var keyboardOffsetFrame: CGRect { - guard let inputFrame = inputAccessoryView?.frame else { return .zero } - return CGRect(origin: inputFrame.origin, size: CGSize(width: inputFrame.width, height: inputFrame.height - iPhoneXBottomInset)) + internal func requiredInitialScrollViewBottomInset() -> CGFloat { + guard let inputAccessoryView = inputAccessoryView else { return 0 } + return max(0, inputAccessoryView.frame.height + additionalBottomInset - automaticallyAddedBottomInset) } - /// On the iPhone X the inputAccessoryView is anchored to the layoutMarginesGuide.bottom anchor - /// so the frame of the inputAccessoryView is larger than the required offset - /// for the MessagesCollectionView. + /// iOS 11's UIScrollView can automatically add safe area insets to its contentInset, + /// which needs to be accounted for when setting the contentInset based on screen coordinates. /// - /// - Returns: The safeAreaInsets.bottom if its an iPhoneX, else 0 - private var iPhoneXBottomInset: CGFloat { + /// - Returns: The distance automatically added to contentInset.bottom, if any. + private var automaticallyAddedBottomInset: CGFloat { if #available(iOS 11.0, *) { - guard UIScreen.main.nativeBounds.height == 2436 else { return 0 } - return view.safeAreaInsets.bottom + return messagesCollectionView.adjustedContentInset.bottom - messagesCollectionView.contentInset.bottom + } else { + return 0 } - return 0 } + } diff --git a/Sources/Controllers/MessagesViewController+Menu.swift b/Sources/Controllers/MessagesViewController+Menu.swift index 573175847..49c873153 100644 --- a/Sources/Controllers/MessagesViewController+Menu.swift +++ b/Sources/Controllers/MessagesViewController+Menu.swift @@ -23,17 +23,18 @@ */ import Foundation +import MessageInputBar extension MessagesViewController { // MARK: - Register / Unregister Observers internal func addMenuControllerObservers() { - NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(_:)), name: .UIMenuControllerWillShowMenu, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(_:)), name: UIMenuController.willShowMenuNotification, object: nil) } internal func removeMenuControllerObservers() { - NotificationCenter.default.removeObserver(self, name: .UIMenuControllerWillShowMenu, object: nil) + NotificationCenter.default.removeObserver(self, name: UIMenuController.willShowMenuNotification, object: nil) } // MARK: - Notification Handlers @@ -45,11 +46,11 @@ extension MessagesViewController { guard let currentMenuController = notification.object as? UIMenuController, let selectedIndexPath = selectedIndexPathForMenu else { return } - NotificationCenter.default.removeObserver(self, name: .UIMenuControllerWillShowMenu, object: nil) + NotificationCenter.default.removeObserver(self, name: UIMenuController.willShowMenuNotification, object: nil) defer { NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(_:)), - name: .UIMenuControllerWillShowMenu, object: nil) + name: UIMenuController.willShowMenuNotification, object: nil) selectedIndexPathForMenu = nil } diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift index 859ba37d8..ff43ffeb1 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.swift @@ -23,6 +23,7 @@ */ import UIKit +import MessageInputBar /// A subclass of `UIViewController` with a `MessagesCollectionView` object /// that is used to display conversation interfaces. @@ -39,7 +40,7 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { /// bottom whenever the `InputTextView` begins editing. /// /// The default value of this property is `false`. - open var scrollsToBottomOnKeybordBeginsEditing: Bool = false + open var scrollsToBottomOnKeyboardBeginsEditing: Bool = false /// A Boolean value that determines whether the `MessagesCollectionView` /// maintains it's current position when the height of the `MessageInputBar` changes. @@ -59,6 +60,17 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { return false } + /// A CGFloat value that adds to (or, if negative, subtracts from) the automatically + /// computed value of `messagesCollectionView.contentInset.bottom`. Meant to be used + /// as a measure of last resort when the built-in algorithm does not produce the right + /// value for your app. Please let us know when you end up having to use this property. + open var additionalBottomInset: CGFloat = 0 { + didSet { + let delta = additionalBottomInset - oldValue + messageCollectionViewBottomInset += delta + } + } + private var isFirstLayout: Bool = true internal var isMessagesControllerBeingDismissed: Bool = false @@ -94,14 +106,19 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { isMessagesControllerBeingDismissed = true } + open override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + isMessagesControllerBeingDismissed = false + } + open override func viewDidLayoutSubviews() { // Hack to prevent animation of the contentInset after viewDidAppear if isFirstLayout { defer { isFirstLayout = false } addKeyboardObservers() - messageCollectionViewBottomInset = keyboardOffsetFrame.height + messageCollectionViewBottomInset = requiredInitialScrollViewBottomInset() } - adjustScrollViewInset() + adjustScrollViewTopInset() } // MARK: - Initializers @@ -164,6 +181,8 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { return collectionView.messagesDataSource?.numberOfItems(inSection: section, in: collectionView) ?? 0 } + /// Note: + /// If you override this method, remember to call MessagesDataSource's customCell(for:at:in:) for MessageKind.custom messages, if necessary open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let messagesCollectionView = collectionView as? MessagesCollectionView else { @@ -190,7 +209,7 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { cell.configure(with: message, at: indexPath, and: messagesCollectionView) return cell case .custom: - fatalError(MessageKitError.customDataUnresolvedCell) + return messagesDataSource.customCell(for: message, at: indexPath, in: messagesCollectionView) } } @@ -205,9 +224,9 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { } switch kind { - case UICollectionElementKindSectionHeader: + case UICollectionView.elementKindSectionHeader: return displayDelegate.messageHeaderView(for: indexPath, in: messagesCollectionView) - case UICollectionElementKindSectionFooter: + case UICollectionView.elementKindSectionFooter: return displayDelegate.messageFooterView(for: indexPath, in: messagesCollectionView) default: fatalError(MessageKitError.unrecognizedSectionKind) @@ -282,11 +301,11 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { private func addObservers() { NotificationCenter.default.addObserver( - self, selector: #selector(clearMemoryCache), name: .UIApplicationDidReceiveMemoryWarning, object: nil) + self, selector: #selector(clearMemoryCache), name: UIApplication.didReceiveMemoryWarningNotification, object: nil) } private func removeObservers() { - NotificationCenter.default.removeObserver(self, name: .UIApplicationDidReceiveMemoryWarning, object: nil) + NotificationCenter.default.removeObserver(self, name: UIApplication.didReceiveMemoryWarningNotification, object: nil) } @objc private func clearMemoryCache() { diff --git a/Sources/Layout/MessageSizeCalculator.swift b/Sources/Layout/MessageSizeCalculator.swift index 7e563629b..afebb0881 100644 --- a/Sources/Layout/MessageSizeCalculator.swift +++ b/Sources/Layout/MessageSizeCalculator.swift @@ -47,8 +47,14 @@ open class MessageSizeCalculator: CellSizeCalculator { public var incomingMessageTopLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42)) public var outgoingMessageTopLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42)) - public var incomingMessageBottomLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42)) - public var outgoingMessageBottomLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42)) + public var incomingMessageBottomLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 8)) + public var outgoingMessageBottomLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 8)) + + public var incomingAccessoryViewSize = CGSize.zero + public var outgoingAccessoryViewSize = CGSize.zero + + public var incomingAccessoryViewPadding = HorizontalEdgeInsets.zero + public var outgoingAccessoryViewPadding = HorizontalEdgeInsets.zero open override func configure(attributes: UICollectionViewLayoutAttributes) { guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return } @@ -68,6 +74,9 @@ open class MessageSizeCalculator: CellSizeCalculator { attributes.messageBottomLabelAlignment = messageBottomLabelAlignment(for: message) attributes.messageBottomLabelSize = messageBottomLabelSize(for: message, at: indexPath) + + attributes.accessoryViewSize = accessoryViewSize(for: message) + attributes.accessoryViewPadding = accessoryViewPadding(for: message) } open override func sizeForItem(at indexPath: IndexPath) -> CGSize { @@ -86,41 +95,44 @@ open class MessageSizeCalculator: CellSizeCalculator { let messageVerticalPadding = messageContainerPadding(for: message).vertical let avatarHeight = avatarSize(for: message).height let avatarVerticalPosition = avatarPosition(for: message).vertical + let accessoryViewHeight = accessoryViewSize(for: message).height switch avatarVerticalPosition { case .messageCenter: let totalLabelHeight: CGFloat = cellTopLabelHeight + messageTopLabelHeight + messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight - return max(avatarHeight, totalLabelHeight) + let cellHeight = max(avatarHeight, totalLabelHeight) + return max(cellHeight, accessoryViewHeight) case .messageBottom: var cellHeight: CGFloat = 0 cellHeight += messageBottomLabelHeight let labelsHeight = messageContainerHeight + messageVerticalPadding + cellTopLabelHeight + messageTopLabelHeight cellHeight += max(labelsHeight, avatarHeight) - return cellHeight + return max(cellHeight, accessoryViewHeight) case .messageTop: var cellHeight: CGFloat = 0 cellHeight += cellTopLabelHeight cellHeight += messageTopLabelHeight let labelsHeight = messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight cellHeight += max(labelsHeight, avatarHeight) - return cellHeight + return max(cellHeight, accessoryViewHeight) case .messageLabelTop: var cellHeight: CGFloat = 0 cellHeight += cellTopLabelHeight let messageLabelsHeight = messageContainerHeight + messageBottomLabelHeight + messageVerticalPadding + messageTopLabelHeight cellHeight += max(messageLabelsHeight, avatarHeight) - return cellHeight + return max(cellHeight, accessoryViewHeight) case .cellTop, .cellBottom: let totalLabelHeight: CGFloat = cellTopLabelHeight + messageTopLabelHeight + messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight - return max(avatarHeight, totalLabelHeight) + let cellHeight = max(avatarHeight, totalLabelHeight) + return max(cellHeight, accessoryViewHeight) } } // MARK: - Avatar - public func avatarPosition(for message: MessageType) -> AvatarPosition { + open func avatarPosition(for message: MessageType) -> AvatarPosition { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) var position = isFromCurrentSender ? outgoingAvatarPosition : incomingAvatarPosition @@ -134,7 +146,7 @@ open class MessageSizeCalculator: CellSizeCalculator { return position } - public func avatarSize(for message: MessageType) -> CGSize { + open func avatarSize(for message: MessageType) -> CGSize { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) return isFromCurrentSender ? outgoingAvatarSize : incomingAvatarSize @@ -142,14 +154,14 @@ open class MessageSizeCalculator: CellSizeCalculator { // MARK: - Top cell Label - public func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + open func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { let layoutDelegate = messagesLayout.messagesLayoutDelegate let collectionView = messagesLayout.messagesCollectionView let height = layoutDelegate.cellTopLabelHeight(for: message, at: indexPath, in: collectionView) return CGSize(width: messagesLayout.itemWidth, height: height) } - public func cellTopLabelAlignment(for message: MessageType) -> LabelAlignment { + open func cellTopLabelAlignment(for message: MessageType) -> LabelAlignment { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) return isFromCurrentSender ? outgoingCellTopLabelAlignment : incomingCellTopLabelAlignment @@ -157,14 +169,14 @@ open class MessageSizeCalculator: CellSizeCalculator { // MARK: - Top message Label - public func messageTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + open func messageTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { let layoutDelegate = messagesLayout.messagesLayoutDelegate let collectionView = messagesLayout.messagesCollectionView let height = layoutDelegate.messageTopLabelHeight(for: message, at: indexPath, in: collectionView) return CGSize(width: messagesLayout.itemWidth, height: height) } - public func messageTopLabelAlignment(for message: MessageType) -> LabelAlignment { + open func messageTopLabelAlignment(for message: MessageType) -> LabelAlignment { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) return isFromCurrentSender ? outgoingMessageTopLabelAlignment : incomingMessageTopLabelAlignment @@ -172,22 +184,36 @@ open class MessageSizeCalculator: CellSizeCalculator { // MARK: - Bottom Label - public func messageBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + open func messageBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { let layoutDelegate = messagesLayout.messagesLayoutDelegate let collectionView = messagesLayout.messagesCollectionView let height = layoutDelegate.messageBottomLabelHeight(for: message, at: indexPath, in: collectionView) return CGSize(width: messagesLayout.itemWidth, height: height) } - public func messageBottomLabelAlignment(for message: MessageType) -> LabelAlignment { + open func messageBottomLabelAlignment(for message: MessageType) -> LabelAlignment { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) return isFromCurrentSender ? outgoingMessageBottomLabelAlignment : incomingMessageBottomLabelAlignment } + // MARK: - Accessory View + + public func accessoryViewSize(for message: MessageType) -> CGSize { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingAccessoryViewSize : incomingAccessoryViewSize + } + + public func accessoryViewPadding(for message: MessageType) -> HorizontalEdgeInsets { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingAccessoryViewPadding : incomingAccessoryViewPadding + } + // MARK: - MessageContainer - public func messageContainerPadding(for message: MessageType) -> UIEdgeInsets { + open func messageContainerPadding(for message: MessageType) -> UIEdgeInsets { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) return isFromCurrentSender ? outgoingMessagePadding : incomingMessagePadding @@ -201,7 +227,9 @@ open class MessageSizeCalculator: CellSizeCalculator { open func messageContainerMaxWidth(for message: MessageType) -> CGFloat { let avatarWidth = avatarSize(for: message).width let messagePadding = messageContainerPadding(for: message) - return messagesLayout.itemWidth - avatarWidth - messagePadding.horizontal + let accessoryWidth = accessoryViewSize(for: message).width + let accessoryPadding = accessoryViewPadding(for: message) + return messagesLayout.itemWidth - avatarWidth - messagePadding.horizontal - accessoryWidth - accessoryPadding.horizontal } // MARK: - Helpers diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 74ad77c96..cd5fe5af8 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -66,19 +66,31 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { public override init() { super.init() - - sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) - - NotificationCenter.default.addObserver(self, selector: #selector(MessagesCollectionViewFlowLayout.handleOrientationChange(_:)), name: .UIDeviceOrientationDidChange, object: nil) + + setupView() + setupObserver() } required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: aDecoder) + + setupView() + setupObserver() } deinit { NotificationCenter.default.removeObserver(self) } + + // MARK: - Methods + + private func setupView() { + sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) + } + + private func setupObserver() { + NotificationCenter.default.addObserver(self, selector: #selector(MessagesCollectionViewFlowLayout.handleOrientationChange(_:)), name: UIDevice.orientationDidChangeNotification, object: nil) + } // MARK: - Attributes @@ -135,6 +147,8 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { lazy open var videoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self) lazy open var locationMessageSizeCalculator = LocationMessageSizeCalculator(layout: self) + /// - Note: + /// If you override this method, remember to call MessageLayoutDelegate's customCellSizeCalculator(for:at:in:) method for MessageKind.custom messages, if necessary open func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator { let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) switch message.kind { @@ -151,7 +165,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { case .location: return locationMessageSizeCalculator case .custom: - fatalError("Must return a CellSizeCalculator for MessageKind.custom(Any?)") + return messagesLayoutDelegate.customCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) } } @@ -219,7 +233,27 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { public func setMessageOutgoingMessageBottomLabelAlignment(_ newAlignment: LabelAlignment) { messageSizeCalculators().forEach { $0.outgoingMessageBottomLabelAlignment = newAlignment } } - + + /// Set `incomingAccessoryViewSize` of all `MessageSizeCalculator`s + public func setMessageIncomingAccessoryViewSize(_ newSize: CGSize) { + messageSizeCalculators().forEach { $0.incomingAccessoryViewSize = newSize } + } + + /// Set `outgoingAvatarSize` of all `MessageSizeCalculator`s + public func setMessageOutgoingAccessoryViewSize(_ newSize: CGSize) { + messageSizeCalculators().forEach { $0.outgoingAccessoryViewSize = newSize } + } + + /// Set `incomingAccessoryViewSize` of all `MessageSizeCalculator`s + public func setMessageIncomingAccessoryViewPadding(_ newPadding: HorizontalEdgeInsets) { + messageSizeCalculators().forEach { $0.incomingAccessoryViewPadding = newPadding } + } + + /// Set `outgoingAvatarSize` of all `MessageSizeCalculator`s + public func setMessageOutgoingAccessoryViewPadding(_ newPadding: HorizontalEdgeInsets) { + messageSizeCalculators().forEach { $0.outgoingAccessoryViewPadding = newPadding } + } + /// Get all `MessageSizeCalculator`s open func messageSizeCalculators() -> [MessageSizeCalculator] { return [textMessageSizeCalculator, attributedTextMessageSizeCalculator, emojiMessageSizeCalculator, photoMessageSizeCalculator, videoMessageSizeCalculator, locationMessageSizeCalculator] diff --git a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift index 678645a2b..e055ac570 100644 --- a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift +++ b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift @@ -46,6 +46,9 @@ open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttribu public var messageBottomLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) public var messageBottomLabelSize: CGSize = .zero + public var accessoryViewSize: CGSize = .zero + public var accessoryViewPadding: HorizontalEdgeInsets = .zero + // MARK: - Methods open override func copy(with zone: NSZone? = nil) -> Any { @@ -63,6 +66,8 @@ open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttribu copy.messageTopLabelSize = messageTopLabelSize copy.messageBottomLabelAlignment = messageBottomLabelAlignment copy.messageBottomLabelSize = messageBottomLabelSize + copy.accessoryViewSize = accessoryViewSize + copy.accessoryViewPadding = accessoryViewPadding return copy // swiftlint:enable force_cast } @@ -82,6 +87,8 @@ open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttribu && attributes.messageTopLabelSize == messageTopLabelSize && attributes.messageBottomLabelAlignment == messageBottomLabelAlignment && attributes.messageBottomLabelSize == messageBottomLabelSize + && attributes.accessoryViewSize == accessoryViewSize + && attributes.accessoryViewPadding == accessoryViewPadding } else { return false } diff --git a/Sources/Layout/TextMessageSizeCalculator.swift b/Sources/Layout/TextMessageSizeCalculator.swift index 9f2b5cc16..95ff839ab 100644 --- a/Sources/Layout/TextMessageSizeCalculator.swift +++ b/Sources/Layout/TextMessageSizeCalculator.swift @@ -26,8 +26,8 @@ import Foundation open class TextMessageSizeCalculator: MessageSizeCalculator { - public var incomingMessageLabelInsets = UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14) - public var outgoingMessageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18) + public var incomingMessageLabelInsets = UIEdgeInsets(top: 10, left: 8, bottom: 10, right: 14) + public var outgoingMessageLabelInsets = UIEdgeInsets(top: 10, left: 14, bottom: 10, right: 8) public var messageLabelFont = UIFont.preferredFont(forTextStyle: .body) diff --git a/Sources/Models/HorizontalEdgeInsets.swift b/Sources/Models/HorizontalEdgeInsets.swift new file mode 100644 index 000000000..9ec2f0f55 --- /dev/null +++ b/Sources/Models/HorizontalEdgeInsets.swift @@ -0,0 +1,55 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + 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 + +/// A varient of `UIEdgeInsets` that only has horizontal inset properties +public struct HorizontalEdgeInsets { + + public var left: CGFloat + public var right: CGFloat + + public init(left: CGFloat, right: CGFloat) { + self.left = left + self.right = right + } + + public static var zero: HorizontalEdgeInsets { + return HorizontalEdgeInsets(left: 0, right: 0) + } +} + +extension HorizontalEdgeInsets: Equatable { + + public static func == (lhs: HorizontalEdgeInsets, rhs: HorizontalEdgeInsets) -> Bool { + return lhs.left == rhs.left && lhs.right == rhs.right + } +} + +extension HorizontalEdgeInsets { + + internal var horizontal: CGFloat { + return left + right + } +} diff --git a/Sources/Models/MessageKind.swift b/Sources/Models/MessageKind.swift index fc130f524..a29d76d5c 100644 --- a/Sources/Models/MessageKind.swift +++ b/Sources/Models/MessageKind.swift @@ -52,9 +52,9 @@ public enum MessageKind { case emoji(String) /// A custom message. - /// - Note: Using this case requires that you override the following methods and handle this case: - /// - `collectionView(_:cellForItemAt indexPath: IndexPath) -> UICollectionViewCell` - /// - `cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator` + /// - Note: Using this case requires that you implement the following methods and handle this case: + /// - MessagesDataSource: customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell + /// - MessagesLayoutDelegate: customCellSizeCalculator(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator case custom(Any?) // MARK: - Not supported yet diff --git a/Sources/Models/MessageKitDateFormatter.swift b/Sources/Models/MessageKitDateFormatter.swift index f8d2ab60b..c0d02bc27 100644 --- a/Sources/Models/MessageKitDateFormatter.swift +++ b/Sources/Models/MessageKitDateFormatter.swift @@ -43,7 +43,7 @@ open class MessageKitDateFormatter { return formatter.string(from: date) } - public func attributedString(from date: Date, with attributes: [NSAttributedStringKey: Any]) -> NSAttributedString { + public func attributedString(from date: Date, with attributes: [NSAttributedString.Key: Any]) -> NSAttributedString { let dateString = string(from: date) return NSAttributedString(string: dateString, attributes: attributes) } diff --git a/Sources/Models/MessageStyle.swift b/Sources/Models/MessageStyle.swift index 22e9c6847..f8abd50e9 100644 --- a/Sources/Models/MessageStyle.swift +++ b/Sources/Models/MessageStyle.swift @@ -35,7 +35,7 @@ public enum MessageStyle { case topRight case bottomRight - internal var imageOrientation: UIImageOrientation { + internal var imageOrientation: UIImage.Orientation { switch self { case .bottomRight: return .up case .bottomLeft: return .upMirrored diff --git a/Sources/Models/NSConstraintLayoutSet.swift b/Sources/Models/NSConstraintLayoutSet.swift index ba098a3a6..cd063dfdc 100644 --- a/Sources/Models/NSConstraintLayoutSet.swift +++ b/Sources/Models/NSConstraintLayoutSet.swift @@ -36,9 +36,9 @@ internal class NSLayoutConstraintSet { internal var height: NSLayoutConstraint? internal init(top: NSLayoutConstraint? = nil, bottom: NSLayoutConstraint? = nil, - left: NSLayoutConstraint? = nil, right: NSLayoutConstraint? = nil, - centerX: NSLayoutConstraint? = nil, centerY: NSLayoutConstraint? = nil, - width: NSLayoutConstraint? = nil, height: NSLayoutConstraint? = nil) { + left: NSLayoutConstraint? = nil, right: NSLayoutConstraint? = nil, + centerX: NSLayoutConstraint? = nil, centerY: NSLayoutConstraint? = nil, + width: NSLayoutConstraint? = nil, height: NSLayoutConstraint? = nil) { self.top = top self.bottom = bottom self.left = left diff --git a/Sources/Protocols/MessageCellDelegate.swift b/Sources/Protocols/MessageCellDelegate.swift index 1300a2239..4fbe9da92 100644 --- a/Sources/Protocols/MessageCellDelegate.swift +++ b/Sources/Protocols/MessageCellDelegate.swift @@ -77,6 +77,16 @@ public protocol MessageCellDelegate: MessageLabelDelegate { /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` /// method `messageForItem(at:indexPath:messagesCollectionView)`. func didTapMessageBottomLabel(in cell: MessageCollectionViewCell) + + /// Triggered when a tap occurs in the accessoryView. + /// + /// - Parameters: + /// - cell: The cell where the tap occurred. + /// + /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s + /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` + /// method `messageForItem(at:indexPath:messagesCollectionView)`. + func didTapAccessoryView(in cell: MessageCollectionViewCell) } @@ -91,4 +101,6 @@ public extension MessageCellDelegate { func didTapMessageTopLabel(in cell: MessageCollectionViewCell) {} func didTapMessageBottomLabel(in cell: MessageCollectionViewCell) {} + + func didTapAccessoryView(in cell: MessageCollectionViewCell) {} } diff --git a/Sources/Protocols/MessageInputBarDelegate.swift b/Sources/Protocols/MessageInputBarDelegate.swift deleted file mode 100644 index 39c5b180f..000000000 --- a/Sources/Protocols/MessageInputBarDelegate.swift +++ /dev/null @@ -1,62 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - 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 - -/// A protocol that can receive different event notifications from the MessageInputBar. -public protocol MessageInputBarDelegate: AnyObject { - - /// Called when the default send button has been selected. - /// - /// - Parameters: - /// - inputBar: The `MessageInputBar`. - /// - text: The current text in the `InputTextView` of the `MessageInputBar`. - func messageInputBar(_ inputBar: MessageInputBar, didPressSendButtonWith text: String) - - /// Called when the instrinsicContentSize of the MessageInputBar has changed. - /// Can be used for adjusting content insets on other views to make sure - /// the MessageInputBar does not cover up any other view. - /// - /// - Parameters: - /// - inputBar: The `MessageInputBar`. - /// - size: The new instrinsic content size. - func messageInputBar(_ inputBar: MessageInputBar, didChangeIntrinsicContentTo size: CGSize) - - /// Called when the `MessageInputBar`'s `InputTextView`'s text has changed. - /// Useful for adding your own logic without the need of assigning a delegate or notification. - /// - /// - Parameters: - /// - inputBar: The MessageInputBar - /// - text: The current text in the MessageInputBar's InputTextView - func messageInputBar(_ inputBar: MessageInputBar, textViewTextDidChangeTo text: String) -} - -public extension MessageInputBarDelegate { - - func messageInputBar(_ inputBar: MessageInputBar, didPressSendButtonWith text: String) {} - - func messageInputBar(_ inputBar: MessageInputBar, didChangeIntrinsicContentTo size: CGSize) {} - - func messageInputBar(_ inputBar: MessageInputBar, textViewTextDidChangeTo text: String) {} -} diff --git a/Sources/Protocols/MessagesDataSource.swift b/Sources/Protocols/MessagesDataSource.swift index e9ee46f77..3caba08bf 100644 --- a/Sources/Protocols/MessagesDataSource.swift +++ b/Sources/Protocols/MessagesDataSource.swift @@ -92,7 +92,17 @@ public protocol MessagesDataSource: AnyObject { /// /// The default value returned by this method is `nil`. func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? - + + /// Custom collectionView cell for message with `custom` message type. + /// + /// - Parameters: + /// - message: The `custom` message type + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// This method will call fatalError() on default. You must override this method if you are using MessageType.custom messages. + func customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell } public extension MessagesDataSource { @@ -116,5 +126,8 @@ public extension MessagesDataSource { func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { return nil } - + + func customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell { + fatalError(MessageKitError.customDataUnresolvedCell) + } } diff --git a/Sources/Protocols/MessagesDisplayDelegate.swift b/Sources/Protocols/MessagesDisplayDelegate.swift index 8dc983880..2af2eac5a 100644 --- a/Sources/Protocols/MessagesDisplayDelegate.swift +++ b/Sources/Protocols/MessagesDisplayDelegate.swift @@ -84,6 +84,18 @@ public protocol MessagesDisplayDelegate: AnyObject { /// The default image configured by this method is `?`. func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) + /// Used to configure the `AccessoryView` in a `MessageContentCell` class. + /// + /// - Parameters: + /// - accessoryView: The `AccessoryView` of the cell. + /// - message: The `MessageType` that will be displayed by this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default image configured by this method is `?`. + func configureAccessoryView(_ accessoryView: UIView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) + // MARK: - Text Messages /// Specifies the color of the text for a `TextMessageCell`. @@ -118,7 +130,7 @@ public protocol MessagesDisplayDelegate: AnyObject { /// - detector: The `DetectorType` for the applied attributes. /// - message: A `MessageType` with a `MessageKind` case of `.text` or `.attributedText` to which the detectors will apply. /// - indexPath: The `IndexPath` of the cell. - func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedStringKey: Any] + func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any] // MARK: - Location Messages @@ -194,6 +206,8 @@ public extension MessagesDisplayDelegate { avatarView.initials = "?" } + func configureAccessoryView(_ accessoryView: UIView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {} + // MARK: - Text Messages Defaults func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { @@ -207,7 +221,7 @@ public extension MessagesDisplayDelegate { return [] } - func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedStringKey: Any] { + func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any] { return MessageLabel.defaultAttributes } diff --git a/Sources/Protocols/MessagesLayoutDelegate.swift b/Sources/Protocols/MessagesLayoutDelegate.swift index 6f825fbf8..4dd75743f 100644 --- a/Sources/Protocols/MessagesLayoutDelegate.swift +++ b/Sources/Protocols/MessagesLayoutDelegate.swift @@ -80,7 +80,17 @@ public protocol MessagesLayoutDelegate: AnyObject { /// - Note: /// The default value returned by this method is zero. func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat - + + /// Custom cell size calculator for messages with MessageType.custom. + /// + /// - Parameters: + /// - message: The custom message + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default implementation will throw fatalError(). You must override this method if you are using messages with MessageType.custom. + func customCellSizeCalculator(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator } public extension MessagesLayoutDelegate { @@ -104,4 +114,8 @@ public extension MessagesLayoutDelegate { func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { return 0 } + + func customCellSizeCalculator(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator { + fatalError("Must return a CellSizeCalculator for MessageKind.custom(Any?)") + } } diff --git a/Sources/Views/AvatarView.swift b/Sources/Views/AvatarView.swift index f88af3558..cf3c14561 100644 --- a/Sources/Views/AvatarView.swift +++ b/Sources/Views/AvatarView.swift @@ -74,9 +74,15 @@ open class AvatarView: UIImageView { super.init(frame: frame) prepareView() } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + prepareView() + } convenience public init() { self.init(frame: .zero) + prepareView() } private func setImageFrom(initials: String?) { @@ -105,7 +111,7 @@ open class AvatarView: UIImageView { let textStyle = NSMutableParagraphStyle() textStyle.alignment = .center - let textFontAttributes: [NSAttributedStringKey: Any] = [NSAttributedStringKey.font: font, NSAttributedStringKey.foregroundColor: placeholderTextColor, NSAttributedStringKey.paragraphStyle: textStyle] + let textFontAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: placeholderTextColor, NSAttributedString.Key.paragraphStyle: textStyle] let textTextHeight: CGFloat = initials.boundingRect(with: CGSize(width: textRect.width, height: CGFloat.infinity), options: .usesLineFragmentOrigin, attributes: textFontAttributes, context: nil).height context.saveGState() @@ -152,10 +158,6 @@ open class AvatarView: UIImageView { return CGRect(startX+2, startY, w-4, h) } - required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - // MARK: - Internal methods internal func prepareView() { diff --git a/Sources/Views/Cells/LocationMessageCell.swift b/Sources/Views/Cells/LocationMessageCell.swift index 553240ed9..ed84ed752 100644 --- a/Sources/Views/Cells/LocationMessageCell.swift +++ b/Sources/Views/Cells/LocationMessageCell.swift @@ -29,7 +29,7 @@ import MapKit open class LocationMessageCell: MessageContentCell { /// The activity indicator to be displayed while the map image is loading. - open var activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) + open var activityIndicator = UIActivityIndicatorView(style: .gray) /// The image view holding the map image. open var imageView = UIImageView() @@ -68,7 +68,7 @@ open class LocationMessageCell: MessageContentCell { activityIndicator.startAnimating() - let snapshotOptions = MKMapSnapshotOptions() + let snapshotOptions = MKMapSnapshotter.Options() snapshotOptions.region = MKCoordinateRegion(center: locationItem.location.coordinate, span: options.span) snapshotOptions.showsBuildings = options.showsBuildings snapshotOptions.showsPointsOfInterest = options.showsPointsOfInterest diff --git a/Sources/Views/Cells/MessageCollectionViewCell.swift b/Sources/Views/Cells/MessageCollectionViewCell.swift index 8d99ab903..08114cb59 100644 --- a/Sources/Views/Cells/MessageCollectionViewCell.swift +++ b/Sources/Views/Cells/MessageCollectionViewCell.swift @@ -34,7 +34,7 @@ open class MessageCollectionViewCell: UICollectionViewCell { } public required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: aDecoder) } } diff --git a/Sources/Views/Cells/MessageContentCell.swift b/Sources/Views/Cells/MessageContentCell.swift index c98609e3b..3a194a68f 100644 --- a/Sources/Views/Cells/MessageContentCell.swift +++ b/Sources/Views/Cells/MessageContentCell.swift @@ -60,6 +60,9 @@ open class MessageContentCell: MessageCollectionViewCell { return label }() + // Should only add customized subviews - don't change accessoryView itself. + open var accessoryView: UIView = UIView() + /// The `MessageCellDelegate` for the cell. open weak var delegate: MessageCellDelegate? @@ -70,10 +73,13 @@ open class MessageContentCell: MessageCollectionViewCell { } required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: aDecoder) + contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + setupSubviews() } open func setupSubviews() { + contentView.addSubview(accessoryView) contentView.addSubview(cellTopLabel) contentView.addSubview(messageTopLabel) contentView.addSubview(messageBottomLabel) @@ -99,6 +105,7 @@ open class MessageContentCell: MessageCollectionViewCell { layoutCellTopLabel(with: attributes) layoutMessageTopLabel(with: attributes) layoutAvatarView(with: attributes) + layoutAccessoryView(with: attributes) } /// Used to configure the cell. @@ -122,6 +129,8 @@ open class MessageContentCell: MessageCollectionViewCell { displayDelegate.configureAvatarView(avatarView, for: message, at: indexPath, in: messagesCollectionView) + displayDelegate.configureAccessoryView(accessoryView, for: message, at: indexPath, in: messagesCollectionView) + messageContainerView.backgroundColor = messageColor messageContainerView.style = messageStyle @@ -149,6 +158,8 @@ open class MessageContentCell: MessageCollectionViewCell { delegate?.didTapMessageTopLabel(in: self) case messageBottomLabel.frame.contains(touchLocation): delegate?.didTapMessageBottomLabel(in: self) + case accessoryView.frame.contains(touchLocation): + delegate?.didTapAccessoryView(in: self) default: break } @@ -216,7 +227,12 @@ open class MessageContentCell: MessageCollectionViewCell { fallthrough } default: - origin.y = attributes.cellTopLabelSize.height + attributes.messageTopLabelSize.height + attributes.messageContainerPadding.top + if attributes.accessoryViewSize.height > attributes.messageContainerSize.height { + let messageHeight = attributes.messageContainerSize.height + attributes.messageContainerPadding.vertical + origin.y = (attributes.size.height / 2) - (messageHeight / 2) + } else { + origin.y = attributes.cellTopLabelSize.height + attributes.messageTopLabelSize.height + attributes.messageContainerPadding.top + } } switch attributes.avatarPosition.horizontal { @@ -260,5 +276,26 @@ open class MessageContentCell: MessageCollectionViewCell { messageBottomLabel.frame = CGRect(origin: origin, size: attributes.messageBottomLabelSize) } - + + /// Positions the cell's accessory view. + /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. + open func layoutAccessoryView(with attributes: MessagesCollectionViewLayoutAttributes) { + + // Accessory view aligned to the middle of the messageContainerView + let y = messageContainerView.frame.midY - (attributes.accessoryViewSize.height / 2) + + var origin = CGPoint(x: 0, y: y) + + // Accessory view is always on the opposite side of avatar + switch attributes.avatarPosition.horizontal { + case .cellLeading: + origin.x = messageContainerView.frame.maxX + attributes.accessoryViewPadding.left + case .cellTrailing: + origin.x = messageContainerView.frame.minX - attributes.accessoryViewPadding.right - attributes.accessoryViewSize.width + case .natural: + fatalError(MessageKitError.avatarPositionUnresolved) + } + + accessoryView.frame = CGRect(origin: origin, size: attributes.accessoryViewSize) + } } diff --git a/Sources/Views/Headers & Footers/MessageReusableView.swift b/Sources/Views/Headers & Footers/MessageReusableView.swift index f8c37c367..56270dd3c 100644 --- a/Sources/Views/Headers & Footers/MessageReusableView.swift +++ b/Sources/Views/Headers & Footers/MessageReusableView.swift @@ -33,7 +33,7 @@ open class MessageReusableView: UICollectionReusableView { } public required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: aDecoder) } } diff --git a/Sources/Views/InputBarItem.swift b/Sources/Views/InputBarItem.swift deleted file mode 100644 index 2643b849d..000000000 --- a/Sources/Views/InputBarItem.swift +++ /dev/null @@ -1,326 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import UIKit - -/** - A InputItem that inherits from UIButton - - ## Important Notes ## - 1. Intended to be used in an `InputStackView` - */ -open class InputBarButtonItem: UIButton { - - /// The spacing properties of the InputBarButtonItem - /// - /// - fixed: The spacing is fixed - /// - flexible: The spacing is flexible - /// - none: There is no spacing - public enum Spacing { - case fixed(CGFloat) - case flexible - case none - } - - public typealias InputBarButtonItemAction = ((InputBarButtonItem) -> Void) - - // MARK: - Properties - - /// A weak reference to the MessageInputBar that the InputBarButtonItem used in - open weak var messageInputBar: MessageInputBar? - - /// The spacing property of the InputBarButtonItem that determines the contentHuggingPriority and any - /// additional space to the intrinsicContentSize - open var spacing: Spacing = .none { - didSet { - switch spacing { - case .flexible: - setContentHuggingPriority(UILayoutPriority(rawValue: 1), for: .horizontal) - case .fixed: - setContentHuggingPriority(UILayoutPriority(rawValue: 1000), for: .horizontal) - case .none: - setContentHuggingPriority(UILayoutPriority(rawValue: 500), for: .horizontal) - } - } - } - - /// When not nil this size overrides the intrinsicContentSize - private var size: CGSize? = CGSize(width: 20, height: 20) { - didSet { - invalidateIntrinsicContentSize() - } - } - - open override var intrinsicContentSize: CGSize { - var contentSize = size ?? super.intrinsicContentSize - switch spacing { - case .fixed(let width): - contentSize.width += width - case .flexible, .none: - break - } - return contentSize - } - - /// A reference to the stack view position that the InputBarButtonItem is held in - open var parentStackViewPosition: InputStackView.Position? - - /// The title for the UIControlState.normal - open var title: String? { - get { - return title(for: .normal) - } - set { - setTitle(newValue, for: .normal) - } - } - - /// The image for the UIControlState.normal - open var image: UIImage? { - get { - return image(for: .normal) - } - set { - setImage(newValue, for: .normal) - } - } - - /// Calls the onSelectedAction or onDeselectedAction when set - open override var isHighlighted: Bool { - didSet { - if isHighlighted { - onSelectedAction?(self) - } else { - onDeselectedAction?(self) - } - } - } - - /// Calls the onEnabledAction or onDisabledAction when set - open override var isEnabled: Bool { - didSet { - if isEnabled { - onEnabledAction?(self) - } else { - onDisabledAction?(self) - } - } - } - - // MARK: - Reactive Hooks - - private var onTouchUpInsideAction: InputBarButtonItemAction? - private var onKeyboardEditingBeginsAction: InputBarButtonItemAction? - private var onKeyboardEditingEndsAction: InputBarButtonItemAction? - private var onTextViewDidChangeAction: ((InputBarButtonItem, InputTextView) -> Void)? - private var onSelectedAction: InputBarButtonItemAction? - private var onDeselectedAction: InputBarButtonItemAction? - private var onEnabledAction: InputBarButtonItemAction? - private var onDisabledAction: InputBarButtonItemAction? - - // MARK: - Initialization - - public convenience init() { - self.init(frame: .zero) - } - - public override init(frame: CGRect) { - super.init(frame: frame) - setup() - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setup() - } - - // MARK: - Setup - - /// Sets up the default properties - open func setup() { - contentVerticalAlignment = .center - contentHorizontalAlignment = .center - imageView?.contentMode = .scaleAspectFit - setContentHuggingPriority(UILayoutPriority(rawValue: 500), for: .horizontal) - setContentHuggingPriority(UILayoutPriority(rawValue: 500), for: .vertical) - setTitleColor(UIColor(red: 0, green: 122/255, blue: 1, alpha: 1), for: .normal) - setTitleColor(UIColor(red: 0, green: 122/255, blue: 1, alpha: 0.3), for: .highlighted) - setTitleColor(.lightGray, for: .disabled) - adjustsImageWhenHighlighted = false - addTarget(self, action: #selector(InputBarButtonItem.touchUpInsideAction), for: .touchUpInside) - } - - // MARK: - Size Adjustment - - /// Sets the size of the InputBarButtonItem which overrides the intrinsicContentSize. When set to nil - /// the default intrinsicContentSize is used. The new size will be laid out in the UIStackView that - /// the InputBarButtonItem is held in - /// - /// - Parameters: - /// - newValue: The new size - /// - animated: If the layout should be animated - open func setSize(_ newValue: CGSize?, animated: Bool) { - size = newValue - if animated, let position = parentStackViewPosition { - messageInputBar?.performLayout(animated) { [weak self] in - self?.messageInputBar?.layoutStackViews([position]) - } - } - } - - // MARK: - Hook Setup Methods - - /// Used to setup your own initial properties - /// - /// - Parameter item: A reference to Self - /// - Returns: Self - @discardableResult - open func configure(_ item: InputBarButtonItemAction) -> Self { - item(self) - return self - } - - /// Sets the onKeyboardEditingBeginsAction - /// - /// - Parameter action: The new onKeyboardEditingBeginsAction - /// - Returns: Self - @discardableResult - open func onKeyboardEditingBegins(_ action: @escaping InputBarButtonItemAction) -> Self { - onKeyboardEditingBeginsAction = action - return self - } - - /// Sets the onKeyboardEditingEndsAction - /// - /// - Parameter action: The new onKeyboardEditingEndsAction - /// - Returns: Self - @discardableResult - open func onKeyboardEditingEnds(_ action: @escaping InputBarButtonItemAction) -> Self { - onKeyboardEditingEndsAction = action - return self - } - - /// Sets the onTextViewDidChangeAction - /// - /// - Parameter action: The new onTextViewDidChangeAction - /// - Returns: Self - @discardableResult - open func onTextViewDidChange(_ action: @escaping (_ item: InputBarButtonItem, _ textView: InputTextView) -> Void) -> Self { - onTextViewDidChangeAction = action - return self - } - - /// Sets the onTouchUpInsideAction - /// - /// - Parameter action: The new onTouchUpInsideAction - /// - Returns: Self - @discardableResult - open func onTouchUpInside(_ action: @escaping InputBarButtonItemAction) -> Self { - onTouchUpInsideAction = action - return self - } - - /// Sets the onSelectedAction - /// - /// - Parameter action: The new onSelectedAction - /// - Returns: Self - @discardableResult - open func onSelected(_ action: @escaping InputBarButtonItemAction) -> Self { - onSelectedAction = action - return self - } - - /// Sets the onDeselectedAction - /// - /// - Parameter action: The new onDeselectedAction - /// - Returns: Self - @discardableResult - open func onDeselected(_ action: @escaping InputBarButtonItemAction) -> Self { - onDeselectedAction = action - return self - } - - /// Sets the onEnabledAction - /// - /// - Parameter action: The new onEnabledAction - /// - Returns: Self - @discardableResult - open func onEnabled(_ action: @escaping InputBarButtonItemAction) -> Self { - onEnabledAction = action - return self - } - - /// Sets the onDisabledAction - /// - /// - Parameter action: The new onDisabledAction - /// - Returns: Self - @discardableResult - open func onDisabled(_ action: @escaping InputBarButtonItemAction) -> Self { - onDisabledAction = action - return self - } - - // MARK: - InputItem Protocol - - /// Executes the onTextViewDidChangeAction with the given textView - /// - /// - Parameter textView: A reference to the InputTextView - open func textViewDidChangeAction(with textView: InputTextView) { - onTextViewDidChangeAction?(self, textView) - } - - /// Executes the onKeyboardEditingEndsAction - open func keyboardEditingEndsAction() { - onKeyboardEditingEndsAction?(self) - } - - /// Executes the onKeyboardEditingBeginsAction - open func keyboardEditingBeginsAction() { - onKeyboardEditingBeginsAction?(self) - } - - /// Executes the onTouchUpInsideAction - @objc - open func touchUpInsideAction() { - onTouchUpInsideAction?(self) - } - - // MARK: - Static Spacers - - /// An InputBarButtonItem that's spacing property is set to be .flexible - open static var flexibleSpace: InputBarButtonItem { - let item = InputBarButtonItem() - item.setSize(.zero, animated: false) - item.spacing = .flexible - return item - } - - /// An InputBarButtonItem that's spacing property is set to be .fixed with the width arguement - open class func fixedSpace(_ width: CGFloat) -> InputBarButtonItem { - let item = InputBarButtonItem() - item.setSize(.zero, animated: false) - item.spacing = .fixed(width) - return item - } -} diff --git a/Sources/Views/InputStackView.swift b/Sources/Views/InputStackView.swift deleted file mode 100644 index eb7509061..000000000 --- a/Sources/Views/InputStackView.swift +++ /dev/null @@ -1,72 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import UIKit - -/** - A UIStackView that's intended for holding `InputBarButtonItem`s - - ## Important Notes ## - 1. Default alignment is .fill - 2. Default distribution is .fill - 3. The distribution property needs to be based on its arranged subviews intrinsicContentSize so it is not recommended to change it - */ -open class InputStackView: UIStackView { - - /// The stack view position in the MessageInputBar - /// - /// - left: Left Stack View - /// - right: Bottom Stack View - /// - bottom: Left Stack View - /// - top: Top Stack View - public enum Position { - case left, right, bottom, top - } - - // MARK: Initialization - - public convenience init(axis: UILayoutConstraintAxis, spacing: CGFloat) { - self.init(frame: .zero) - self.axis = axis - self.spacing = spacing - } - - public override init(frame: CGRect) { - super.init(frame: frame) - setup() - } - - required public init(coder: NSCoder) { - super.init(coder: coder) - } - - // MARK: - Setup - - /// Sets up the default properties - open func setup() { - translatesAutoresizingMaskIntoConstraints = false - distribution = .fill - alignment = .bottom - } -} diff --git a/Sources/Views/InputTextView.swift b/Sources/Views/InputTextView.swift deleted file mode 100644 index c15e9e83a..000000000 --- a/Sources/Views/InputTextView.swift +++ /dev/null @@ -1,373 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import UIKit - -/** - A UITextView that has a UILabel embedded for placeholder text - - ## Important Notes ## - 1. Changing the font, textAlignment or textContainerInset automatically performs the same modifications to the placeholderLabel - 2. Intended to be used in an `MessageInputBar` - 3. Default placeholder text is "New Message" - 4. Will pass a pasted image it's `MessageInputBar`'s `InputManager`s - */ -open class InputTextView: UITextView { - - // MARK: - Properties - - open override var text: String! { - didSet { - postTextViewDidChangeNotification() - placeholderLabel.isHidden = !text.isEmpty - } - } - - open override var attributedText: NSAttributedString! { - didSet { - postTextViewDidChangeNotification() - placeholderLabel.isHidden = !text.isEmpty - } - } - - /// The images that are currently stored as NSTextAttachment's - open var images: [UIImage] { - return parseForAttachedImages() - } - - open var components: [Any] { - return parseForComponents() - } - - open var isImagePasteEnabled: Bool = true - - /// A UILabel that holds the InputTextView's placeholder text - open let placeholderLabel: UILabel = { - let label = UILabel() - label.numberOfLines = 0 - label.textColor = .lightGray - label.text = "New Message" - label.backgroundColor = .clear - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - /// The placeholder text that appears when there is no text. The default value is "New Message" - open var placeholder: String? = "New Message" { - didSet { - placeholderLabel.text = placeholder - } - } - - /// The placeholderLabel's textColor - open var placeholderTextColor: UIColor? = .lightGray { - didSet { - placeholderLabel.textColor = placeholderTextColor - } - } - - /// The UIEdgeInsets the placeholderLabel has within the InputTextView - open var placeholderLabelInsets: UIEdgeInsets = UIEdgeInsets(top: 4, left: 7, bottom: 4, right: 7) { - didSet { - updateConstraintsForPlaceholderLabel() - } - } - - /// The font of the InputTextView. When set the placeholderLabel's font is also updated - open override var font: UIFont! { - didSet { - placeholderLabel.font = font - } - } - - /// The textAlignment of the InputTextView. When set the placeholderLabel's textAlignment is also updated - open override var textAlignment: NSTextAlignment { - didSet { - placeholderLabel.textAlignment = textAlignment - } - } - - open override var scrollIndicatorInsets: UIEdgeInsets { - didSet { - // When .zero a rendering issue can occur - if scrollIndicatorInsets == .zero { - scrollIndicatorInsets = UIEdgeInsets(top: .leastNonzeroMagnitude, - left: .leastNonzeroMagnitude, - bottom: .leastNonzeroMagnitude, - right: .leastNonzeroMagnitude) - } - } - } - - /// A weak reference to the MessageInputBar that the InputTextView is contained within - open weak var messageInputBar: MessageInputBar? - - /// The constraints of the placeholderLabel - private var placeholderLabelConstraintSet: NSLayoutConstraintSet? - - // MARK: - Initializers - - public convenience init() { - self.init(frame: .zero) - } - - public override init(frame: CGRect, textContainer: NSTextContainer?) { - super.init(frame: frame, textContainer: textContainer) - setup() - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setup() - } - - deinit { - NotificationCenter.default.removeObserver(self) - } - - // MARK: - Setup - - /// Sets up the default properties - open func setup() { - - font = UIFont.preferredFont(forTextStyle: .body) - textContainerInset = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) - scrollIndicatorInsets = UIEdgeInsets(top: .leastNonzeroMagnitude, - left: .leastNonzeroMagnitude, - bottom: .leastNonzeroMagnitude, - right: .leastNonzeroMagnitude) - isScrollEnabled = false - layer.cornerRadius = 5.0 - layer.borderWidth = 1.25 - layer.borderColor = UIColor.lightGray.cgColor - allowsEditingTextAttributes = false - setupPlaceholderLabel() - setupObservers() - } - - // swiftlint:disable colon - /// Adds the placeholderLabel to the view and sets up its initial constraints - private func setupPlaceholderLabel() { - - addSubview(placeholderLabel) - placeholderLabelConstraintSet = NSLayoutConstraintSet( - top: placeholderLabel.topAnchor.constraint(equalTo: topAnchor, constant: placeholderLabelInsets.top), - bottom: placeholderLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -placeholderLabelInsets.bottom), - left: placeholderLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: placeholderLabelInsets.left), - right: placeholderLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -placeholderLabelInsets.right), - centerX: placeholderLabel.centerXAnchor.constraint(equalTo: centerXAnchor), - centerY: placeholderLabel.centerYAnchor.constraint(equalTo: centerYAnchor) - ) - placeholderLabelConstraintSet?.centerX?.priority = .defaultLow - placeholderLabelConstraintSet?.centerY?.priority = .defaultLow - placeholderLabelConstraintSet?.activate() - } - // swiftlint:enable colon - - /// Adds the required notification observers - private func setupObservers() { - - NotificationCenter.default.addObserver(self, - selector: #selector(InputTextView.redrawTextAttachments), - name: .UIDeviceOrientationDidChange, object: nil) - } - - /// Updates the placeholderLabels constraint constants to match the placeholderLabelInsets - private func updateConstraintsForPlaceholderLabel() { - - placeholderLabelConstraintSet?.top?.constant = placeholderLabelInsets.top - placeholderLabelConstraintSet?.bottom?.constant = -placeholderLabelInsets.bottom - placeholderLabelConstraintSet?.left?.constant = placeholderLabelInsets.left - placeholderLabelConstraintSet?.right?.constant = -placeholderLabelInsets.right - } - - // MARK: - Notification - - private func postTextViewDidChangeNotification() { - NotificationCenter.default.post(name: .UITextViewTextDidChange, object: self) - } - - // MARK: - Image Paste Support - - open override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { - - if action == NSSelectorFromString("paste:") && UIPasteboard.general.image != nil { - return isImagePasteEnabled - } - return super.canPerformAction(action, withSender: sender) - } - - open override func paste(_ sender: Any?) { - - guard let image = UIPasteboard.general.image else { - return super.paste(sender) - } - pasteImageInTextContainer(with: image) - } - - /// Addes a new UIImage to the NSTextContainer as an NSTextAttachment - /// - /// - Parameter image: The image to add - private func pasteImageInTextContainer(with image: UIImage) { - - // Add the new image as an NSTextAttachment - let attributedImageString = NSAttributedString(attachment: textAttachment(using: image)) - - let isEmpty = attributedText.length == 0 - - // Add a new line character before the image, this is what iMessage does - let newAttributedStingComponent = isEmpty ? NSMutableAttributedString(string: "") : NSMutableAttributedString(string: "\n") - newAttributedStingComponent.append(attributedImageString) - - // Add a new line character after the image, this is what iMessage does - newAttributedStingComponent.append(NSAttributedString(string: "\n")) - - // The attributes that should be applied to the new NSAttributedString to match the current attributes - let attributes: [NSAttributedStringKey: Any] = [ - NSAttributedStringKey.font: font ?? UIFont.preferredFont(forTextStyle: .body), - NSAttributedStringKey.foregroundColor: textColor ?? .black - ] - newAttributedStingComponent.addAttributes(attributes, range: NSRange(location: 0, length: newAttributedStingComponent.length)) - - textStorage.beginEditing() - // Paste over selected text - textStorage.replaceCharacters(in: selectedRange, with: newAttributedStingComponent) - textStorage.endEditing() - - // Advance the range to the selected range plus the number of characters added - let location = selectedRange.location + (isEmpty ? 2 : 3) - selectedRange = NSRange(location: location, length: 0) - - // Broadcast a notification to recievers such as the MessageInputBar which will handle resizing - NotificationCenter.default.post(name: .UITextViewTextDidChange, object: self) - } - - /// Returns an NSTextAttachment the provided image that will fit inside the NSTextContainer - /// - /// - Parameter image: The image to create an attachment with - /// - Returns: The formatted NSTextAttachment - private func textAttachment(using image: UIImage) -> NSTextAttachment { - - guard let cgImage = image.cgImage else { return NSTextAttachment() } - let scale = image.size.width / (frame.width - 2 * (textContainerInset.left + textContainerInset.right)) - let textAttachment = NSTextAttachment() - textAttachment.image = UIImage(cgImage: cgImage, scale: scale, orientation: .up) - return textAttachment - } - - /// Returns all images that exist as NSTextAttachment's - /// - /// - Returns: An array of type UIImage - private func parseForAttachedImages() -> [UIImage] { - - var images = [UIImage]() - let range = NSRange(location: 0, length: attributedText.length) - attributedText.enumerateAttribute(.attachment, in: range, options: [], using: { value, range, _ -> Void in - - if let attachment = value as? NSTextAttachment { - if let image = attachment.image { - images.append(image) - } else if let image = attachment.image(forBounds: attachment.bounds, - textContainer: nil, - characterIndex: range.location) { - images.append(image) - } - } - }) - return images - } - - /// Returns an array of components (either a String or UIImage) that makes up the textContainer in - /// the order that they were typed - /// - /// - Returns: An array of objects guaranteed to be of UIImage or String - private func parseForComponents() -> [Any] { - - var components = [Any]() - var attachments = [(NSRange, UIImage)]() - let length = attributedText.length - let range = NSRange(location: 0, length: length) - attributedText.enumerateAttribute(.attachment, in: range) { (object, range, _) in - if let attachment = object as? NSTextAttachment { - if let image = attachment.image { - attachments.append((range, image)) - } else if let image = attachment.image(forBounds: attachment.bounds, - textContainer: nil, - characterIndex: range.location) { - attachments.append((range,image)) - } - } - } - - var curLocation = 0 - if attachments.count == 0 { - let text = attributedText.string.trimmingCharacters(in: .whitespacesAndNewlines) - if !text.isEmpty { - components.append(text) - } - } - else { - attachments.forEach { (attachment) in - let (range, image) = attachment - if curLocation < range.location { - let textRange = NSMakeRange(curLocation, range.location) - let text = attributedText.attributedSubstring(from: textRange).string.trimmingCharacters(in: .whitespacesAndNewlines) - if !text.isEmpty { - components.append(text) - } - } - - curLocation = range.location + range.length - components.append(image) - } - if curLocation < length - 1 { - let text = attributedText.attributedSubstring(from: NSMakeRange(curLocation, length - curLocation)).string.trimmingCharacters(in: .whitespacesAndNewlines) - if !text.isEmpty { - components.append(text) - } - } - } - - return components - } - - /// Redraws the NSTextAttachments in the NSTextContainer to fit the current bounds - @objc - private func redrawTextAttachments() { - - guard images.count > 0 else { return } - let range = NSRange(location: 0, length: attributedText.length) - attributedText.enumerateAttribute(.attachment, in: range, options: [], using: { value, _, _ -> Void in - if let attachment = value as? NSTextAttachment, let image = attachment.image { - - // Calculates a new width/height ratio to fit the image in the current frame - let newWidth = frame.width - 2 * (textContainerInset.left + textContainerInset.right) - let ratio = image.size.height / image.size.width - attachment.bounds.size = CGSize(width: newWidth, height: ratio * newWidth) - } - }) - layoutManager.invalidateLayout(forCharacterRange: range, actualCharacterRange: nil) - } - -} diff --git a/Sources/Views/InsetLabel.swift b/Sources/Views/InsetLabel.swift index b543a8ffc..72bb9f8dc 100644 --- a/Sources/Views/InsetLabel.swift +++ b/Sources/Views/InsetLabel.swift @@ -31,7 +31,7 @@ open class InsetLabel: UILabel { } open override func drawText(in rect: CGRect) { - let insetRect = UIEdgeInsetsInsetRect(rect, textInsets) + let insetRect = rect.inset(by: textInsets) super.drawText(in: insetRect) } diff --git a/Sources/Views/MessageContainerView.swift b/Sources/Views/MessageContainerView.swift index 714232936..4577bc5fb 100644 --- a/Sources/Views/MessageContainerView.swift +++ b/Sources/Views/MessageContainerView.swift @@ -49,7 +49,7 @@ open class MessageContainerView: UIImageView { case .none, .custom: break case .bubble, .bubbleTail, .bubbleOutline, .bubbleTailOutline: - imageMask.frame = bounds + imageMask.frame = bounds.insetBy(dx: 0.0, dy: 2.0) } } diff --git a/Sources/Views/MessageInputBar.swift b/Sources/Views/MessageInputBar.swift deleted file mode 100644 index d925248db..000000000 --- a/Sources/Views/MessageInputBar.swift +++ /dev/null @@ -1,716 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import UIKit - -/// A powerful InputAccessoryView ideal for messaging applications -open class MessageInputBar: UIView { - - // MARK: - Properties - - /// A delegate to broadcast notifications from the MessageInputBar - open weak var delegate: MessageInputBarDelegate? - - /// The background UIView anchored to the bottom, left, and right of the MessageInputBar - /// with a top anchor equal to the bottom of the top InputStackView - open var backgroundView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .inputBarGray - return view - }() - - /// A content UIView that holds the left/right/bottom InputStackViews and InputTextView. Anchored to the bottom of the - /// topStackView and inset by the padding UIEdgeInsets - open var contentView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - /** - A UIVisualEffectView that adds a blur effect to make the view appear transparent. - - ## Important Notes ## - 1. The blurView is initially not added to the backgroundView to improve performance when not needed. When `isTranslucent` is set to TRUE for the first time the blurView is added and anchored to the `backgroundView`s edge anchors - */ - open var blurView: UIVisualEffectView = { - let blurEffect = UIBlurEffect(style: .light) - let view = UIVisualEffectView(effect: blurEffect) - view.translatesAutoresizingMaskIntoConstraints = false - return view - }() - - /// Determines if the MessageInputBar should have a translucent effect - open var isTranslucent: Bool = false { - didSet { - if isTranslucent && blurView.superview == nil { - backgroundView.addSubview(blurView) - blurView.fillSuperview() - } - blurView.isHidden = !isTranslucent - let color: UIColor = backgroundView.backgroundColor ?? .inputBarGray - backgroundView.backgroundColor = isTranslucent ? color.withAlphaComponent(0.75) : color.withAlphaComponent(1.0) - } - } - - /// A SeparatorLine that is anchored at the top of the MessageInputBar with a height of 1 - open let separatorLine = SeparatorLine() - - /** - The InputStackView at the InputStackView.top position - - ## Important Notes ## - 1. It's axis is initially set to .vertical - 2. It's alignment is initially set to .fill - */ - open let topStackView: InputStackView = { - let stackView = InputStackView(axis: .vertical, spacing: 0) - stackView.alignment = .fill - return stackView - }() - - /** - The InputStackView at the InputStackView.left position - - ## Important Notes ## - 1. It's axis is initially set to .horizontal - */ - open let leftStackView = InputStackView(axis: .horizontal, spacing: 0) - - /** - The InputStackView at the InputStackView.right position - - ## Important Notes ## - 1. It's axis is initially set to .horizontal - */ - open let rightStackView = InputStackView(axis: .horizontal, spacing: 0) - - /** - The InputStackView at the InputStackView.bottom position - - ## Important Notes ## - 1. It's axis is initially set to .horizontal - 2. It's spacing is initially set to 15 - */ - open let bottomStackView = InputStackView(axis: .horizontal, spacing: 15) - - /// The InputTextView a user can input a message in - open lazy var inputTextView: InputTextView = { - let textView = InputTextView() - textView.translatesAutoresizingMaskIntoConstraints = false - textView.messageInputBar = self - return textView - }() - - /// A InputBarButtonItem used as the send button and initially placed in the rightStackView - open var sendButton: InputBarButtonItem = { - return InputBarButtonItem() - .configure { - $0.setSize(CGSize(width: 52, height: 28), animated: false) - $0.isEnabled = false - $0.title = "Send" - $0.titleLabel?.font = UIFont.preferredFont(forTextStyle: .headline) - }.onTouchUpInside { - $0.messageInputBar?.didSelectSendButton() - } - }() - - /// A boolean that determines whether the sendButton's `isEnabled` state should be managed automatically. - open var shouldManageSendButtonEnabledState = true - - /** - The anchor constants that inset the contentView - - ```` - V:|...[InputStackView.top]-(padding.top)-[contentView]-(padding.bottom)-| - - H:|-(padding.left)-[contentView]-(padding.right)-| - ```` - - */ - open var padding: UIEdgeInsets = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12) { - didSet { - updatePadding() - } - } - - /** - The anchor constants used by the top InputStackView - - ## Important Notes ## - 1. The topStackViewPadding.bottom property is not used. Use padding.top to add separation - - ```` - V:|-(topStackViewPadding.top)-[InputStackView.top]-(padding.top)-[InputTextView]-...| - - H:|-(topStackViewPadding.left)-[InputStackView.top]-(topStackViewPadding.right)-| - ```` - - */ - open var topStackViewPadding: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) { - didSet { - updateTopStackViewPadding() - } - } - - /** - The anchor constants used by the InputStackView - - ```` - V:|...-(padding.top)-(textViewPadding.top)-[InputTextView]-(textViewPadding.bottom)-[InputStackView.bottom]-...| - - H:|...-[InputStackView.left]-(textViewPadding.left)-[InputTextView]-(textViewPadding.right)-[InputStackView.right]-...| - ```` - - */ - open var textViewPadding: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8) { - didSet { - updateTextViewPadding() - } - } - - /// Returns the most recent size calculated by `calculateIntrinsicContentSize()` - open override var intrinsicContentSize: CGSize { - return cachedIntrinsicContentSize - } - - /// The intrinsicContentSize can change a lot so the delegate method - /// `inputBar(self, didChangeIntrinsicContentTo: size)` only needs to be called - /// when it's different - public private(set) var previousIntrinsicContentSize: CGSize? - - /// The most recent calculation of the intrinsicContentSize - private lazy var cachedIntrinsicContentSize: CGSize = calculateIntrinsicContentSize() - - /// A boolean that indicates if the maxTextViewHeight has been met. Keeping track of this - /// improves the performance - public private(set) var isOverMaxTextViewHeight = false - - /// A boolean that determines if the maxTextViewHeight should be auto updated on device rotation - open var shouldAutoUpdateMaxTextViewHeight = true - - /// The maximum height that the InputTextView can reach - open var maxTextViewHeight: CGFloat = 0 { - didSet { - textViewHeightAnchor?.constant = maxTextViewHeight - invalidateIntrinsicContentSize() - } - } - - /// The height that will fit the current text in the InputTextView based on its current bounds - public var requiredInputTextViewHeight: CGFloat { - let maxTextViewSize = CGSize(width: inputTextView.bounds.width, height: .greatestFiniteMagnitude) - return inputTextView.sizeThatFits(maxTextViewSize).height.rounded(.down) - } - - /// The fixed widthAnchor constant of the leftStackView - public private(set) var leftStackViewWidthConstant: CGFloat = 0 { - didSet { - leftStackViewLayoutSet?.width?.constant = leftStackViewWidthConstant - } - } - - /// The fixed widthAnchor constant of the rightStackView - public private(set) var rightStackViewWidthConstant: CGFloat = 52 { - didSet { - rightStackViewLayoutSet?.width?.constant = rightStackViewWidthConstant - } - } - - /// The InputBarItems held in the leftStackView - public private(set) var leftStackViewItems: [InputBarButtonItem] = [] - - /// The InputBarItems held in the rightStackView - public private(set) var rightStackViewItems: [InputBarButtonItem] = [] - - /// The InputBarItems held in the bottomStackView - public private(set) var bottomStackViewItems: [InputBarButtonItem] = [] - - /// The InputBarItems held in the topStackView - public private(set) var topStackViewItems: [InputBarButtonItem] = [] - - /// The InputBarItems held to make use of their hooks but they are not automatically added to a UIStackView - open var nonStackViewItems: [InputBarButtonItem] = [] - - /// Returns a flatMap of all the items in each of the UIStackViews - public var items: [InputBarButtonItem] { - return [leftStackViewItems, rightStackViewItems, bottomStackViewItems, nonStackViewItems].flatMap { $0 } - } - - // MARK: - Auto-Layout Management - - private var textViewLayoutSet: NSLayoutConstraintSet? - private var textViewHeightAnchor: NSLayoutConstraint? - private var topStackViewLayoutSet: NSLayoutConstraintSet? - private var leftStackViewLayoutSet: NSLayoutConstraintSet? - private var rightStackViewLayoutSet: NSLayoutConstraintSet? - private var bottomStackViewLayoutSet: NSLayoutConstraintSet? - private var contentViewLayoutSet: NSLayoutConstraintSet? - private var windowAnchor: NSLayoutConstraint? - private var backgroundViewBottomAnchor: NSLayoutConstraint? - - // MARK: - Initialization - - public convenience init() { - self.init(frame: .zero) - } - - public override init(frame: CGRect) { - super.init(frame: frame) - setup() - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - setup() - } - - deinit { - NotificationCenter.default.removeObserver(self) - } - - open override func didMoveToWindow() { - super.didMoveToWindow() - setupConstraints(to: window) - } - - // MARK: - Setup - - /// Sets up the default properties - open func setup() { - - autoresizingMask = [.flexibleHeight] - setupSubviews() - setupConstraints() - setupObservers() - } - - /// Adds the required notification observers - private func setupObservers() { - - NotificationCenter.default.addObserver(self, - selector: #selector(MessageInputBar.textViewDidChange), - name: .UITextViewTextDidChange, object: inputTextView) - NotificationCenter.default.addObserver(self, - selector: #selector(MessageInputBar.textViewDidBeginEditing), - name: .UITextViewTextDidBeginEditing, object: inputTextView) - NotificationCenter.default.addObserver(self, - selector: #selector(MessageInputBar.textViewDidEndEditing), - name: .UITextViewTextDidEndEditing, object: inputTextView) - } - - /// Adds all of the subviews - private func setupSubviews() { - - addSubview(backgroundView) - addSubview(topStackView) - addSubview(contentView) - addSubview(separatorLine) - contentView.addSubview(inputTextView) - contentView.addSubview(leftStackView) - contentView.addSubview(rightStackView) - contentView.addSubview(bottomStackView) - setStackViewItems([sendButton], forStack: .right, animated: false) - } - - // swiftlint:disable function_body_length colon - /// Sets up the initial constraints of each subview - private func setupConstraints() { - - // The constraints within the MessageInputBar - separatorLine.addConstraints(topAnchor, left: leftAnchor, right: rightAnchor) - backgroundViewBottomAnchor = backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor) - backgroundViewBottomAnchor?.isActive = true - backgroundView.addConstraints(topStackView.bottomAnchor, left: leftAnchor, right: rightAnchor) - - topStackViewLayoutSet = NSLayoutConstraintSet( - top: topStackView.topAnchor.constraint(equalTo: topAnchor, constant: topStackViewPadding.top), - bottom: topStackView.bottomAnchor.constraint(equalTo: contentView.topAnchor, constant: -padding.top), - left: topStackView.leftAnchor.constraint(equalTo: leftAnchor, constant: topStackViewPadding.left), - right: topStackView.rightAnchor.constraint(equalTo: rightAnchor, constant: -topStackViewPadding.right) - ) - - contentViewLayoutSet = NSLayoutConstraintSet( - top: contentView.topAnchor.constraint(equalTo: topStackView.bottomAnchor, constant: padding.top), - bottom: contentView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -padding.bottom), - left: contentView.leftAnchor.constraint(equalTo: leftAnchor, constant: padding.left), - right: contentView.rightAnchor.constraint(equalTo: rightAnchor, constant: -padding.right) - ) - - if #available(iOS 11.0, *) { - // Switch to safeAreaLayoutGuide - contentViewLayoutSet?.bottom = contentView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -padding.bottom) - contentViewLayoutSet?.left = contentView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor, constant: padding.left) - contentViewLayoutSet?.right = contentView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: -padding.right) - - topStackViewLayoutSet?.left = topStackView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor, constant: topStackViewPadding.left) - topStackViewLayoutSet?.right = topStackView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: -topStackViewPadding.right) - } - - // Constraints Within the contentView - textViewLayoutSet = NSLayoutConstraintSet( - top: inputTextView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: textViewPadding.top), - bottom: inputTextView.bottomAnchor.constraint(equalTo: bottomStackView.topAnchor, constant: -textViewPadding.bottom), - left: inputTextView.leftAnchor.constraint(equalTo: leftStackView.rightAnchor, constant: textViewPadding.left), - right: inputTextView.rightAnchor.constraint(equalTo: rightStackView.leftAnchor, constant: -textViewPadding.right) - ) - maxTextViewHeight = calculateMaxTextViewHeight() - textViewHeightAnchor = inputTextView.heightAnchor.constraint(equalToConstant: maxTextViewHeight) - - leftStackViewLayoutSet = NSLayoutConstraintSet( - top: leftStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0), - bottom: leftStackView.bottomAnchor.constraint(equalTo: inputTextView.bottomAnchor, constant: 0), - left: leftStackView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 0), - width: leftStackView.widthAnchor.constraint(equalToConstant: leftStackViewWidthConstant) - ) - - rightStackViewLayoutSet = NSLayoutConstraintSet( - top: rightStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0), - bottom: rightStackView.bottomAnchor.constraint(equalTo: inputTextView.bottomAnchor, constant: 0), - right: rightStackView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: 0), - width: rightStackView.widthAnchor.constraint(equalToConstant: rightStackViewWidthConstant) - ) - - bottomStackViewLayoutSet = NSLayoutConstraintSet( - top: bottomStackView.topAnchor.constraint(equalTo: inputTextView.bottomAnchor, constant: textViewPadding.bottom), - bottom: bottomStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0), - left: bottomStackView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 0), - right: bottomStackView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: 0) - ) - activateConstraints() - } - // swiftlint:enable function_body_length colon - - /// Respect iPhone X safeAreaInsets - /// Adds a constraint to anchor the bottomAnchor of the contentView to the window's safeAreaLayoutGuide.bottomAnchor - /// - /// - Parameter window: The window to anchor to - private func setupConstraints(to window: UIWindow?) { - if #available(iOS 11.0, *) { - guard UIScreen.main.nativeBounds.height == 2436 else { return } - if let window = window { - windowAnchor?.isActive = false - windowAnchor = contentView.bottomAnchor.constraintLessThanOrEqualToSystemSpacingBelow(window.safeAreaLayoutGuide.bottomAnchor, multiplier: 1) - windowAnchor?.constant = -padding.bottom - windowAnchor?.priority = UILayoutPriority(rawValue: 750) - windowAnchor?.isActive = true - backgroundViewBottomAnchor?.constant = 34 - } - } - } - - // MARK: - Constraint Layout Updates - - /// Updates the constraint constants that correspond to the padding UIEdgeInsets - private func updatePadding() { - topStackViewLayoutSet?.bottom?.constant = -padding.top - contentViewLayoutSet?.top?.constant = padding.top - contentViewLayoutSet?.left?.constant = padding.left - contentViewLayoutSet?.right?.constant = -padding.right - contentViewLayoutSet?.bottom?.constant = -padding.bottom - windowAnchor?.constant = -padding.bottom - } - - /// Updates the constraint constants that correspond to the textViewPadding UIEdgeInsets - private func updateTextViewPadding() { - textViewLayoutSet?.top?.constant = textViewPadding.top - textViewLayoutSet?.left?.constant = textViewPadding.left - textViewLayoutSet?.right?.constant = -textViewPadding.right - textViewLayoutSet?.bottom?.constant = -textViewPadding.bottom - bottomStackViewLayoutSet?.top?.constant = textViewPadding.bottom - } - - /// Updates the constraint constants that correspond to the topStackViewPadding UIEdgeInsets - private func updateTopStackViewPadding() { - topStackViewLayoutSet?.top?.constant = topStackViewPadding.top - topStackViewLayoutSet?.left?.constant = topStackViewPadding.left - topStackViewLayoutSet?.right?.constant = -topStackViewPadding.right - } - - /// Invalidates the view’s intrinsic content size - open override func invalidateIntrinsicContentSize() { - super.invalidateIntrinsicContentSize() - cachedIntrinsicContentSize = calculateIntrinsicContentSize() - if previousIntrinsicContentSize != cachedIntrinsicContentSize { - delegate?.messageInputBar(self, didChangeIntrinsicContentTo: cachedIntrinsicContentSize) - previousIntrinsicContentSize = cachedIntrinsicContentSize - } - } - - // MARK: - Layout Helper Methods - - /// Calculates the correct intrinsicContentSize of the MessageInputBar. This takes into account the various padding edge - /// insets, InputTextView's height and top/bottom InputStackView's heights. - /// - /// - Returns: The required intrinsicContentSize - open func calculateIntrinsicContentSize() -> CGSize { - - var inputTextViewHeight = requiredInputTextViewHeight - if inputTextViewHeight >= maxTextViewHeight { - if !isOverMaxTextViewHeight { - textViewHeightAnchor?.isActive = true - inputTextView.isScrollEnabled = true - isOverMaxTextViewHeight = true - } - inputTextViewHeight = maxTextViewHeight - } else { - if isOverMaxTextViewHeight { - textViewHeightAnchor?.isActive = false - inputTextView.isScrollEnabled = false - isOverMaxTextViewHeight = false - inputTextView.invalidateIntrinsicContentSize() - } - } - - // Calculate the required height - let totalPadding = padding.top + padding.bottom + topStackViewPadding.top + textViewPadding.top + textViewPadding.bottom - let topStackViewHeight = topStackView.arrangedSubviews.count > 0 ? topStackView.bounds.height : 0 - let bottomStackViewHeight = bottomStackView.arrangedSubviews.count > 0 ? bottomStackView.bounds.height : 0 - let verticalStackViewHeight = topStackViewHeight + bottomStackViewHeight - let requiredHeight = inputTextViewHeight + totalPadding + verticalStackViewHeight - return CGSize(width: bounds.width, height: requiredHeight) - } - - /// Returns the max height the InputTextView can grow to based on the UIScreen - /// - /// - Returns: Max Height - open func calculateMaxTextViewHeight() -> CGFloat { - if traitCollection.verticalSizeClass == .regular { - return (UIScreen.main.bounds.height / 3).rounded(.down) - } - return (UIScreen.main.bounds.height / 5).rounded(.down) - } - - /// Layout the given InputStackView's - /// - /// - Parameter positions: The UIStackView's to layout - public func layoutStackViews(_ positions: [InputStackView.Position] = [.left, .right, .bottom, .top]) { - - guard superview != nil else { return } - - for position in positions { - switch position { - case .left: - leftStackView.setNeedsLayout() - leftStackView.layoutIfNeeded() - case .right: - rightStackView.setNeedsLayout() - rightStackView.layoutIfNeeded() - case .bottom: - bottomStackView.setNeedsLayout() - bottomStackView.layoutIfNeeded() - case .top: - topStackView.setNeedsLayout() - topStackView.layoutIfNeeded() - } - } - } - - /// Performs layout changes over the main thread - /// - /// - Parameters: - /// - animated: If the layout should be animated - /// - animations: Code - internal func performLayout(_ animated: Bool, _ animations: @escaping () -> Void) { - deactivateConstraints() - if animated { - DispatchQueue.main.async { - UIView.animate(withDuration: 0.3, animations: animations) - } - } else { - UIView.performWithoutAnimation { animations() } - } - activateConstraints() - } - - /// Activates the NSLayoutConstraintSet's - private func activateConstraints() { - contentViewLayoutSet?.activate() - textViewLayoutSet?.activate() - leftStackViewLayoutSet?.activate() - rightStackViewLayoutSet?.activate() - bottomStackViewLayoutSet?.activate() - topStackViewLayoutSet?.activate() - } - - /// Deactivates the NSLayoutConstraintSet's - private func deactivateConstraints() { - contentViewLayoutSet?.deactivate() - textViewLayoutSet?.deactivate() - leftStackViewLayoutSet?.deactivate() - rightStackViewLayoutSet?.deactivate() - bottomStackViewLayoutSet?.deactivate() - topStackViewLayoutSet?.deactivate() - } - - // MARK: - UIStackView InputBarItem Methods - - // swiftlint:disable function_body_length - /// Removes all of the arranged subviews from the UIStackView and adds the given items. Sets the messageInputBar property of the InputBarButtonItem - /// - /// - Parameters: - /// - items: New UIStackView arranged views - /// - position: The targeted UIStackView - /// - animated: If the layout should be animated - open func setStackViewItems(_ items: [InputBarButtonItem], forStack position: InputStackView.Position, animated: Bool) { - - func setNewItems() { - switch position { - case .left: - leftStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - leftStackViewItems = items - leftStackViewItems.forEach { - $0.messageInputBar = self - $0.parentStackViewPosition = position - leftStackView.addArrangedSubview($0) - } - guard superview != nil else { return } - leftStackView.layoutIfNeeded() - case .right: - rightStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - rightStackViewItems = items - rightStackViewItems.forEach { - $0.messageInputBar = self - $0.parentStackViewPosition = position - rightStackView.addArrangedSubview($0) - } - guard superview != nil else { return } - rightStackView.layoutIfNeeded() - case .bottom: - bottomStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - bottomStackViewItems = items - bottomStackViewItems.forEach { - $0.messageInputBar = self - $0.parentStackViewPosition = position - bottomStackView.addArrangedSubview($0) - } - guard superview != nil else { return } - bottomStackView.layoutIfNeeded() - case .top: - topStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } - topStackViewItems = items - topStackViewItems.forEach { - $0.messageInputBar = self - $0.parentStackViewPosition = position - topStackView.addArrangedSubview($0) - } - guard superview != nil else { return } - topStackView.layoutIfNeeded() - } - invalidateIntrinsicContentSize() - } - - performLayout(animated) { - setNewItems() - } - } - // swiftlint:enable function_body_length - - /// Sets the leftStackViewWidthConstant - /// - /// - Parameters: - /// - newValue: New widthAnchor constant - /// - animated: If the layout should be animated - open func setLeftStackViewWidthConstant(to newValue: CGFloat, animated: Bool) { - performLayout(animated) { - self.leftStackViewWidthConstant = newValue - self.layoutStackViews([.left]) - guard self.superview != nil else { return } - self.layoutIfNeeded() - } - } - - /// Sets the rightStackViewWidthConstant - /// - /// - Parameters: - /// - newValue: New widthAnchor constant - /// - animated: If the layout should be animated - open func setRightStackViewWidthConstant(to newValue: CGFloat, animated: Bool) { - performLayout(animated) { - self.rightStackViewWidthConstant = newValue - self.layoutStackViews([.right]) - guard self.superview != nil else { return } - self.layoutIfNeeded() - } - } - - // MARK: - Notifications/Hooks - - /// Invalidates the intrinsicContentSize - open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - if traitCollection.verticalSizeClass != previousTraitCollection?.verticalSizeClass || traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass { - if shouldAutoUpdateMaxTextViewHeight { - maxTextViewHeight = calculateMaxTextViewHeight() - } - invalidateIntrinsicContentSize() - } - } - - /// Enables/Disables the sendButton based on the InputTextView's text being empty - /// Calls each items `textViewDidChangeAction` method - /// Calls the delegates `textViewTextDidChangeTo` method - /// Invalidates the intrinsicContentSize - @objc - open func textViewDidChange() { - let trimmedText = inputTextView.text.trimmingCharacters(in: .whitespacesAndNewlines) - - if shouldManageSendButtonEnabledState { - sendButton.isEnabled = !trimmedText.isEmpty || inputTextView.images.count > 0 - } - inputTextView.placeholderLabel.isHidden = !inputTextView.text.isEmpty - - items.forEach { $0.textViewDidChangeAction(with: inputTextView) } - - delegate?.messageInputBar(self, textViewTextDidChangeTo: trimmedText) - - if requiredInputTextViewHeight != inputTextView.bounds.height { - // Prevent un-needed content size invalidation - invalidateIntrinsicContentSize() - } - } - - /// Calls each items `keyboardEditingBeginsAction` method - /// Invalidates the intrinsicContentSize so that the keyboard does not overlap the view - @objc - open func textViewDidBeginEditing() { - items.forEach { $0.keyboardEditingBeginsAction() } - } - - /// Calls each items `keyboardEditingEndsAction` method - @objc - open func textViewDidEndEditing() { - items.forEach { $0.keyboardEditingEndsAction() } - } - - // MARK: - User Actions - - /// Calls the delegates `didPressSendButtonWith` method - /// Assumes that the InputTextView's text has been set to empty and calls `inputTextViewDidChange()` - /// Invalidates each of the inputManagers - open func didSelectSendButton() { - delegate?.messageInputBar(self, didPressSendButtonWith: inputTextView.text) - } -} diff --git a/Sources/Views/MessageLabel.swift b/Sources/Views/MessageLabel.swift index 7270e8051..d451d4864 100644 --- a/Sources/Views/MessageLabel.swift +++ b/Sources/Views/MessageLabel.swift @@ -112,30 +112,37 @@ open class MessageLabel: UILabel { if !isConfiguring { setNeedsDisplay() } } } + + open override var intrinsicContentSize: CGSize { + var size = super.intrinsicContentSize + size.width += textInsets.horizontal + size.height += textInsets.vertical + return size + } internal var messageLabelFont: UIFont? private var attributesNeedUpdate = false - public static var defaultAttributes: [NSAttributedStringKey: Any] = { + public static var defaultAttributes: [NSAttributedString.Key: Any] = { return [ - NSAttributedStringKey.foregroundColor: UIColor.darkText, - NSAttributedStringKey.underlineStyle: NSUnderlineStyle.styleSingle.rawValue, - NSAttributedStringKey.underlineColor: UIColor.darkText + NSAttributedString.Key.foregroundColor: UIColor.darkText, + NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue, + NSAttributedString.Key.underlineColor: UIColor.darkText ] }() - open internal(set) var addressAttributes: [NSAttributedStringKey: Any] = defaultAttributes + open internal(set) var addressAttributes: [NSAttributedString.Key: Any] = defaultAttributes - open internal(set) var dateAttributes: [NSAttributedStringKey: Any] = defaultAttributes + open internal(set) var dateAttributes: [NSAttributedString.Key: Any] = defaultAttributes - open internal(set) var phoneNumberAttributes: [NSAttributedStringKey: Any] = defaultAttributes + open internal(set) var phoneNumberAttributes: [NSAttributedString.Key: Any] = defaultAttributes - open internal(set) var urlAttributes: [NSAttributedStringKey: Any] = defaultAttributes + open internal(set) var urlAttributes: [NSAttributedString.Key: Any] = defaultAttributes - open internal(set) var transitInformationAttributes: [NSAttributedStringKey: Any] = defaultAttributes + open internal(set) var transitInformationAttributes: [NSAttributedString.Key: Any] = defaultAttributes - public func setAttributes(_ attributes: [NSAttributedStringKey: Any], detector: DetectorType) { + public func setAttributes(_ attributes: [NSAttributedString.Key: Any], detector: DetectorType) { switch detector { case .phoneNumber: phoneNumberAttributes = attributes @@ -159,19 +166,19 @@ open class MessageLabel: UILabel { public override init(frame: CGRect) { super.init(frame: frame) - self.numberOfLines = 0 - self.lineBreakMode = .byWordWrapping + setupView() } public required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: aDecoder) + setupView() } // MARK: - Open Methods open override func drawText(in rect: CGRect) { - let insetRect = UIEdgeInsetsInsetRect(rect, textInsets) + let insetRect = rect.inset(by: textInsets) textContainer.size = CGSize(width: insetRect.width, height: rect.height) let origin = insetRect.origin @@ -263,7 +270,7 @@ open class MessageLabel: UILabel { } } - private func detectorAttributes(for detectorType: DetectorType) -> [NSAttributedStringKey: Any] { + private func detectorAttributes(for detectorType: DetectorType) -> [NSAttributedString.Key: Any] { switch detectorType { case .address: @@ -280,7 +287,7 @@ open class MessageLabel: UILabel { } - private func detectorAttributes(for checkingResultType: NSTextCheckingResult.CheckingType) -> [NSAttributedStringKey: Any] { + private func detectorAttributes(for checkingResultType: NSTextCheckingResult.CheckingType) -> [NSAttributedString.Key: Any] { switch checkingResultType { case .address: return addressAttributes @@ -296,6 +303,11 @@ open class MessageLabel: UILabel { fatalError(MessageKitError.unrecognizedCheckingResult) } } + + private func setupView() { + numberOfLines = 0 + lineBreakMode = .byWordWrapping + } // MARK: - Parsing Text @@ -313,7 +325,7 @@ open class MessageLabel: UILabel { // Enumerate NSAttributedString NSLinks and append ranges var results: [NSTextCheckingResult] = matches - text.enumerateAttribute(NSAttributedStringKey.link, in: range, options: []) { value, range, _ in + text.enumerateAttribute(NSAttributedString.Key.link, in: range, options: []) { value, range, _ in guard let url = value as? URL else { return } let result = NSTextCheckingResult.linkCheckingResult(range: range, url: url) results.append(result) @@ -387,7 +399,7 @@ open class MessageLabel: UILabel { } - internal func handleGesture(_ touchLocation: CGPoint) -> Bool { + open func handleGesture(_ touchLocation: CGPoint) -> Bool { guard let index = stringIndex(at: touchLocation) else { return false } diff --git a/Sources/Views/MessagesCollectionView.swift b/Sources/Views/MessagesCollectionView.swift index 5a465d14a..ee1e4cd3c 100644 --- a/Sources/Views/MessagesCollectionView.swift +++ b/Sources/Views/MessagesCollectionView.swift @@ -52,7 +52,7 @@ open class MessagesCollectionView: UICollectionView { } required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(frame: .zero, collectionViewLayout: MessagesCollectionViewFlowLayout()) } public convenience init() { @@ -65,8 +65,8 @@ open class MessagesCollectionView: UICollectionView { register(TextMessageCell.self) register(MediaMessageCell.self) register(LocationMessageCell.self) - register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader) - register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionFooter) + register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader) + register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter) } private func setupGestureRecognizers() { @@ -122,6 +122,13 @@ open class MessagesCollectionView: UICollectionView { forSupplementaryViewOfKind: kind, withReuseIdentifier: String(describing: T.self)) } + + /// Registers a nib with reusable view for a specific SectionKind + public func register(_ nib: UINib? = UINib(nibName: String(describing: T.self), bundle: nil), headerFooterClassOfNib headerFooterClass: T.Type, forSupplementaryViewOfKind kind: String) { + register(nib, + forSupplementaryViewOfKind: kind, + withReuseIdentifier: String(describing: T.self)) + } /// Generically dequeues a cell of the correct type allowing you to avoid scattering your code with guard-let-else-fatal public func dequeueReusableCell(_ cellClass: T.Type, for indexPath: IndexPath) -> T { @@ -133,7 +140,7 @@ open class MessagesCollectionView: UICollectionView { /// Generically dequeues a header of the correct type allowing you to avoid scattering your code with guard-let-else-fatal public func dequeueReusableHeaderView(_ viewClass: T.Type, for indexPath: IndexPath) -> T { - let view = dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: String(describing: T.self), for: indexPath) + let view = dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: String(describing: T.self), for: indexPath) guard let viewType = view as? T else { fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(String(describing: T.self))") } @@ -142,7 +149,7 @@ open class MessagesCollectionView: UICollectionView { /// Generically dequeues a footer of the correct type allowing you to avoid scattering your code with guard-let-else-fatal public func dequeueReusableFooterView(_ viewClass: T.Type, for indexPath: IndexPath) -> T { - let view = dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionFooter, withReuseIdentifier: String(describing: T.self), for: indexPath) + let view = dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: String(describing: T.self), for: indexPath) guard let viewType = view as? T else { fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(String(describing: T.self))") } diff --git a/Sources/Views/PlayButtonView.swift b/Sources/Views/PlayButtonView.swift index 9c4f40837..b446ccfda 100644 --- a/Sources/Views/PlayButtonView.swift +++ b/Sources/Views/PlayButtonView.swift @@ -28,7 +28,7 @@ open class PlayButtonView: UIView { // MARK: - Properties - open let triangleView = UIView() + public let triangleView = UIView() private var triangleCenterXConstraint: NSLayoutConstraint? private var cacheFrame: CGRect = .zero @@ -40,14 +40,15 @@ open class PlayButtonView: UIView { setupSubviews() setupConstraints() - - triangleView.clipsToBounds = true - triangleView.backgroundColor = .black - backgroundColor = .playButtonLightGray + setupView() } required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: aDecoder) + + setupSubviews() + setupConstraints() + setupView() } // MARK: - Methods @@ -66,6 +67,13 @@ open class PlayButtonView: UIView { private func setupSubviews() { addSubview(triangleView) } + + private func setupView() { + triangleView.clipsToBounds = true + triangleView.backgroundColor = .black + + backgroundColor = .playButtonLightGray + } private func setupConstraints() { triangleView.translatesAutoresizingMaskIntoConstraints = false diff --git a/Tests/ControllersTest/MessageLabelSpec.swift b/Tests/ControllersTest/MessageLabelSpec.swift index ead9e373b..abf28a00d 100644 --- a/Tests/ControllersTest/MessageLabelSpec.swift +++ b/Tests/ControllersTest/MessageLabelSpec.swift @@ -243,7 +243,7 @@ final class MessageLabelSpec: QuickSpec { fileprivate extension MessageLabel { - var textAttributes: [NSAttributedStringKey: Any] { + var textAttributes: [NSAttributedString.Key: Any] { let length = attributedText!.length var range = NSRange(location: 0, length: length) return attributedText!.attributes(at: 0, effectiveRange: &range) diff --git a/Tests/ControllersTest/MessagesViewControllerSpec.swift b/Tests/ControllersTest/MessagesViewControllerSpec.swift index ac190593e..1cf4d5655 100644 --- a/Tests/ControllersTest/MessagesViewControllerSpec.swift +++ b/Tests/ControllersTest/MessagesViewControllerSpec.swift @@ -24,6 +24,7 @@ import Quick import Nimble +import MessageInputBar @testable import MessageKit //swiftlint:disable function_body_length @@ -40,7 +41,7 @@ final class MessagesViewControllerSpec: QuickSpec { describe("default property values") { context("after initialization") { it("sets scrollsToBottomOnKeyboardBeginsEditing to false") { - expect(controller.scrollsToBottomOnKeybordBeginsEditing).to(beFalse()) + expect(controller.scrollsToBottomOnKeyboardBeginsEditing).to(beFalse()) } it("sets canBecomeFirstResponder to true") { expect(controller.canBecomeFirstResponder).to(beTrue()) @@ -55,6 +56,9 @@ final class MessagesViewControllerSpec: QuickSpec { it("has a MessagesCollectionView") { expect(controller.messagesCollectionView).toNot(beNil()) } + it("sets the CollectionView's layout to be an instance of MessagesCollectionViewFlowLayout") { + expect(controller.messagesCollectionView.collectionViewLayout).to(beAnInstanceOf(MessagesCollectionViewFlowLayout.self)) + } } context("after viewDidLoad") { beforeEach { @@ -71,7 +75,7 @@ final class MessagesViewControllerSpec: QuickSpec { } it("sets keyboardDismissMode to .interactive") { let dismissMode = controller.messagesCollectionView.keyboardDismissMode - expect(dismissMode).to(equal(UIScrollViewKeyboardDismissMode.interactive)) + expect(dismissMode).to(equal(UIScrollView.KeyboardDismissMode.interactive)) } it("sets alwaysBounceVertical to true") { expect(controller.messagesCollectionView.alwaysBounceVertical).to(beTrue()) @@ -124,12 +128,12 @@ final class MessagesViewControllerSpec: QuickSpec { } describe("scrolling behavior when keyboard begins editing") { - context("scrollsToBottomOnKeybordBeginsEditing is true") { + context("scrollsToBottomOnKeyboardBeginsEditing is true") { it("should scroll to bottom") { } } - context("scrollsToBottomOnKeybordBeginsEditing is false") { + context("scrollsToBottomOnKeyboardBeginsEditing is false") { it("should not scroll to bottom") { } diff --git a/Tests/ControllersTest/MessagesViewControllerTests.swift b/Tests/ControllersTest/MessagesViewControllerTests.swift index 520667be1..fb30c51fe 100644 --- a/Tests/ControllersTest/MessagesViewControllerTests.swift +++ b/Tests/ControllersTest/MessagesViewControllerTests.swift @@ -55,18 +55,6 @@ class MessagesViewControllerTests: XCTestCase { // MARK: - Test - func testMessageCollectionViewLayout_isMessageCollectionViewLayout() { - XCTAssertNotNil(sut.messagesCollectionView.collectionViewLayout) - XCTAssertTrue(sut.messagesCollectionView.collectionViewLayout is MessagesCollectionViewFlowLayout) - } - - func testMessageCollectionView_hasMessageCollectionFlowLayoutAfterViewDidLoad() { - let layout = sut.messagesCollectionView.collectionViewLayout - - XCTAssertNotNil(layout) - XCTAssertTrue(layout is MessagesCollectionViewFlowLayout) - } - func testViewDidLoad_shouldSetDelegateAndDataSourceToTheSameObject() { XCTAssertEqual(sut.messagesCollectionView.delegate as? MessagesViewController, sut.messagesCollectionView.dataSource as? MessagesViewController) @@ -122,7 +110,7 @@ class MessagesViewControllerTests: XCTestCase { func testCellForItemWithAttributedTextData_returnsTextMessageCell() { let messagesDataSource = MockMessagesDataSource() sut.messagesCollectionView.messagesDataSource = messagesDataSource - let attributes = [NSAttributedStringKey.foregroundColor: UIColor.black] + let attributes = [NSAttributedString.Key.foregroundColor: UIColor.black] let attriutedString = NSAttributedString(string: "Test", attributes: attributes) messagesDataSource.messages.append(MockMessage(attributedText: attriutedString, sender: messagesDataSource.senders[0], diff --git a/Tests/ModelTests/MessageKitDateFormatterTests.swift b/Tests/ModelTests/MessageKitDateFormatterTests.swift index 2f77479e8..e9b24175a 100644 --- a/Tests/ModelTests/MessageKitDateFormatterTests.swift +++ b/Tests/ModelTests/MessageKitDateFormatterTests.swift @@ -28,7 +28,7 @@ import XCTest class MessageKitDateFormatterTests: XCTestCase { var formatter: DateFormatter! - let attributes = [NSAttributedStringKey.backgroundColor: "red"] + let attributes = [NSAttributedString.Key.backgroundColor: "red"] override func setUp() { super.setUp() diff --git a/Tests/ModelTests/MessageStyleTests.swift b/Tests/ModelTests/MessageStyleTests.swift index fd77e66f3..7534ac99e 100644 --- a/Tests/ModelTests/MessageStyleTests.swift +++ b/Tests/ModelTests/MessageStyleTests.swift @@ -38,10 +38,10 @@ class MessageStyleTests: XCTestCase { } func testTailCornerImageOrientation() { - XCTAssertEqual(MessageStyle.TailCorner.bottomRight.imageOrientation, UIImageOrientation.up) - XCTAssertEqual(MessageStyle.TailCorner.bottomLeft.imageOrientation, UIImageOrientation.upMirrored) - XCTAssertEqual(MessageStyle.TailCorner.topLeft.imageOrientation, UIImageOrientation.down) - XCTAssertEqual(MessageStyle.TailCorner.topRight.imageOrientation, UIImageOrientation.downMirrored) + XCTAssertEqual(MessageStyle.TailCorner.bottomRight.imageOrientation, UIImage.Orientation.up) + XCTAssertEqual(MessageStyle.TailCorner.bottomLeft.imageOrientation, UIImage.Orientation.upMirrored) + XCTAssertEqual(MessageStyle.TailCorner.topLeft.imageOrientation, UIImage.Orientation.down) + XCTAssertEqual(MessageStyle.TailCorner.topRight.imageOrientation, UIImage.Orientation.downMirrored) } func testTailStyleImageNameSuffix() { @@ -56,16 +56,16 @@ class MessageStyleTests: XCTestCase { func testImageBubble() { let assetBundle = Bundle.messageKitAssetBundle() let imagePath = assetBundle.path(forResource: "bubble_full", ofType: "png", inDirectory: "Images") - let originalData = UIImagePNGRepresentation(MessageStyle.bubble.image ?? UIImage()) - let testData = UIImagePNGRepresentation(stretch(UIImage(contentsOfFile: imagePath!)!).withRenderingMode(.alwaysTemplate)) + let originalData = (MessageStyle.bubble.image ?? UIImage()).pngData() + let testData = stretch(UIImage(contentsOfFile: imagePath!)!).withRenderingMode(.alwaysTemplate).pngData() XCTAssertEqual(originalData, testData) } func testImageBubbleOutline() { let assetBundle = Bundle.messageKitAssetBundle() let imagePath = assetBundle.path(forResource: "bubble_outlined", ofType: "png", inDirectory: "Images") - let originalData = UIImagePNGRepresentation(MessageStyle.bubbleOutline(.black).image ?? UIImage()) - let testData = UIImagePNGRepresentation(stretch(UIImage(contentsOfFile: imagePath!)!).withRenderingMode(.alwaysTemplate)) + let originalData = (MessageStyle.bubbleOutline(.black).image ?? UIImage()).pngData() + let testData = stretch(UIImage(contentsOfFile: imagePath!)!).withRenderingMode(.alwaysTemplate).pngData() XCTAssertEqual(originalData, testData) } @@ -73,20 +73,20 @@ class MessageStyleTests: XCTestCase { let assetBundle = Bundle.messageKitAssetBundle() let imagePath = assetBundle.path(forResource: "bubble_full_tail_v2", ofType: "png", inDirectory: "Images") - var originalData = UIImagePNGRepresentation(MessageStyle.bubbleTail(.bottomLeft, .curved).image ?? UIImage()) - var testData = UIImagePNGRepresentation(stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate)) + var originalData = (MessageStyle.bubbleTail(.bottomLeft, .curved).image ?? UIImage()).pngData() + var testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate).pngData() XCTAssertEqual(originalData, testData) - originalData = UIImagePNGRepresentation(MessageStyle.bubbleTail(.bottomRight, .curved).image ?? UIImage()) - testData = UIImagePNGRepresentation(stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate)) + originalData = (MessageStyle.bubbleTail(.bottomRight, .curved).image ?? UIImage()).pngData() + testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate).pngData() XCTAssertEqual(originalData, testData) - originalData = UIImagePNGRepresentation(MessageStyle.bubbleTail(.topLeft, .curved).image ?? UIImage()) - testData = UIImagePNGRepresentation(stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate)) + originalData = (MessageStyle.bubbleTail(.topLeft, .curved).image ?? UIImage()).pngData() + testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate).pngData() XCTAssertEqual(originalData, testData) - originalData = UIImagePNGRepresentation(MessageStyle.bubbleTail(.topRight, .curved).image ?? UIImage()) - testData = UIImagePNGRepresentation(stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topRight)!).withRenderingMode(.alwaysTemplate)) + originalData = (MessageStyle.bubbleTail(.topRight, .curved).image ?? UIImage()).pngData() + testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topRight)!).withRenderingMode(.alwaysTemplate).pngData() XCTAssertEqual(originalData, testData) } @@ -94,20 +94,20 @@ class MessageStyleTests: XCTestCase { let assetBundle = Bundle.messageKitAssetBundle() let imagePath = assetBundle.path(forResource: "bubble_full_tail_v1", ofType: "png", inDirectory: "Images") - var originalData = UIImagePNGRepresentation(MessageStyle.bubbleTail(.bottomLeft, .pointedEdge).image ?? UIImage()) - var testData = UIImagePNGRepresentation(stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate)) + var originalData = (MessageStyle.bubbleTail(.bottomLeft, .pointedEdge).image ?? UIImage()).pngData() + var testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate).pngData() XCTAssertEqual(originalData, testData) - originalData = UIImagePNGRepresentation(MessageStyle.bubbleTail(.bottomRight, .pointedEdge).image ?? UIImage()) - testData = UIImagePNGRepresentation(stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate)) + originalData = (MessageStyle.bubbleTail(.bottomRight, .pointedEdge).image ?? UIImage()).pngData() + testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate).pngData() XCTAssertEqual(originalData, testData) - originalData = UIImagePNGRepresentation(MessageStyle.bubbleTail(.topLeft, .pointedEdge).image ?? UIImage()) - testData = UIImagePNGRepresentation(stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate)) + originalData = (MessageStyle.bubbleTail(.topLeft, .pointedEdge).image ?? UIImage()).pngData() + testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate).pngData() XCTAssertEqual(originalData, testData) - originalData = UIImagePNGRepresentation(MessageStyle.bubbleTail(.topRight, .pointedEdge).image ?? UIImage()) - testData = UIImagePNGRepresentation(stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topRight)!).withRenderingMode(.alwaysTemplate)) + originalData = (MessageStyle.bubbleTail(.topRight, .pointedEdge).image ?? UIImage()).pngData() + testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topRight)!).withRenderingMode(.alwaysTemplate).pngData() XCTAssertEqual(originalData, testData) } @@ -115,20 +115,20 @@ class MessageStyleTests: XCTestCase { let assetBundle = Bundle.messageKitAssetBundle() let imagePath = assetBundle.path(forResource: "bubble_outlined_tail_v2", ofType: "png", inDirectory: "Images") - var originalData = UIImagePNGRepresentation(MessageStyle.bubbleTailOutline(.red, .bottomLeft, .curved).image ?? UIImage()) - var testData = UIImagePNGRepresentation(stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate)) + var originalData = (MessageStyle.bubbleTailOutline(.red, .bottomLeft, .curved).image ?? UIImage()).pngData() + var testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate).pngData() XCTAssertEqual(originalData, testData) - originalData = UIImagePNGRepresentation(MessageStyle.bubbleTailOutline(.red, .bottomRight, .curved).image ?? UIImage()) - testData = UIImagePNGRepresentation(stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate)) + originalData = (MessageStyle.bubbleTailOutline(.red, .bottomRight, .curved).image ?? UIImage()).pngData() + testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate).pngData() XCTAssertEqual(originalData, testData) - originalData = UIImagePNGRepresentation(MessageStyle.bubbleTailOutline(.red, .topLeft, .curved).image ?? UIImage()) - testData = UIImagePNGRepresentation(stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate)) + originalData = (MessageStyle.bubbleTailOutline(.red, .topLeft, .curved).image ?? UIImage()).pngData() + testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate).pngData() XCTAssertEqual(originalData, testData) - originalData = UIImagePNGRepresentation(MessageStyle.bubbleTailOutline(.red, .topRight, .curved).image ?? UIImage()) - testData = UIImagePNGRepresentation(stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topRight)!).withRenderingMode(.alwaysTemplate)) + originalData = (MessageStyle.bubbleTailOutline(.red, .topRight, .curved).image ?? UIImage()).pngData() + testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topRight)!).withRenderingMode(.alwaysTemplate).pngData() XCTAssertEqual(originalData, testData) } @@ -136,20 +136,20 @@ class MessageStyleTests: XCTestCase { let assetBundle = Bundle.messageKitAssetBundle() let imagePath = assetBundle.path(forResource: "bubble_outlined_tail_v1", ofType: "png", inDirectory: "Images") - var originalData = UIImagePNGRepresentation(MessageStyle.bubbleTailOutline(.red, .bottomLeft, .pointedEdge).image ?? UIImage()) - var testData = UIImagePNGRepresentation(stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate)) + var originalData = (MessageStyle.bubbleTailOutline(.red, .bottomLeft, .pointedEdge).image ?? UIImage()).pngData() + var testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomLeft)!).withRenderingMode(.alwaysTemplate).pngData() XCTAssertEqual(originalData, testData) - originalData = UIImagePNGRepresentation(MessageStyle.bubbleTailOutline(.red, .bottomRight, .pointedEdge).image ?? UIImage()) - testData = UIImagePNGRepresentation(stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate)) + originalData = (MessageStyle.bubbleTailOutline(.red, .bottomRight, .pointedEdge).image ?? UIImage()).pngData() + testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .bottomRight)!).withRenderingMode(.alwaysTemplate).pngData() XCTAssertEqual(originalData, testData) - originalData = UIImagePNGRepresentation(MessageStyle.bubbleTailOutline(.red, .topLeft, .pointedEdge).image ?? UIImage()) - testData = UIImagePNGRepresentation(stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate)) + originalData = (MessageStyle.bubbleTailOutline(.red, .topLeft, .pointedEdge).image ?? UIImage()).pngData() + testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topLeft)!).withRenderingMode(.alwaysTemplate).pngData() XCTAssertEqual(originalData, testData) - originalData = UIImagePNGRepresentation(MessageStyle.bubbleTailOutline(.red, .topRight, .pointedEdge).image ?? UIImage()) - testData = UIImagePNGRepresentation(stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topRight)!).withRenderingMode(.alwaysTemplate)) + originalData = (MessageStyle.bubbleTailOutline(.red, .topRight, .pointedEdge).image ?? UIImage()).pngData() + testData = stretch(transform(image: UIImage(contentsOfFile: imagePath!)!, corner: .topRight)!).withRenderingMode(.alwaysTemplate).pngData() XCTAssertEqual(originalData, testData) } diff --git a/Tests/ViewsTests/InputBarItemTests.swift b/Tests/ViewsTests/InputBarItemTests.swift deleted file mode 100644 index f4cd141af..000000000 --- a/Tests/ViewsTests/InputBarItemTests.swift +++ /dev/null @@ -1,103 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - 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 XCTest -@testable import MessageKit - -class InputBarItemTests: XCTestCase { - - var button: InputBarButtonItem! - - override func setUp() { - super.setUp() - button = InputBarButtonItem() - } - - override func tearDown() { - button = nil - super.tearDown() - } - - func testSetup() { - XCTAssertEqual(button.contentVerticalAlignment, .center) - XCTAssertEqual(button.contentHorizontalAlignment, .center) - XCTAssertEqual(button.imageView?.contentMode, .scaleAspectFit) - XCTAssertEqual(button.titleColor(for: .normal), UIColor(red: 0, green: 122/255, blue: 1, alpha: 1)) - XCTAssertEqual(button.titleColor(for: .highlighted), UIColor(red: 0, green: 122/255, blue: 1, alpha: 0.3)) - XCTAssertEqual(button.titleColor(for: .disabled), UIColor.lightGray) - XCTAssertFalse(button.adjustsImageWhenHighlighted) - } - - func testImagePropertyDefaultNil() { - XCTAssertNil(button.image) - } - - func testImagePropertyGettersAndSetters() { - button.image = UIImage() - XCTAssertNotNil(button.image) - XCTAssertEqual(UIImagePNGRepresentation(button.image!), UIImagePNGRepresentation(UIImage())) - } - - func testIsHighlightedProperty() { - var onSelectedCalled = false - button.onSelected { (_) in - onSelectedCalled = true - } - - button.isHighlighted = true - - XCTAssert(onSelectedCalled) - } - - func testIsNotHighlightedProperty() { - var onDeselectedCalled = false - button.onDeselected { (_) in - onDeselectedCalled = true - } - - button.isHighlighted = false - - XCTAssert(onDeselectedCalled) - } - - func testIsEnabledProperty() { - var onEnabledCalled = false - button.onEnabled { (_) in - onEnabledCalled = true - } - button.isEnabled = true - - XCTAssert(onEnabledCalled) - } - - func testIsNotEnabledProperty() { - var onDisabledCalled = false - button.onDisabled { (_) in - onDisabledCalled = true - } - button.isEnabled = false - - XCTAssert(onDisabledCalled) - } -} diff --git a/Tests/ViewsTests/InputTextViewTests.swift b/Tests/ViewsTests/InputTextViewTests.swift deleted file mode 100644 index 4edd7b717..000000000 --- a/Tests/ViewsTests/InputTextViewTests.swift +++ /dev/null @@ -1,90 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - 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 XCTest -@testable import MessageKit - -class InputTextViewTests: XCTestCase { - var textView: InputTextView! - - override func setUp() { - super.setUp() - textView = InputTextView() - } - - override func tearDown() { - textView = nil - super.tearDown() - } - - func testPlaceholderLabelSetup() { - XCTAssertEqual(textView.placeholderLabel.numberOfLines, 0) - XCTAssertEqual(textView.placeholderLabel.textColor, UIColor.lightGray) - XCTAssertEqual(textView.placeholderLabel.text, "New Message") - XCTAssertEqual(textView.placeholderLabel.backgroundColor, UIColor.clear) - XCTAssertFalse(textView.placeholderLabel.translatesAutoresizingMaskIntoConstraints) - } - - func testPlaceholderLabelIsHiddenWhenTextIsEmpty() { - textView.text = "" - XCTAssertFalse(textView.placeholderLabel.isHidden) - } - - func testPlaceholderLabelIsNotHiddenWhenTextIsNotEmpty() { - textView.text = "New Text" - XCTAssert(textView.placeholderLabel.isHidden) - } - - func testPlaceholderTextChanging() { - textView.placeholder = "New Placeholder" - XCTAssertEqual(textView.placeholderLabel.text, "New Placeholder") - } - - func testPlaceholderTextColorChanging() { - textView.placeholderTextColor = UIColor.red - XCTAssertEqual(textView.placeholderLabel.textColor, UIColor.red) - } - - func testFontChanging() { - textView.font = UIFont.systemFont(ofSize: 14) - XCTAssertEqual(textView.placeholderLabel.font, UIFont.systemFont(ofSize: 14)) - } - - func testTextAlignmentChanging() { - textView.textAlignment = .center - XCTAssertEqual(textView.placeholderLabel.textAlignment, .center) - } - - func testSetup() { - textView.setup() - XCTAssertEqual(textView.font, UIFont.preferredFont(forTextStyle: .body)) - XCTAssertEqual(textView.textContainerInset, UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4)) - XCTAssertFalse(textView.isScrollEnabled) - XCTAssertEqual(textView.layer.cornerRadius, 5.0) - XCTAssertEqual(textView.layer.borderWidth, 1.25) - XCTAssertEqual(textView.layer.borderColor, UIColor.lightGray.cgColor) - XCTAssertTrue(textView.subviews.contains(textView.placeholderLabel)) - } - -} diff --git a/Tests/ViewsTests/MessageInputBarTests.swift b/Tests/ViewsTests/MessageInputBarTests.swift deleted file mode 100644 index 7ef318a48..000000000 --- a/Tests/ViewsTests/MessageInputBarTests.swift +++ /dev/null @@ -1,120 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - 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 XCTest -@testable import MessageKit - -class MessageInputBarTests: XCTestCase { - - var sut: MessageInputBar! - - override func setUp() { - super.setUp() - sut = MessageInputBar() - } - - override func tearDown() { - sut = nil - super.tearDown() - } - - func testBlurEffectTranslatesAutoresizingMaskIntoConstraints_isFalseAfterInit() { - XCTAssertFalse(sut.blurView.translatesAutoresizingMaskIntoConstraints) - } - - func testIsTranslucent_isFalseForDefault() { - XCTAssertFalse(sut.isTranslucent) - } - - func testUISetups_forIsTranslucentIsTrue() { - sut.isTranslucent = true - XCTAssertFalse(sut.blurView.isHidden) - } - - func testUISetups_forIsTranslucentIsFalse() { - sut.isTranslucent = false - XCTAssertTrue(sut.blurView.isHidden) - } - - func testSeparatorLine_isNotNilAfterInit() { - XCTAssertNotNil(sut.separatorLine) - } - - func testLeftStackView_isNotNilAfterInit() { - XCTAssertNotNil(sut.leftStackView) - } - - func testLeftStackViewAxis_isHorizontalAfterInit() { - XCTAssertEqual(sut.leftStackView.axis, .horizontal) - } - - func testLeftStackViewSpacing_isZeroAfterInit() { - XCTAssertEqual(sut.leftStackView.spacing, 0) - } - - func testRightStackView_isNotNilAfterInit() { - XCTAssertNotNil(sut.rightStackView) - } - - func testRightStackViewAxis_isHorizontalAfterInit() { - XCTAssertEqual(sut.rightStackView.axis, .horizontal) - } - - func testRightStackViewSpacing_isZeroAfterInit() { - XCTAssertEqual(sut.rightStackView.spacing, 0) - } - - func testBottomStackView_isNotNilAfterInit() { - XCTAssertNotNil(sut.bottomStackView) - } - - func testBottomStackViewAxis_isHorizontalAfterInit() { - XCTAssertEqual(sut.bottomStackView.axis, .horizontal) - } - - func testBottomStackViewSpacing_isZeroAfterInit() { - XCTAssertEqual(sut.bottomStackView.spacing, 15) - } - - func testInputTextViewMessageInputBar_isSelf() { - XCTAssertEqual(sut.inputTextView.messageInputBar, sut) - } - - func testInputTextViewTranslatesAutoresizingMaskIntoConstraints_isFalseAfterInit() { - XCTAssertFalse(sut.inputTextView.translatesAutoresizingMaskIntoConstraints) - } - - func testSendButtonTitle_isSendAfterInit() { - XCTAssertEqual(sut.sendButton.title, "Send") - } - - func testSendButtonIsEnabled_isFalseAfterInit() { - XCTAssertFalse(sut.sendButton.isEnabled) - } - - func testSendButtonFont_isHeadlineAfterInit() { - XCTAssertEqual(sut.sendButton.titleLabel?.font, UIFont.preferredFont(forTextStyle: .headline)) - } - -} diff --git a/bin/setup b/bin/setup new file mode 100755 index 000000000..363892bd3 --- /dev/null +++ b/bin/setup @@ -0,0 +1,9 @@ +#!/usr/bin/env sh + +if ! command -v carthage > /dev/null; then + printf 'Carthage is not installed.\n' + printf 'See https://github.com/Carthage/Carthage for install instructions.\n' + exit 1 +fi + +carthage update --use-submodules --no-build --no-use-binaries --new-resolver