From 2e114e1f11ae6a4526a34f7481c60ab4c7963b3d Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Wed, 20 Dec 2017 11:35:34 +0800 Subject: [PATCH 01/22] Move tap gesture from cell to collectionView --- .../Cells/MessageCollectionViewCell.swift | 9 --------- Sources/Views/MessagesCollectionView.swift | 20 +++++++++++++++++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/Sources/Views/Cells/MessageCollectionViewCell.swift b/Sources/Views/Cells/MessageCollectionViewCell.swift index eeb07498..e0bbbff1 100644 --- a/Sources/Views/Cells/MessageCollectionViewCell.swift +++ b/Sources/Views/Cells/MessageCollectionViewCell.swift @@ -57,7 +57,6 @@ open class MessageCollectionViewCell: UICollectionViewCell, CollectionViewReusab super.init(frame: frame) contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] setupSubviews() - setupGestureRecognizers() } required public init?(coder aDecoder: NSCoder) { @@ -115,16 +114,8 @@ open class MessageCollectionViewCell: UICollectionViewCell, CollectionViewReusab cellBottomLabel.attributedText = bottomText } - func setupGestureRecognizers() { - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:))) - contentView.addGestureRecognizer(tapGesture) - } - /// Handle tap gesture on contentView and its subviews like messageContainerView, cellTopLabel, cellBottomLabel, avatarView .... - @objc open func handleTapGesture(_ gesture: UIGestureRecognizer) { - guard gesture.state == .ended else { return } - let touchLocation = gesture.location(in: self) switch true { diff --git a/Sources/Views/MessagesCollectionView.swift b/Sources/Views/MessagesCollectionView.swift index c2851015..d0da8b2d 100644 --- a/Sources/Views/MessagesCollectionView.swift +++ b/Sources/Views/MessagesCollectionView.swift @@ -51,6 +51,7 @@ open class MessagesCollectionView: UICollectionView { public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { super.init(frame: frame, collectionViewLayout: layout) backgroundColor = .white + setupGestureRecognizers() } required public init?(coder aDecoder: NSCoder) { @@ -62,6 +63,25 @@ open class MessagesCollectionView: UICollectionView { } // MARK: - Methods + + func setupGestureRecognizers() { + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:))) + tapGesture.delaysTouchesBegan = true + addGestureRecognizer(tapGesture) + } + + @objc + open func handleTapGesture(_ gesture: UIGestureRecognizer) { + guard gesture.state == .ended else { return } + + let touchLocation = gesture.location(in: self) + + guard let indexPath = indexPathForItem(at: touchLocation) else { return } + + let cell = cellForItem(at: indexPath) as? MessageCollectionViewCell + + cell?.handleTapGesture(gesture) + } public func scrollToBottom(animated: Bool = false) { let collectionViewContentHeight = collectionViewLayout.collectionViewContentSize.height From 20e3a5ece802fb7792f67d95ba5272fbdf23a6b9 Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Wed, 20 Dec 2017 11:39:06 +0800 Subject: [PATCH 02/22] Remove blank lines --- Sources/Views/MessagesCollectionView.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/Views/MessagesCollectionView.swift b/Sources/Views/MessagesCollectionView.swift index d0da8b2d..3be77319 100644 --- a/Sources/Views/MessagesCollectionView.swift +++ b/Sources/Views/MessagesCollectionView.swift @@ -75,11 +75,9 @@ open class MessagesCollectionView: UICollectionView { guard gesture.state == .ended else { return } let touchLocation = gesture.location(in: self) - guard let indexPath = indexPathForItem(at: touchLocation) else { return } let cell = cellForItem(at: indexPath) as? MessageCollectionViewCell - cell?.handleTapGesture(gesture) } From 67c67fc1dc2bf49367e018e0318adf93204936c2 Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Thu, 21 Dec 2017 14:55:42 +0800 Subject: [PATCH 03/22] Add CHANGELOG entry --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4858a5b..a7969884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa ## Upcoming release +### Changed +- **Breaking Change** Moved the `handleTapGesture(_ gesture: UIGestureRecognizer)` method from `MessagesCollectionViewCell` to `MessagesCollectionView`. +- [#417](https://github.com/MessageKit/MessageKit/pull/417) by [@zhongwuzw](https://github.com/zhongwuzw). + + ## [[Prerelease] 0.12.0](https://github.com/MessageKit/MessageKit/releases/tag/0.12.0) ### Added From b4aeea146c6c3c194adde6fcfa0b2f2e79dc4016 Mon Sep 17 00:00:00 2001 From: Wu Zhong Date: Fri, 22 Dec 2017 06:52:04 +0800 Subject: [PATCH 04/22] =?UTF-8?q?Add=20configureImageForAvatarView(::::)?= =?UTF-8?q?=20protocol=20to=20expose=20avatarView=20&=E2=80=A6=20(#416)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add configureImageForAvatarView(::::) protocol to expose avatarView && remove avatar(:::) * Refactor AvatarView && change configureImageForAvatarView's name to configureAvatarView * Optimize code && fix tests * Add CHANGELOG entry * Revert "Add CHANGELOG entry" This reverts commit 0124ab920b0108190997e50481ecb313b7d0dd46. --- CHANGELOG.md | 19 ++++++++-- .../Sources/ConversationViewController.swift | 9 +++-- Sources/Protocols/MessagesDataSource.swift | 14 ------- .../Protocols/MessagesDisplayDelegate.swift | 15 ++++++++ .../Supporting/MessageKit+Availability.swift | 21 ++++++++++ Sources/Views/AvatarView.swift | 38 ++++++++++--------- .../Cells/MessageCollectionViewCell.swift | 4 +- .../MessagesDisplayDelegateTests.swift | 6 --- Tests/ViewsTests/AvatarViewTests.swift | 2 +- 9 files changed, 80 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7969884..108b32b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,23 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa ## Upcoming release -### Changed -- **Breaking Change** Moved the `handleTapGesture(_ gesture: UIGestureRecognizer)` method from `MessagesCollectionViewCell` to `MessagesCollectionView`. -- [#417](https://github.com/MessageKit/MessageKit/pull/417) by [@zhongwuzw](https://github.com/zhongwuzw). +### Added +- Added `configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)` method in `MessagesDisplayDelegate` `protocol` to configure `avatarView`. +[#416](https://github.com/MessageKit/MessageKit/pull/416) by [@zhongwuzw](https://github.com/zhongwuzw). + +### Removed + +- **Breaking Change** Removed `avatar(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)` method of `MessagesDataSource`, use `configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)` instead. +[#416](https://github.com/MessageKit/MessageKit/pull/416) by [@zhongwuzw](https://github.com/zhongwuzw). + +### Changed + +- **Breaking Change** Moved the `handleTapGesture(_ gesture: UIGestureRecognizer)` method from `MessagesCollectionViewCell` to `MessagesCollectionView`. +[#417](https://github.com/MessageKit/MessageKit/pull/417) by [@zhongwuzw](https://github.com/zhongwuzw). + +- **Breaking Change** Changed `AvatarView` from type `UIView` to type `UIImageView`. + [#417](https://github.com/MessageKit/MessageKit/pull/417) by [@zhongwuzw](https://github.com/zhongwuzw). ## [[Prerelease] 0.12.0](https://github.com/MessageKit/MessageKit/releases/tag/0.12.0) diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index 292caea9..ed3983c5 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -267,10 +267,6 @@ extension ConversationViewController: MessagesDataSource { return messageList[indexPath.section] } - func avatar(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Avatar { - return SampleData.shared.getAvatarFor(sender: message.sender) - } - func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { let name = message.sender.displayName return NSAttributedString(string: name, attributes: [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: .caption1)]) @@ -322,6 +318,11 @@ extension ConversationViewController: MessagesDisplayDelegate { // 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 diff --git a/Sources/Protocols/MessagesDataSource.swift b/Sources/Protocols/MessagesDataSource.swift index b1aad9f0..47c43969 100644 --- a/Sources/Protocols/MessagesDataSource.swift +++ b/Sources/Protocols/MessagesDataSource.swift @@ -51,16 +51,6 @@ public protocol MessagesDataSource: AnyObject { /// - messagesCollectionView: The `MessagesCollectionView` in which the messages will be displayed. func numberOfMessages(in messagesCollectionView: MessagesCollectionView) -> Int - /// The `Avatar` information to be used by the `AvatarView`. - /// - /// - Parameters: - /// - 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. - /// - /// The default value returned by this method is `Avatar()`. - func avatar(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Avatar - /// The attributed text to be used for cell's top label. /// /// - Parameters: @@ -89,10 +79,6 @@ public extension MessagesDataSource { return message.sender == currentSender() } - func avatar(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Avatar { - return Avatar() - } - func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { return nil } diff --git a/Sources/Protocols/MessagesDisplayDelegate.swift b/Sources/Protocols/MessagesDisplayDelegate.swift index d67c1bec..d2bd79b4 100644 --- a/Sources/Protocols/MessagesDisplayDelegate.swift +++ b/Sources/Protocols/MessagesDisplayDelegate.swift @@ -83,6 +83,17 @@ public protocol MessagesDisplayDelegate: AnyObject { /// /// The default value returned by this method is a `MessageFooterView`. func messageFooterView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageFooterView + + /// Configure `AvatarView`‘s image. + /// + /// - Parameters: + /// - avatarView: The `AvatarView` 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. + /// + /// The default image configured by this method is `?`. + func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) // MARK: - Text Messages @@ -193,6 +204,10 @@ public extension MessagesDisplayDelegate { func messageFooterView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageFooterView { return messagesCollectionView.dequeueReusableFooterView(MessageFooterView.self, for: indexPath) } + + func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { + avatarView.initials = "?" + } // MARK: - Text Messages Defaults diff --git a/Sources/Supporting/MessageKit+Availability.swift b/Sources/Supporting/MessageKit+Availability.swift index 71a9dfc0..bf26405b 100644 --- a/Sources/Supporting/MessageKit+Availability.swift +++ b/Sources/Supporting/MessageKit+Availability.swift @@ -62,6 +62,8 @@ extension MessagesLayoutDelegate { } +// MARK: - MessagesCollectionViewFlowLayout + extension MessagesCollectionViewFlowLayout { /// A Boolean value that determines if the `AvatarView` is always on the leading @@ -88,6 +90,25 @@ extension MessagesCollectionViewFlowLayout { } +// MARK: - MessagesDataSource + +extension MessagesDataSource { + /// The `Avatar` information to be used by the `AvatarView`. + /// + /// - Parameters: + /// - 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. + /// + /// The default value returned by this method is `Avatar()`. + @available(*, deprecated: 0.12.1, message: "Removed in MessageKit 0.12.1.") + func avatar(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Avatar { + fatalError("Fatal Error: avatar(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) is no longer supported") + } +} + +// MARK: - MessagesViewController + extension MessagesViewController { /// A Boolean value that determines whether the `MessagesCollectionView` scrolls to the /// bottom on the view's first layout. diff --git a/Sources/Views/AvatarView.swift b/Sources/Views/AvatarView.swift index 96b88684..e392b991 100644 --- a/Sources/Views/AvatarView.swift +++ b/Sources/Views/AvatarView.swift @@ -24,27 +24,25 @@ import Foundation -open class AvatarView: UIView { +open class AvatarView: UIImageView { // MARK: - Properties - - open var avatar: Avatar = Avatar() - - open var imageView = UIImageView() - - public var image: UIImage? { - return imageView.image + + open var initials: String? { + didSet { + setImageFrom(initials: initials) + } } open var placeholderFont: UIFont = UIFont.preferredFont(forTextStyle: .caption1) { didSet { - set(avatar: avatar) + setImageFrom(initials: initials) } } open var placeholderTextColor: UIColor = .white { didSet { - set(avatar: avatar) + setImageFrom(initials: initials) } } @@ -61,14 +59,12 @@ open class AvatarView: UIView { // MARK: - Overridden Properties open override var frame: CGRect { didSet { - imageView.frame = bounds setCorner(radius: self.radius) } } open override var bounds: CGRect { didSet { - imageView.frame = bounds setCorner(radius: self.radius) } } @@ -82,6 +78,11 @@ open class AvatarView: UIView { convenience public init() { self.init(frame: .zero) } + + private func setImageFrom(initials: String?) { + guard let initials = initials else { return } + image = getImageFrom(initials: initials) + } private func getImageFrom(initials: String) -> UIImage { let width = frame.width @@ -160,17 +161,18 @@ open class AvatarView: UIView { contentMode = .scaleAspectFill layer.masksToBounds = true clipsToBounds = true - imageView.contentMode = .scaleAspectFill - imageView.frame = frame - addSubview(imageView) - imageView.image = avatar.image ?? getImageFrom(initials: avatar.initials) setCorner(radius: nil) } // MARK: - Open setters - + open func set(avatar: Avatar) { - imageView.image = avatar.image ?? getImageFrom(initials: avatar.initials) + if let image = avatar.image { + self.image = image + } + else { + initials = avatar.initials + } } open func setCorner(radius: CGFloat?) { diff --git a/Sources/Views/Cells/MessageCollectionViewCell.swift b/Sources/Views/Cells/MessageCollectionViewCell.swift index e0bbbff1..12e1d3d4 100644 --- a/Sources/Views/Cells/MessageCollectionViewCell.swift +++ b/Sources/Views/Cells/MessageCollectionViewCell.swift @@ -101,15 +101,15 @@ open class MessageCollectionViewCell: UICollectionViewCell, CollectionViewReusab let messageColor = displayDelegate.backgroundColor(for: message, at: indexPath, in: messagesCollectionView) let messageStyle = displayDelegate.messageStyle(for: message, at: indexPath, in: messagesCollectionView) + + displayDelegate.configureAvatarView(avatarView, for: message, at: indexPath, in: messagesCollectionView) messageContainerView.backgroundColor = messageColor messageContainerView.style = messageStyle - let avatar = dataSource.avatar(for: message, at: indexPath, in: messagesCollectionView) let topText = dataSource.cellTopLabelAttributedText(for: message, at: indexPath) let bottomText = dataSource.cellBottomLabelAttributedText(for: message, at: indexPath) - avatarView.set(avatar: avatar) cellTopLabel.attributedText = topText cellBottomLabel.attributedText = bottomText } diff --git a/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift b/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift index 859a2fba..9b797698 100644 --- a/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift +++ b/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift @@ -85,12 +85,6 @@ class MessagesDisplayDelegateTests: XCTestCase { XCTAssertEqual(backgroundColor, .clear) } - func testAvatarDefaultState() { - XCTAssertNotNil(sut.dataProvider.avatar(for: sut.dataProvider.messages[0], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView).initials) - } - func testCellTopLabelDefaultState() { XCTAssertNil(sut.dataProvider.cellTopLabelAttributedText(for: sut.dataProvider.messages[0], at: IndexPath(item: 0, section: 0))) diff --git a/Tests/ViewsTests/AvatarViewTests.swift b/Tests/ViewsTests/AvatarViewTests.swift index aeceb264..6b67ef5b 100644 --- a/Tests/ViewsTests/AvatarViewTests.swift +++ b/Tests/ViewsTests/AvatarViewTests.swift @@ -41,7 +41,6 @@ class AvatarViewTests: XCTestCase { } func testNoParams() { - XCTAssertEqual(avatarView.avatar.initials, "?") XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) XCTAssertEqual(avatarView.backgroundColor, UIColor.gray) } @@ -57,6 +56,7 @@ class AvatarViewTests: XCTestCase { func testInitialsOnly() { let avatar = Avatar(initials: "DL") avatarView.set(avatar: avatar) + XCTAssertEqual(avatarView.initials, avatar.initials) XCTAssertEqual(avatar.initials, "DL") XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) XCTAssertEqual(avatarView.backgroundColor, UIColor.gray) From eb1c6f50a631119f62d7f8875fb03221f6e7e789 Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Fri, 22 Dec 2017 23:29:45 +0800 Subject: [PATCH 05/22] Added copy support for message bubble --- CHANGELOG.md | 3 + .../Controllers/MessagesViewController.swift | 72 ++++++++++++++++++- .../Cells/MessageCollectionViewCell.swift | 9 +++ 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 108b32b1..1d97836b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa - Added `configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)` method in `MessagesDisplayDelegate` `protocol` to configure `avatarView`. [#416](https://github.com/MessageKit/MessageKit/pull/416) by [@zhongwuzw](https://github.com/zhongwuzw). +- Added copy support for message bubble. +[#418](https://github.com/MessageKit/MessageKit/pull/418) by [@zhongwuzw](https://github.com/zhongwuzw). + ### Removed - **Breaking Change** Removed `avatar(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)` method of `MessagesDataSource`, use `configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)` instead. diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift index 8d009401..d2f4b7b1 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.swift @@ -59,6 +59,9 @@ open class MessagesViewController: UIViewController { return false } + /// Indicated selected indexPath when handle menu action + fileprivate var selectedIndexPathForMenu: IndexPath? + /// A Boolean value used to determine if `viewDidLayoutSubviews()` has been called. private var isFirstLayout: Bool = true @@ -84,6 +87,7 @@ open class MessagesViewController: UIViewController { setupConstraints() registerReusableViews() setupDelegates() + addMenuControllerObservers() } @@ -100,6 +104,7 @@ open class MessagesViewController: UIViewController { deinit { removeKeyboardObservers() + removeMenuControllerObservers() } // MARK: - Methods [Private] @@ -188,7 +193,40 @@ extension MessagesViewController: UICollectionViewDelegateFlowLayout { let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) return messagesLayoutDelegate.footerViewSize(for: message, at: indexPath, in: messagesCollectionView) } - + + public func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool { + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return false } + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + + switch message.data { + case .text, .attributedText, .emoji, .photo: + selectedIndexPathForMenu = indexPath + return true + default: + return false + } + } + + public func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool { + return (action == NSSelectorFromString("copy:")) + } + + public func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) { + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { fatalError("Please set messagesDataSource") } + let pasteBoard = UIPasteboard.general + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + + switch message.data { + case .text(let text), .emoji(let text): + pasteBoard.string = text + case .attributedText(let attributedText): + pasteBoard.string = attributedText.string + case .photo(let image): + pasteBoard.image = image + default: + break + } + } } // MARK: - UICollectionViewDataSource Conformance @@ -328,3 +366,35 @@ fileprivate extension MessagesViewController { return 0 } } + +// MARK: - Menu Handling +extension MessagesViewController { + /// Add observer for `UIMenuControllerWillShowMenu` notification + fileprivate func addMenuControllerObservers() { + NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(aNotification:)), name: NSNotification.Name.UIMenuControllerWillShowMenu, object: nil) + } + + /// Remove observer for `UIMenuControllerWillShowMenu` notification + fileprivate func removeMenuControllerObservers() { + NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIMenuControllerWillShowMenu, object: nil) + } + + /// Show menuController and set target rect to selected bubble + @objc func menuControllerWillShow(aNotification:Notification) { + if let currentMenuController = aNotification.object as? UIMenuController, + let selectedIndexPath = selectedIndexPathForMenu { + NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIMenuControllerWillShowMenu, object: nil) + defer { + NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(aNotification:)), name: NSNotification.Name.UIMenuControllerWillShowMenu, object: nil) + selectedIndexPathForMenu = nil + } + + currentMenuController.setMenuVisible(false, animated: false) + + guard let selectedCell = messagesCollectionView.cellForItem(at: selectedIndexPath) as? MessageCollectionViewCell else { return } + let selectedCellMessageBubbleFrame = selectedCell.convert(selectedCell.messageContainerView.frame, to: view) + currentMenuController.setTargetRect(selectedCellMessageBubbleFrame, in: view) + currentMenuController.setMenuVisible(true, animated: true) + } + } +} diff --git a/Sources/Views/Cells/MessageCollectionViewCell.swift b/Sources/Views/Cells/MessageCollectionViewCell.swift index 12e1d3d4..bef7e066 100644 --- a/Sources/Views/Cells/MessageCollectionViewCell.swift +++ b/Sources/Views/Cells/MessageCollectionViewCell.swift @@ -131,6 +131,15 @@ open class MessageCollectionViewCell: UICollectionViewCell, CollectionViewReusab break } } + + /// Handle long press gesture, return true when gestureRecognizer's touch point in `messageContainerView`'s frame + open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + let touchPoint = gestureRecognizer.location(in: self) + if gestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) { + return messageContainerView.frame.contains(touchPoint) + } + return false + } /// Handle `ContentView`'s tap gesture, return false when `ContentView` doesn't needs to handle gesture open func cellContentView(canHandle touchPoint: CGPoint) -> Bool { From 611c71dd8a6ef798385f4c0f38afef00b648d992 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Fri, 22 Dec 2017 20:16:16 -0800 Subject: [PATCH 06/22] Image Paste Support for InputTextView --- CHANGELOG.md | 2 + .../Sources/ConversationViewController.swift | 23 +++-- Sources/Views/InputTextView.swift | 88 +++++++++++++++++++ Sources/Views/MessageInputBar.swift | 2 +- 4 files changed, 109 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 108b32b1..101cf480 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa - Added `configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)` method in `MessagesDisplayDelegate` `protocol` to configure `avatarView`. [#416](https://github.com/MessageKit/MessageKit/pull/416) by [@zhongwuzw](https://github.com/zhongwuzw). +- Added `UIImage` paste support to the `InputTextView`. Images can easily be accessed using the `InputTextView.images` property. See the example project for an updated use case. [#423](https://github.com/MessageKit/MessageKit/pull/423) by [@nathantannar4](https://github.com/nathantannar4). + ### Removed - **Breaking Change** Removed `avatar(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)` method of `MessagesDataSource`, use `configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)` instead. diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index ed3983c5..6983836f 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -440,12 +440,25 @@ extension ConversationViewController: MessageLabelDelegate { extension ConversationViewController: MessageInputBarDelegate { func messageInputBar(_ inputBar: MessageInputBar, didPressSendButtonWith text: String) { - let attributedText = NSAttributedString(string: text, attributes: [.font: UIFont.systemFont(ofSize: 8), .foregroundColor: UIColor.blue]) - let id = UUID().uuidString - let message = MockMessage(attributedText: attributedText, sender: currentSender(), messageId: id, date: Date()) - messageList.append(message) + + // Each NSTextAttachment that contains an image will count as one empty character in the text: String + if text.count > inputBar.inputTextView.images.count { + + let attributedText = NSAttributedString(string: text, attributes: [.font: UIFont.systemFont(ofSize: 8), .foregroundColor: UIColor.blue]) + + let message = MockMessage(text: text, sender: currentSender(), messageId: UUID().uuidString, date: Date()) + messageList.append(message) + messagesCollectionView.insertSections([messageList.count - 1]) + } + + for image in inputBar.inputTextView.images { + + let imageMessage = MockMessage(image: image, sender: currentSender(), messageId: UUID().uuidString, date: Date()) + messageList.append(imageMessage) + messagesCollectionView.insertSections([messageList.count - 1]) + } + inputBar.inputTextView.text = String() - messagesCollectionView.insertSections([messageList.count - 1]) messagesCollectionView.scrollToBottom() } diff --git a/Sources/Views/InputTextView.swift b/Sources/Views/InputTextView.swift index 31981171..e7465b93 100644 --- a/Sources/Views/InputTextView.swift +++ b/Sources/Views/InputTextView.swift @@ -49,6 +49,13 @@ open class InputTextView: UITextView { } } + /// The images that are currently stored as NSTextAttachment's + open var images: [UIImage] { + return getAttachedImages() + } + + open var isImagePasteEnabled: Bool = true + /// A UILabel that holds the InputTextView's placeholder text open let placeholderLabel: UILabel = { let label = UILabel() @@ -148,6 +155,7 @@ open class InputTextView: UITextView { layer.cornerRadius = 5.0 layer.borderWidth = 1.25 layer.borderColor = UIColor.lightGray.cgColor + allowsEditingTextAttributes = false setupPlaceholderLabel() } @@ -176,4 +184,84 @@ open class InputTextView: UITextView { placeholderLabelConstraintSet?.left?.constant = placeholderLabelInsets.left placeholderLabelConstraintSet?.right?.constant = -placeholderLabelInsets.right } + + // 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) { + + let font = self.font ?? UIFont.preferredFont(forTextStyle: .body) + let textColor = self.textColor ?? .black + + let attributes: [NSAttributedStringKey:Any] = [ + NSAttributedStringKey.font : font, + NSAttributedStringKey.foregroundColor : textColor, + ] + + // Add the new image as an NSTextAttachment + let attributedImageString = NSAttributedString(attachment: textAttachment(using: image)) + + let newAttributedStingComponent = NSMutableAttributedString(string: "") + newAttributedStingComponent.append(attributedImageString) + newAttributedStingComponent.addAttributes(attributes, range: NSRange(location: 0, length: newAttributedStingComponent.length)) + + // Paste over selected text and advance selectedRange + textStorage.replaceCharacters(in: selectedRange, with: newAttributedStingComponent) + selectedRange = NSRange(location: selectedRange.location + 1, length: 1) + + // 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 getAttachedImages() -> [UIImage] { + + var images = [UIImage]() + let range = NSRange(location: 0, length: attributedText.length) + attributedText.enumerateAttribute(.attachment, in: range, options: [], using: { value, range, stop -> 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 + } + } diff --git a/Sources/Views/MessageInputBar.swift b/Sources/Views/MessageInputBar.swift index c18b804b..d47ae952 100644 --- a/Sources/Views/MessageInputBar.swift +++ b/Sources/Views/MessageInputBar.swift @@ -666,7 +666,7 @@ open class MessageInputBar: UIView { open func textViewDidChange() { let trimmedText = inputTextView.text.trimmingCharacters(in: .whitespacesAndNewlines) - sendButton.isEnabled = !trimmedText.isEmpty + sendButton.isEnabled = !trimmedText.isEmpty || inputTextView.images.count > 0 inputTextView.placeholderLabel.isHidden = !inputTextView.text.isEmpty items.forEach { $0.textViewDidChangeAction(with: inputTextView) } From 959b34eafd7e192fdb0ed6e02720e1fde84a3626 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Sat, 23 Dec 2017 10:06:01 -0800 Subject: [PATCH 07/22] attributedText in Example --- Example/Sources/ConversationViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index 6983836f..8ef058ed 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -446,7 +446,7 @@ extension ConversationViewController: MessageInputBarDelegate { let attributedText = NSAttributedString(string: text, attributes: [.font: UIFont.systemFont(ofSize: 8), .foregroundColor: UIColor.blue]) - let message = MockMessage(text: text, sender: currentSender(), messageId: UUID().uuidString, date: Date()) + let message = MockMessage(attributedText: attributedText, sender: currentSender(), messageId: UUID().uuidString, date: Date()) messageList.append(message) messagesCollectionView.insertSections([messageList.count - 1]) } From 42ea9ef0aa7600c79677f1472ee7c9de939492ae Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Sun, 24 Dec 2017 22:09:24 +0800 Subject: [PATCH 08/22] Fixed `iPhoneX` `MessageInputBar` transparent bottom area --- CHANGELOG.md | 5 +++++ Sources/Views/MessageInputBar.swift | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 108b32b1..9727c10e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa ## Upcoming release +### Fixed + +- Fixed `iPhoneX` `MessageInputBar` transparent bottom area when `keyboardDismissMode` is `interactive`. +[#425](https://github.com/MessageKit/MessageKit/pull/425) by [@zhongwuzw](https://github.com/zhongwuzw). + ### Added - Added `configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)` method in `MessagesDisplayDelegate` `protocol` to configure `avatarView`. diff --git a/Sources/Views/MessageInputBar.swift b/Sources/Views/MessageInputBar.swift index c18b804b..d31cfa5b 100644 --- a/Sources/Views/MessageInputBar.swift +++ b/Sources/Views/MessageInputBar.swift @@ -266,6 +266,7 @@ open class MessageInputBar: UIView { private var bottomStackViewLayoutSet: NSLayoutConstraintSet? private var contentViewLayoutSet: NSLayoutConstraintSet? private var windowAnchor: NSLayoutConstraint? + private var backgroundViewBottomAnchor: NSLayoutConstraint? // MARK: - Initialization @@ -336,7 +337,9 @@ open class MessageInputBar: UIView { // The constraints within the MessageInputBar separatorLine.addConstraints(topAnchor, left: leftAnchor, right: rightAnchor, heightConstant: 1) - backgroundView.addConstraints(topStackView.bottomAnchor, left: leftAnchor, bottom: bottomAnchor, 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), @@ -408,6 +411,7 @@ open class MessageInputBar: UIView { windowAnchor?.constant = -padding.bottom windowAnchor?.priority = UILayoutPriority(rawValue: 750) windowAnchor?.isActive = true + backgroundViewBottomAnchor?.constant = 34 } } } From ac3093e54bc2403f4ab966e566b3906fa0f1aeb0 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Mon, 25 Dec 2017 16:30:44 -0800 Subject: [PATCH 09/22] Support for parsing components in the NSAttributedSting --- .../Sources/ConversationViewController.swift | 28 +++--- Sources/Views/InputTextView.swift | 99 ++++++++++++++++--- 2 files changed, 103 insertions(+), 24 deletions(-) diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index 8ef058ed..38329c87 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -442,20 +442,24 @@ 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 - if text.count > inputBar.inputTextView.images.count { - - let attributedText = NSAttributedString(string: text, attributes: [.font: UIFont.systemFont(ofSize: 8), .foregroundColor: UIColor.blue]) - - let message = MockMessage(attributedText: attributedText, sender: currentSender(), messageId: UUID().uuidString, date: Date()) - messageList.append(message) - messagesCollectionView.insertSections([messageList.count - 1]) - } - for image in inputBar.inputTextView.images { + 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]) + } - let imageMessage = MockMessage(image: image, sender: currentSender(), messageId: UUID().uuidString, date: Date()) - messageList.append(imageMessage) - messagesCollectionView.insertSections([messageList.count - 1]) } inputBar.inputTextView.text = String() diff --git a/Sources/Views/InputTextView.swift b/Sources/Views/InputTextView.swift index e7465b93..98bf9614 100644 --- a/Sources/Views/InputTextView.swift +++ b/Sources/Views/InputTextView.swift @@ -51,7 +51,11 @@ open class InputTextView: UITextView { /// The images that are currently stored as NSTextAttachment's open var images: [UIImage] { - return getAttachedImages() + return parseForAttachedImages() + } + + open var components: [Any] { + return parseForComponents() } open var isImagePasteEnabled: Bool = true @@ -157,6 +161,7 @@ open class InputTextView: UITextView { layer.borderColor = UIColor.lightGray.cgColor allowsEditingTextAttributes = false setupPlaceholderLabel() + setupObservers() } /// Adds the placeholderLabel to the view and sets up its initial constraints @@ -176,6 +181,14 @@ open class InputTextView: UITextView { placeholderLabelConstraintSet?.activate() } + /// 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() { @@ -207,25 +220,36 @@ open class InputTextView: UITextView { /// /// - Parameter image: The image to add private func pasteImageInTextContainer(with image: UIImage) { - - let font = self.font ?? UIFont.preferredFont(forTextStyle: .body) - let textColor = self.textColor ?? .black - + + // The attributes that should be applied to the new NSAttributedString to match the current attributes let attributes: [NSAttributedStringKey:Any] = [ - NSAttributedStringKey.font : font, - NSAttributedStringKey.foregroundColor : textColor, + NSAttributedStringKey.font : font ?? UIFont.preferredFont(forTextStyle: .body), + NSAttributedStringKey.foregroundColor : textColor ?? .black, ] // Add the new image as an NSTextAttachment let attributedImageString = NSAttributedString(attachment: textAttachment(using: image)) - let newAttributedStingComponent = NSMutableAttributedString(string: "") + + let newAttributedStingComponent: NSMutableAttributedString + if attributedText.length == 0 { + newAttributedStingComponent = NSMutableAttributedString(string: "") + } else { + // Add a new line character before the image, this is what iMessage does + newAttributedStingComponent = NSMutableAttributedString(string: "\n") + } newAttributedStingComponent.append(attributedImageString) + + // Add a new line character after the image, this is what iMessage does + newAttributedStingComponent.append(NSAttributedString(string: "\n")) + newAttributedStingComponent.addAttributes(attributes, range: NSRange(location: 0, length: newAttributedStingComponent.length)) - // Paste over selected text and advance selectedRange + // Paste over selected text textStorage.replaceCharacters(in: selectedRange, with: newAttributedStingComponent) - selectedRange = NSRange(location: selectedRange.location + 1, length: 1) + + // Advance the range to the selected range plus the number of characters added + selectedRange = NSRange(location: selectedRange.location + 3, length: 1) // Broadcast a notification to recievers such as the MessageInputBar which will handle resizing NotificationCenter.default.post(name: .UITextViewTextDidChange, object: self) @@ -247,7 +271,7 @@ open class InputTextView: UITextView { /// Returns all images that exist as NSTextAttachment's /// /// - Returns: An array of type UIImage - private func getAttachedImages() -> [UIImage] { + private func parseForAttachedImages() -> [UIImage] { var images = [UIImage]() let range = NSRange(location: 0, length: attributedText.length) @@ -256,7 +280,9 @@ open class InputTextView: UITextView { 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) { + } else if let image = attachment.image(forBounds: attachment.bounds, + textContainer: nil, + characterIndex: range.location) { images.append(image) } } @@ -264,4 +290,53 @@ open class InputTextView: UITextView { 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]() + let range = NSRange(location: 0, length: attributedText.length) + attributedText.enumerateAttributes(in: range, options: []) { (object, range, stop) in + + if object.keys.contains(.attachment){ + if let attachment = object[.attachment] as? NSTextAttachment { + if let image = attachment.image { + components.append(image) + } else if let image = attachment.image(forBounds: attachment.bounds, + textContainer: nil, + characterIndex: range.location) { + components.append(image) + } + } + } else { + let stringValue = attributedText.attributedSubstring(from: range).string.trimmingCharacters(in: .whitespacesAndNewlines) + if !stringValue.isEmpty { + components.append(stringValue) + } + } + } + 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, range, stop -> 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 = CGRect(x: bounds.origin.x, y: bounds.origin.y, + width: newWidth, height: ratio * newWidth) + } + }) + layoutManager.invalidateLayout(forCharacterRange: range, actualCharacterRange: nil) + } + } From 35d57bd100170db08af2d4d126b47536f5db60d8 Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Wed, 27 Dec 2017 21:07:33 -0800 Subject: [PATCH 10/22] Fixes --- Sources/Views/InputTextView.swift | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/Sources/Views/InputTextView.swift b/Sources/Views/InputTextView.swift index 98bf9614..5eea7695 100644 --- a/Sources/Views/InputTextView.swift +++ b/Sources/Views/InputTextView.swift @@ -220,36 +220,32 @@ open class InputTextView: UITextView { /// /// - Parameter image: The image to add private func pasteImageInTextContainer(with image: UIImage) { - - // 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, - ] // Add the new image as an NSTextAttachment let attributedImageString = NSAttributedString(attachment: textAttachment(using: image)) + let isEmpty = attributedText.length == 0 - let newAttributedStingComponent: NSMutableAttributedString - if attributedText.length == 0 { - newAttributedStingComponent = NSMutableAttributedString(string: "") - } else { - // Add a new line character before the image, this is what iMessage does - newAttributedStingComponent = NSMutableAttributedString(string: "\n") - } + // 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)) // Paste over selected text textStorage.replaceCharacters(in: selectedRange, with: newAttributedStingComponent) // Advance the range to the selected range plus the number of characters added - selectedRange = NSRange(location: selectedRange.location + 3, length: 1) + 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) From 7a57b539f29f43492439d9ae81f780f56b7a6614 Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Thu, 28 Dec 2017 18:20:37 +0800 Subject: [PATCH 11/22] Fix inputTextView layout issue when rotated --- Sources/Views/InputTextView.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/Views/InputTextView.swift b/Sources/Views/InputTextView.swift index 5eea7695..9f979f14 100644 --- a/Sources/Views/InputTextView.swift +++ b/Sources/Views/InputTextView.swift @@ -240,8 +240,10 @@ open class InputTextView: UITextView { ] 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) @@ -328,8 +330,7 @@ open class InputTextView: UITextView { // 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 = CGRect(x: bounds.origin.x, y: bounds.origin.y, - width: newWidth, height: ratio * newWidth) + attachment.bounds.size = CGSize(width: newWidth, height: ratio * newWidth) } }) layoutManager.invalidateLayout(forCharacterRange: range, actualCharacterRange: nil) From 7b4461473d95f562acbb22410e5bfd7b4ee5e269 Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Tue, 9 Jan 2018 10:55:50 +0800 Subject: [PATCH 12/22] Optimize menu position in some situations --- CHANGELOG.md | 2 +- .../Controllers/MessagesViewController.swift | 48 ++++++++++++++++--- .../Cells/MessageCollectionViewCell.swift | 6 +-- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d97836b..772c0c26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa - Added `configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView)` method in `MessagesDisplayDelegate` `protocol` to configure `avatarView`. [#416](https://github.com/MessageKit/MessageKit/pull/416) by [@zhongwuzw](https://github.com/zhongwuzw). -- Added copy support for message bubble. +- Added copy support for image, text, and emoji messages. [#418](https://github.com/MessageKit/MessageKit/pull/418) by [@zhongwuzw](https://github.com/zhongwuzw). ### Removed diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift index d2f4b7b1..528ceaf9 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.swift @@ -369,23 +369,30 @@ fileprivate extension MessagesViewController { // MARK: - Menu Handling extension MessagesViewController { + private var navigationBarFrame: CGRect { + guard let navigationController = navigationController, !navigationController.navigationBar.isHidden else { + return .zero + } + return navigationController.navigationBar.frame + } + /// Add observer for `UIMenuControllerWillShowMenu` notification fileprivate func addMenuControllerObservers() { - NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(aNotification:)), name: NSNotification.Name.UIMenuControllerWillShowMenu, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(notification:)), name: .UIMenuControllerWillShowMenu, object: nil) } /// Remove observer for `UIMenuControllerWillShowMenu` notification fileprivate func removeMenuControllerObservers() { - NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIMenuControllerWillShowMenu, object: nil) + NotificationCenter.default.removeObserver(self, name: .UIMenuControllerWillShowMenu, object: nil) } /// Show menuController and set target rect to selected bubble - @objc func menuControllerWillShow(aNotification:Notification) { - if let currentMenuController = aNotification.object as? UIMenuController, + @objc func menuControllerWillShow(notification: Notification) { + if let currentMenuController = notification.object as? UIMenuController, let selectedIndexPath = selectedIndexPathForMenu { - NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIMenuControllerWillShowMenu, object: nil) + NotificationCenter.default.removeObserver(self, name: .UIMenuControllerWillShowMenu, object: nil) defer { - NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(aNotification:)), name: NSNotification.Name.UIMenuControllerWillShowMenu, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(notification:)), name: .UIMenuControllerWillShowMenu, object: nil) selectedIndexPathForMenu = nil } @@ -393,7 +400,34 @@ extension MessagesViewController { guard let selectedCell = messagesCollectionView.cellForItem(at: selectedIndexPath) as? MessageCollectionViewCell else { return } let selectedCellMessageBubbleFrame = selectedCell.convert(selectedCell.messageContainerView.frame, to: view) - currentMenuController.setTargetRect(selectedCellMessageBubbleFrame, in: view) + + var messageInputBarFrame: CGRect = .zero + if let messageInputBarSuperview = messageInputBar.superview { + messageInputBarFrame = view.convert(messageInputBar.frame, from: messageInputBarSuperview) + } + + var topNavigationBarFrame: CGRect = navigationBarFrame + if navigationBarFrame != .zero, let navigationBarSuperview = navigationController?.navigationBar.superview { + topNavigationBarFrame = view.convert(navigationController!.navigationBar.frame, from: navigationBarSuperview) + } + + let menuHeight = currentMenuController.menuFrame.height + + let selectedCellMessageBubblePlusMenuFrame = CGRect(selectedCellMessageBubbleFrame.origin.x, selectedCellMessageBubbleFrame.origin.y - menuHeight, selectedCellMessageBubbleFrame.size.width, selectedCellMessageBubbleFrame.size.height + 2 * menuHeight) + + var targetRect: CGRect = selectedCellMessageBubbleFrame + currentMenuController.arrowDirection = .default + + /// Message bubble intersects with navigationBar and keyboard + if selectedCellMessageBubblePlusMenuFrame.intersects(topNavigationBarFrame) && selectedCellMessageBubblePlusMenuFrame.intersects(messageInputBarFrame) { + let centerY = (selectedCellMessageBubblePlusMenuFrame.intersection(messageInputBarFrame).minY + selectedCellMessageBubblePlusMenuFrame.intersection(topNavigationBarFrame).maxY) / 2 + targetRect = CGRect(selectedCellMessageBubblePlusMenuFrame.midX, centerY, 1, 1) + } /// Message bubble only intersects with navigationBar + else if selectedCellMessageBubblePlusMenuFrame.intersects(topNavigationBarFrame) { + currentMenuController.arrowDirection = .up + } + + currentMenuController.setTargetRect(targetRect, in: view) currentMenuController.setMenuVisible(true, animated: true) } } diff --git a/Sources/Views/Cells/MessageCollectionViewCell.swift b/Sources/Views/Cells/MessageCollectionViewCell.swift index bef7e066..df5627f1 100644 --- a/Sources/Views/Cells/MessageCollectionViewCell.swift +++ b/Sources/Views/Cells/MessageCollectionViewCell.swift @@ -135,10 +135,8 @@ open class MessageCollectionViewCell: UICollectionViewCell, CollectionViewReusab /// Handle long press gesture, return true when gestureRecognizer's touch point in `messageContainerView`'s frame open override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { let touchPoint = gestureRecognizer.location(in: self) - if gestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) { - return messageContainerView.frame.contains(touchPoint) - } - return false + guard gestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) else { return false } + return messageContainerView.frame.contains(touchPoint) } /// Handle `ContentView`'s tap gesture, return false when `ContentView` doesn't needs to handle gesture From faa4f92d1dc2e439c15fe12d840805fc8560710d Mon Sep 17 00:00:00 2001 From: Alessio Arsuffi Date: Wed, 10 Jan 2018 12:01:59 +0100 Subject: [PATCH 13/22] LocationMessageSnapshotOptions public Added public init and public accessors for LocationMessageSnapshotOptions; added test also; --- .../Sources/ConversationViewController.swift | 7 ++++- .../LocationMessageSnapshotOptions.swift | 30 ++++++++++++++----- .../MessagesViewControllerTests.swift | 6 +++- .../MessagesDisplayDelegateTests.swift | 5 +++- .../MessageCollectionViewCellTests.swift | 9 ++++-- 5 files changed, 44 insertions(+), 13 deletions(-) diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index 38329c87..184b5333 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.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 @@ -344,6 +344,11 @@ extension ConversationViewController: MessagesDisplayDelegate { }, completion: nil) } } + + func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions { + + return LocationMessageSnapshotOptions() + } } // MARK: - MessagesLayoutDelegate diff --git a/Sources/Models/LocationMessageSnapshotOptions.swift b/Sources/Models/LocationMessageSnapshotOptions.swift index 98c60565..300019ec 100644 --- a/Sources/Models/LocationMessageSnapshotOptions.swift +++ b/Sources/Models/LocationMessageSnapshotOptions.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 @@ -27,24 +27,38 @@ import MapKit /// An object grouping the settings used by the `MKMapSnapshotter` through the `LocationMessageDisplayDelegate`. public struct LocationMessageSnapshotOptions { + /// Initialize LocationMessageSnapshotOptions with given parameters + /// + /// - Parameters: + /// - showsBuildings: A Boolean value indicating whether the snapshot image should display buildings. + /// - showsPointsOfInterest: A Boolean value indicating whether the snapshot image should display points of interest. + /// - span: The span of the snapshot. + /// - scale: The scale of the snapshot. + public init(showsBuildings: Bool = false, showsPointsOfInterest: Bool = false, span: MKCoordinateSpan = MKCoordinateSpan(latitudeDelta: 0, longitudeDelta: 0) , scale: CGFloat = UIScreen.main.scale) { + self.showsBuildings = showsBuildings + self.showsPointsOfInterest = showsPointsOfInterest + self.span = span + self.scale = scale + } + /// A Boolean value indicating whether the snapshot image should display buildings. /// /// The default value of this property is `false`. - var showsBuildings = false - + public var showsBuildings = false + /// A Boolean value indicating whether the snapshot image should display points of interest. /// /// The default value of this property is `false`. - var showsPointsOfInterest = false - + public var showsPointsOfInterest = false + /// The span of the snapshot. /// /// The default value of this property uses a width of `0` and height of `0`. - var span: MKCoordinateSpan = MKCoordinateSpan(latitudeDelta: 0, longitudeDelta: 0) - + public var span: MKCoordinateSpan = MKCoordinateSpan(latitudeDelta: 0, longitudeDelta: 0) + /// The scale of the snapshot. /// /// The default value of this property uses the `UIScreen.main.scale`. - var scale: CGFloat = UIScreen.main.scale + public var scale: CGFloat = UIScreen.main.scale } diff --git a/Tests/ControllersTest/MessagesViewControllerTests.swift b/Tests/ControllersTest/MessagesViewControllerTests.swift index 981e6870..302fd999 100644 --- a/Tests/ControllersTest/MessagesViewControllerTests.swift +++ b/Tests/ControllersTest/MessagesViewControllerTests.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 @@ -205,4 +205,8 @@ private class MockLayoutDelegate: MessagesLayoutDelegate, MessagesDisplayDelegat func heightForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { return 10.0 } + + func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions { + return LocationMessageSnapshotOptions() + } } diff --git a/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift b/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift index 9b797698..1db060e5 100644 --- a/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift +++ b/Tests/ProtocolsTests/MessagesDisplayDelegateTests.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 @@ -261,4 +261,7 @@ private class MockMessagesViewController: MessagesViewController, MessagesDispla return dataSource } + func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions { + return LocationMessageSnapshotOptions() + } } diff --git a/Tests/ViewsTests/MessageCollectionViewCellTests.swift b/Tests/ViewsTests/MessageCollectionViewCellTests.swift index 943246f0..9771c47f 100644 --- a/Tests/ViewsTests/MessageCollectionViewCellTests.swift +++ b/Tests/ViewsTests/MessageCollectionViewCellTests.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 @@ -76,6 +76,11 @@ class MessageCollectionViewCellTests: XCTestCase { extension MessageCollectionViewCellTests { - fileprivate class MockMessagesDisplayDelegate: MessagesDisplayDelegate { } + fileprivate class MockMessagesDisplayDelegate: MessagesDisplayDelegate { + + func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions { + return LocationMessageSnapshotOptions() + } + } } From 060cd797840fe969f702afe76b1f758050bbef82 Mon Sep 17 00:00:00 2001 From: Alessio Arsuffi Date: Wed, 10 Jan 2018 15:36:50 +0100 Subject: [PATCH 14/22] Fix default values Fix default values --- Sources/Models/LocationMessageSnapshotOptions.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Models/LocationMessageSnapshotOptions.swift b/Sources/Models/LocationMessageSnapshotOptions.swift index 300019ec..ef844240 100644 --- a/Sources/Models/LocationMessageSnapshotOptions.swift +++ b/Sources/Models/LocationMessageSnapshotOptions.swift @@ -44,21 +44,21 @@ public struct LocationMessageSnapshotOptions { /// A Boolean value indicating whether the snapshot image should display buildings. /// /// The default value of this property is `false`. - public var showsBuildings = false + public var showsBuildings: Bool /// A Boolean value indicating whether the snapshot image should display points of interest. /// /// The default value of this property is `false`. - public var showsPointsOfInterest = false + public var showsPointsOfInterest: Bool /// The span of the snapshot. /// /// The default value of this property uses a width of `0` and height of `0`. - public var span: MKCoordinateSpan = MKCoordinateSpan(latitudeDelta: 0, longitudeDelta: 0) + public var span: MKCoordinateSpan /// The scale of the snapshot. /// /// The default value of this property uses the `UIScreen.main.scale`. - public var scale: CGFloat = UIScreen.main.scale + public var scale: CGFloat } From ea9908e617fded9349739a4bb3d1f5289731b00a Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Thu, 11 Jan 2018 11:12:24 +0800 Subject: [PATCH 15/22] Fix message layout issue when sliding back controller --- CHANGELOG.md | 3 +++ Sources/Views/Cells/TextMessageCell.swift | 6 +----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b69b28b1..4176b57f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa ### Fixed +- Fixed message rendering when `MessagesViewController` sliding back. +[#454](https://github.com/MessageKit/MessageKit/pull/454) by [@zhongwuzw](https://github.com/zhongwuzw). + - Fixed `iPhoneX` `MessageInputBar` transparent bottom area when `keyboardDismissMode` is `interactive`. [#425](https://github.com/MessageKit/MessageKit/pull/425) by [@zhongwuzw](https://github.com/zhongwuzw). diff --git a/Sources/Views/Cells/TextMessageCell.swift b/Sources/Views/Cells/TextMessageCell.swift index 0d792828..bd040ee4 100644 --- a/Sources/Views/Cells/TextMessageCell.swift +++ b/Sources/Views/Cells/TextMessageCell.swift @@ -45,6 +45,7 @@ open class TextMessageCell: MessageCollectionViewCell { if let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes { messageLabel.textInsets = attributes.messageLabelInsets messageLabel.font = attributes.messageLabelFont + messageLabel.frame = messageContainerView.bounds } } @@ -57,11 +58,6 @@ open class TextMessageCell: MessageCollectionViewCell { open override func setupSubviews() { super.setupSubviews() messageContainerView.addSubview(messageLabel) - setupConstraints() - } - - open func setupConstraints() { - messageLabel.fillSuperview() } open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) { From 4a8d08e9e3dacb94a512d761457367903384abc5 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Thu, 11 Jan 2018 22:19:38 -0600 Subject: [PATCH 16/22] Separate MessagesViewController functionality into files --- Example/Podfile.lock | 4 +- .../MessagesViewController+DataSource.swift | 98 ++++++ .../MessagesViewController+Delegate.swift | 87 +++++ .../MessagesViewController+Keyboard.swift | 106 ++++++ .../MessagesViewController+Menu.swift | 101 ++++++ .../Controllers/MessagesViewController.swift | 322 ++---------------- 6 files changed, 414 insertions(+), 304 deletions(-) create mode 100644 Sources/Controllers/MessagesViewController+DataSource.swift create mode 100644 Sources/Controllers/MessagesViewController+Delegate.swift create mode 100644 Sources/Controllers/MessagesViewController+Keyboard.swift create mode 100644 Sources/Controllers/MessagesViewController+Menu.swift diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 9fad67d8..bda77b4b 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - MessageKit (0.11.0) + - MessageKit (0.12.0) DEPENDENCIES: - MessageKit (from `../`) @@ -9,7 +9,7 @@ EXTERNAL SOURCES: :path: ../ SPEC CHECKSUMS: - MessageKit: 96caa3ba2c32f9393b406256c5caa29ae9189d71 + MessageKit: 616b81ac09e7ec9627fa47f26f9b9c684b33eab6 PODFILE CHECKSUM: cecdb7bc8129cf99f66de9f68eea3256fec30c3d diff --git a/Sources/Controllers/MessagesViewController+DataSource.swift b/Sources/Controllers/MessagesViewController+DataSource.swift new file mode 100644 index 00000000..b14a6a5e --- /dev/null +++ b/Sources/Controllers/MessagesViewController+DataSource.swift @@ -0,0 +1,98 @@ +/* + MIT License + + Copyright (c) 2017 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 MessagesViewController: UICollectionViewDataSource { + + open func numberOfSections(in collectionView: UICollectionView) -> Int { + guard let collectionView = collectionView as? MessagesCollectionView else { return 0 } + + // Each message is its own section + return collectionView.messagesDataSource?.numberOfMessages(in: collectionView) ?? 0 + } + + open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + guard let collectionView = collectionView as? MessagesCollectionView else { return 0 } + + let messageCount = collectionView.messagesDataSource?.numberOfMessages(in: collectionView) ?? 0 + // There will only ever be 1 message per section + return messageCount > 0 ? 1 : 0 + + } + + open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + + guard let messagesCollectionView = collectionView as? MessagesCollectionView else { + fatalError("Managed collectionView: \(collectionView.debugDescription) is not a MessagesCollectionView.") + } + + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { + fatalError("MessagesDataSource has not been set.") + } + + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + + switch message.data { + case .text, .attributedText, .emoji: + let cell = messagesCollectionView.dequeueReusableCell(TextMessageCell.self, for: indexPath) + cell.configure(with: message, at: indexPath, and: messagesCollectionView) + return cell + case .photo, .video: + let cell = messagesCollectionView.dequeueReusableCell(MediaMessageCell.self, for: indexPath) + cell.configure(with: message, at: indexPath, and: messagesCollectionView) + return cell + case .location: + let cell = messagesCollectionView.dequeueReusableCell(LocationMessageCell.self, for: indexPath) + cell.configure(with: message, at: indexPath, and: messagesCollectionView) + return cell + } + } + + open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + + guard let messagesCollectionView = collectionView as? MessagesCollectionView else { + fatalError("Managed collectionView: \(collectionView.debugDescription) is not a MessagesCollectionView.") + } + + guard let dataSource = messagesCollectionView.messagesDataSource else { + fatalError("MessagesDataSource has not been set.") + } + + guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { + fatalError("MessagesDisplayDelegate has not been set.") + } + + let message = dataSource.messageForItem(at: indexPath, in: messagesCollectionView) + + switch kind { + case UICollectionElementKindSectionHeader: + return displayDelegate.messageHeaderView(for: message, at: indexPath, in: messagesCollectionView) + case UICollectionElementKindSectionFooter: + return displayDelegate.messageFooterView(for: message, at: indexPath, in: messagesCollectionView) + default: + fatalError("Unrecognized element of kind: \(kind)") + } + } +} diff --git a/Sources/Controllers/MessagesViewController+Delegate.swift b/Sources/Controllers/MessagesViewController+Delegate.swift new file mode 100644 index 00000000..5d5c7235 --- /dev/null +++ b/Sources/Controllers/MessagesViewController+Delegate.swift @@ -0,0 +1,87 @@ +/* + MIT License + + Copyright (c) 2017 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 MessagesViewController: UICollectionViewDelegateFlowLayout { + + open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + guard let messagesFlowLayout = collectionViewLayout as? MessagesCollectionViewFlowLayout else { return .zero } + return messagesFlowLayout.sizeForItem(at: indexPath) + } + + open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { + guard let messagesCollectionView = collectionView as? MessagesCollectionView else { return .zero } + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return .zero } + guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { return .zero } + // Could pose a problem if subclass behaviors allows more than one item per section + let indexPath = IndexPath(item: 0, section: section) + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + return messagesLayoutDelegate.headerViewSize(for: message, at: indexPath, in: messagesCollectionView) + } + + open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { + guard let messagesCollectionView = collectionView as? MessagesCollectionView else { return .zero } + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return .zero } + guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { return .zero } + // Could pose a problem if subclass behaviors allows more than one item per section + let indexPath = IndexPath(item: 0, section: section) + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + return messagesLayoutDelegate.footerViewSize(for: message, at: indexPath, in: messagesCollectionView) + } + + open func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool { + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return false } + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + + switch message.data { + case .text, .attributedText, .emoji, .photo: + selectedIndexPathForMenu = indexPath + return true + default: + return false + } + } + + open func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool { + return (action == NSSelectorFromString("copy:")) + } + + open func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) { + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { fatalError("Please set messagesDataSource") } + let pasteBoard = UIPasteboard.general + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) + + switch message.data { + case .text(let text), .emoji(let text): + pasteBoard.string = text + case .attributedText(let attributedText): + pasteBoard.string = attributedText.string + case .photo(let image): + pasteBoard.image = image + default: + break + } + } +} diff --git a/Sources/Controllers/MessagesViewController+Keyboard.swift b/Sources/Controllers/MessagesViewController+Keyboard.swift new file mode 100644 index 00000000..4ab6d518 --- /dev/null +++ b/Sources/Controllers/MessagesViewController+Keyboard.swift @@ -0,0 +1,106 @@ +/* + MIT License + + Copyright (c) 2017 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 + +extension MessagesViewController { + + // MARK: - Register / Unregister Observers + + func addKeyboardObservers() { + NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardDidChangeState), name: .UIKeyboardWillChangeFrame, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(handleTextViewDidBeginEditing), name: .UITextViewTextDidBeginEditing, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(adjustScrollViewInset), name: .UIDeviceOrientationDidChange, object: nil) + } + + 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) + } + + // MARK: - Notification Handlers + + @objc + fileprivate func handleTextViewDidBeginEditing(_ notification: Notification) { + if scrollsToBottomOnKeybordBeginsEditing { + guard let inputTextView = notification.object as? InputTextView, inputTextView === messageInputBar.inputTextView else { return } + messagesCollectionView.scrollToBottom(animated: true) + } + } + + @objc + fileprivate func handleKeyboardDidChangeState(_ notification: Notification) { + guard let keyboardEndFrame = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? CGRect else { return } + + if (keyboardEndFrame.origin.y + keyboardEndFrame.size.height) > UIScreen.main.bounds.height { + // Hardware keyboard is found + messageCollectionViewBottomInset = view.frame.size.height - keyboardEndFrame.origin.y - iPhoneXBottomInset + } else { + //Software keyboard is found + let afterBottomInset = keyboardEndFrame.height > keyboardOffsetFrame.height ? (keyboardEndFrame.height - iPhoneXBottomInset) : keyboardOffsetFrame.height + let differenceOfBottomInset = afterBottomInset - messageCollectionViewBottomInset + let contentOffset = CGPoint(x: messagesCollectionView.contentOffset.x, y: messagesCollectionView.contentOffset.y + differenceOfBottomInset) + + if maintainPositionOnKeyboardFrameChanged { + messagesCollectionView.setContentOffset(contentOffset, animated: false) + } + + messageCollectionViewBottomInset = afterBottomInset + } + } + + @objc + func adjustScrollViewInset() { + if #available(iOS 11.0, *) { + // No need to add to the top contentInset + } else { + let navigationBarInset = navigationController?.navigationBar.frame.height ?? 0 + let statusBarInset: CGFloat = UIApplication.shared.isStatusBarHidden ? 0 : 20 + let topInset = navigationBarInset + statusBarInset + messagesCollectionView.contentInset.top = topInset + messagesCollectionView.scrollIndicatorInsets.top = topInset + } + } + + // MARK: - Helpers + + 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)) + } + + /// 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. + /// + /// - Returns: The safeAreaInsets.bottom if its an iPhoneX, else 0 + fileprivate var iPhoneXBottomInset: CGFloat { + if #available(iOS 11.0, *) { + guard UIScreen.main.nativeBounds.height == 2436 else { return 0 } + return view.safeAreaInsets.bottom + } + return 0 + } +} diff --git a/Sources/Controllers/MessagesViewController+Menu.swift b/Sources/Controllers/MessagesViewController+Menu.swift new file mode 100644 index 00000000..3c9c328b --- /dev/null +++ b/Sources/Controllers/MessagesViewController+Menu.swift @@ -0,0 +1,101 @@ +/* + MIT License + + Copyright (c) 2017 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 + +extension MessagesViewController { + + // MARK: - Register / Unregister Observers + + /// Add observer for `UIMenuControllerWillShowMenu` notification + func addMenuControllerObservers() { + NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(notification:)), name: .UIMenuControllerWillShowMenu, object: nil) + } + + /// Remove observer for `UIMenuControllerWillShowMenu` notification + func removeMenuControllerObservers() { + NotificationCenter.default.removeObserver(self, name: .UIMenuControllerWillShowMenu, object: nil) + } + + // MARK: - Notification Handlers + + /// Show menuController and set target rect to selected bubble + @objc + fileprivate func menuControllerWillShow(notification: Notification) { + + guard let currentMenuController = notification.object as? UIMenuController, + let selectedIndexPath = selectedIndexPathForMenu else { return } + + NotificationCenter.default.removeObserver(self, name: .UIMenuControllerWillShowMenu, object: nil) + defer { + NotificationCenter.default.addObserver(self, + selector: #selector(MessagesViewController.menuControllerWillShow(notification:)), + name: .UIMenuControllerWillShowMenu, object: nil) + selectedIndexPathForMenu = nil + } + + currentMenuController.setMenuVisible(false, animated: false) + + guard let selectedCell = messagesCollectionView.cellForItem(at: selectedIndexPath) as? MessageCollectionViewCell else { return } + let selectedCellMessageBubbleFrame = selectedCell.convert(selectedCell.messageContainerView.frame, to: view) + + var messageInputBarFrame: CGRect = .zero + if let messageInputBarSuperview = messageInputBar.superview { + messageInputBarFrame = view.convert(messageInputBar.frame, from: messageInputBarSuperview) + } + + var topNavigationBarFrame: CGRect = navigationBarFrame + if navigationBarFrame != .zero, let navigationBarSuperview = navigationController?.navigationBar.superview { + topNavigationBarFrame = view.convert(navigationController!.navigationBar.frame, from: navigationBarSuperview) + } + + let menuHeight = currentMenuController.menuFrame.height + + let selectedCellMessageBubblePlusMenuFrame = CGRect(selectedCellMessageBubbleFrame.origin.x, selectedCellMessageBubbleFrame.origin.y - menuHeight, selectedCellMessageBubbleFrame.size.width, selectedCellMessageBubbleFrame.size.height + 2 * menuHeight) + + var targetRect: CGRect = selectedCellMessageBubbleFrame + currentMenuController.arrowDirection = .default + + /// Message bubble intersects with navigationBar and keyboard + if selectedCellMessageBubblePlusMenuFrame.intersects(topNavigationBarFrame) && selectedCellMessageBubblePlusMenuFrame.intersects(messageInputBarFrame) { + let centerY = (selectedCellMessageBubblePlusMenuFrame.intersection(messageInputBarFrame).minY + selectedCellMessageBubblePlusMenuFrame.intersection(topNavigationBarFrame).maxY) / 2 + targetRect = CGRect(selectedCellMessageBubblePlusMenuFrame.midX, centerY, 1, 1) + } /// Message bubble only intersects with navigationBar + else if selectedCellMessageBubblePlusMenuFrame.intersects(topNavigationBarFrame) { + currentMenuController.arrowDirection = .up + } + + currentMenuController.setTargetRect(targetRect, in: view) + currentMenuController.setMenuVisible(true, animated: true) + } + + // MARK: - Helpers + + fileprivate var navigationBarFrame: CGRect { + guard let navigationController = navigationController, !navigationController.navigationBar.isHidden else { + return .zero + } + return navigationController.navigationBar.frame + } +} diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift index 528ceaf9..ca5ab87c 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.swift @@ -1,4 +1,3 @@ - /* MIT License @@ -58,14 +57,14 @@ open class MessagesViewController: UIViewController { open override var shouldAutorotate: Bool { return false } - - /// Indicated selected indexPath when handle menu action - fileprivate var selectedIndexPathForMenu: IndexPath? - + /// A Boolean value used to determine if `viewDidLayoutSubviews()` has been called. private var isFirstLayout: Bool = true - private var messageCollectionViewBottomInset: CGFloat = 0 { + /// Indicated selected indexPath when handle menu action + var selectedIndexPathForMenu: IndexPath? + + var messageCollectionViewBottomInset: CGFloat = 0 { didSet { messagesCollectionView.contentInset.bottom = messageCollectionViewBottomInset messagesCollectionView.scrollIndicatorInsets.bottom = messageCollectionViewBottomInset @@ -76,19 +75,12 @@ open class MessagesViewController: UIViewController { open override func viewDidLoad() { super.viewDidLoad() - - extendedLayoutIncludesOpaqueBars = true - automaticallyAdjustsScrollViewInsets = false - view.backgroundColor = .white - messagesCollectionView.keyboardDismissMode = .interactive - messagesCollectionView.alwaysBounceVertical = true - + setupDefaults() setupSubviews() setupConstraints() registerReusableViews() setupDelegates() addMenuControllerObservers() - } open override func viewDidLayoutSubviews() { @@ -109,15 +101,28 @@ open class MessagesViewController: UIViewController { // MARK: - Methods [Private] + /// Sets the default values for the MessagesViewController + private func setupDefaults() { + extendedLayoutIncludesOpaqueBars = true + automaticallyAdjustsScrollViewInsets = false + view.backgroundColor = .white + messagesCollectionView.keyboardDismissMode = .interactive + messagesCollectionView.alwaysBounceVertical = true + } + /// Sets the delegate and dataSource of the messagesCollectionView property. private func setupDelegates() { messagesCollectionView.delegate = self messagesCollectionView.dataSource = self } + /// Adds the messagesCollectionView to the controllers root view. + private func setupSubviews() { + view.addSubview(messagesCollectionView) + } + /// Registers all cells and supplementary views of the messagesCollectionView property. private func registerReusableViews() { - messagesCollectionView.register(TextMessageCell.self) messagesCollectionView.register(MediaMessageCell.self) messagesCollectionView.register(LocationMessageCell.self) @@ -125,12 +130,6 @@ open class MessagesViewController: UIViewController { messagesCollectionView.register(MessageFooterView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionFooter) messagesCollectionView.register(MessageHeaderView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader) messagesCollectionView.register(MessageDateHeaderView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader) - - } - - /// Adds the messagesCollectionView to the controllers root view. - private func setupSubviews() { - view.addSubview(messagesCollectionView) } /// Sets the constraints of the `MessagesCollectionView`. @@ -150,285 +149,4 @@ open class MessagesViewController: UIViewController { } adjustScrollViewInset() } - - @objc - private func adjustScrollViewInset() { - if #available(iOS 11.0, *) { - // No need to add to the top contentInset - } else { - let navigationBarInset = navigationController?.navigationBar.frame.height ?? 0 - let statusBarInset: CGFloat = UIApplication.shared.isStatusBarHidden ? 0 : 20 - let topInset = navigationBarInset + statusBarInset - messagesCollectionView.contentInset.top = topInset - messagesCollectionView.scrollIndicatorInsets.top = topInset - } - } -} - -// MARK: - UICollectionViewDelegate & UICollectionViewDelegateFlowLayout Conformance - -extension MessagesViewController: UICollectionViewDelegateFlowLayout { - - open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - guard let messagesFlowLayout = collectionViewLayout as? MessagesCollectionViewFlowLayout else { return .zero } - return messagesFlowLayout.sizeForItem(at: indexPath) - } - - open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { - guard let messagesCollectionView = collectionView as? MessagesCollectionView else { return .zero } - guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return .zero } - guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { return .zero } - // Could pose a problem if subclass behaviors allows more than one item per section - let indexPath = IndexPath(item: 0, section: section) - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - return messagesLayoutDelegate.headerViewSize(for: message, at: indexPath, in: messagesCollectionView) - } - - open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { - guard let messagesCollectionView = collectionView as? MessagesCollectionView else { return .zero } - guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return .zero } - guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { return .zero } - // Could pose a problem if subclass behaviors allows more than one item per section - let indexPath = IndexPath(item: 0, section: section) - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - return messagesLayoutDelegate.footerViewSize(for: message, at: indexPath, in: messagesCollectionView) - } - - public func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool { - guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return false } - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - - switch message.data { - case .text, .attributedText, .emoji, .photo: - selectedIndexPathForMenu = indexPath - return true - default: - return false - } - } - - public func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool { - return (action == NSSelectorFromString("copy:")) - } - - public func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) { - guard let messagesDataSource = messagesCollectionView.messagesDataSource else { fatalError("Please set messagesDataSource") } - let pasteBoard = UIPasteboard.general - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - - switch message.data { - case .text(let text), .emoji(let text): - pasteBoard.string = text - case .attributedText(let attributedText): - pasteBoard.string = attributedText.string - case .photo(let image): - pasteBoard.image = image - default: - break - } - } -} - -// MARK: - UICollectionViewDataSource Conformance - -extension MessagesViewController: UICollectionViewDataSource { - - open func numberOfSections(in collectionView: UICollectionView) -> Int { - guard let collectionView = collectionView as? MessagesCollectionView else { return 0 } - - // Each message is its own section - return collectionView.messagesDataSource?.numberOfMessages(in: collectionView) ?? 0 - } - - open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - guard let collectionView = collectionView as? MessagesCollectionView else { return 0 } - - let messageCount = collectionView.messagesDataSource?.numberOfMessages(in: collectionView) ?? 0 - // There will only ever be 1 message per section - return messageCount > 0 ? 1 : 0 - - } - - open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - - guard let messagesCollectionView = collectionView as? MessagesCollectionView else { - fatalError("Managed collectionView: \(collectionView.debugDescription) is not a MessagesCollectionView.") - } - - guard let messagesDataSource = messagesCollectionView.messagesDataSource else { - fatalError("MessagesDataSource has not been set.") - } - - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - - switch message.data { - case .text, .attributedText, .emoji: - let cell = messagesCollectionView.dequeueReusableCell(TextMessageCell.self, for: indexPath) - cell.configure(with: message, at: indexPath, and: messagesCollectionView) - return cell - case .photo, .video: - let cell = messagesCollectionView.dequeueReusableCell(MediaMessageCell.self, for: indexPath) - cell.configure(with: message, at: indexPath, and: messagesCollectionView) - return cell - case .location: - let cell = messagesCollectionView.dequeueReusableCell(LocationMessageCell.self, for: indexPath) - cell.configure(with: message, at: indexPath, and: messagesCollectionView) - return cell - } - } - - open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { - - guard let messagesCollectionView = collectionView as? MessagesCollectionView else { - fatalError("Managed collectionView: \(collectionView.debugDescription) is not a MessagesCollectionView.") - } - - guard let dataSource = messagesCollectionView.messagesDataSource else { - fatalError("MessagesDataSource has not been set.") - } - - guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { - fatalError("MessagesDisplayDelegate has not been set.") - } - - let message = dataSource.messageForItem(at: indexPath, in: messagesCollectionView) - - switch kind { - case UICollectionElementKindSectionHeader: - return displayDelegate.messageHeaderView(for: message, at: indexPath, in: messagesCollectionView) - case UICollectionElementKindSectionFooter: - return displayDelegate.messageFooterView(for: message, at: indexPath, in: messagesCollectionView) - default: - fatalError("Unrecognized element of kind: \(kind)") - } - } -} - -// MARK: - Keyboard Handling - -fileprivate extension MessagesViewController { - - func addKeyboardObservers() { - NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardDidChangeState), name: .UIKeyboardWillChangeFrame, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(handleTextViewDidBeginEditing), name: .UITextViewTextDidBeginEditing, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(adjustScrollViewInset), name: .UIDeviceOrientationDidChange, object: nil) - } - - 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) - } - - @objc - func handleTextViewDidBeginEditing(_ notification: Notification) { - if scrollsToBottomOnKeybordBeginsEditing { - guard let inputTextView = notification.object as? InputTextView, inputTextView === messageInputBar.inputTextView else { return } - messagesCollectionView.scrollToBottom(animated: true) - } - } - - @objc - func handleKeyboardDidChangeState(_ notification: Notification) { - guard let keyboardEndFrame = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? CGRect else { return } - - if (keyboardEndFrame.origin.y + keyboardEndFrame.size.height) > UIScreen.main.bounds.height { - // Hardware keyboard is found - messageCollectionViewBottomInset = view.frame.size.height - keyboardEndFrame.origin.y - iPhoneXBottomInset - } else { - //Software keyboard is found - let afterBottomInset = keyboardEndFrame.height > keyboardOffsetFrame.height ? (keyboardEndFrame.height - iPhoneXBottomInset) : keyboardOffsetFrame.height - let differenceOfBottomInset = afterBottomInset - messageCollectionViewBottomInset - let contentOffset = CGPoint(x: messagesCollectionView.contentOffset.x, y: messagesCollectionView.contentOffset.y + differenceOfBottomInset) - - if maintainPositionOnKeyboardFrameChanged { - messagesCollectionView.setContentOffset(contentOffset, animated: false) - } - - messageCollectionViewBottomInset = afterBottomInset - } - } - - fileprivate 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)) - } - - /// 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 - /// - /// - Returns: The safeAreaInsets.bottom if its an iPhoneX, else 0 - fileprivate var iPhoneXBottomInset: CGFloat { - if #available(iOS 11.0, *) { - guard UIScreen.main.nativeBounds.height == 2436 else { return 0 } - return view.safeAreaInsets.bottom - } - return 0 - } -} - -// MARK: - Menu Handling -extension MessagesViewController { - private var navigationBarFrame: CGRect { - guard let navigationController = navigationController, !navigationController.navigationBar.isHidden else { - return .zero - } - return navigationController.navigationBar.frame - } - - /// Add observer for `UIMenuControllerWillShowMenu` notification - fileprivate func addMenuControllerObservers() { - NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(notification:)), name: .UIMenuControllerWillShowMenu, object: nil) - } - - /// Remove observer for `UIMenuControllerWillShowMenu` notification - fileprivate func removeMenuControllerObservers() { - NotificationCenter.default.removeObserver(self, name: .UIMenuControllerWillShowMenu, object: nil) - } - - /// Show menuController and set target rect to selected bubble - @objc func menuControllerWillShow(notification: Notification) { - if let currentMenuController = notification.object as? UIMenuController, - let selectedIndexPath = selectedIndexPathForMenu { - NotificationCenter.default.removeObserver(self, name: .UIMenuControllerWillShowMenu, object: nil) - defer { - NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(notification:)), name: .UIMenuControllerWillShowMenu, object: nil) - selectedIndexPathForMenu = nil - } - - currentMenuController.setMenuVisible(false, animated: false) - - guard let selectedCell = messagesCollectionView.cellForItem(at: selectedIndexPath) as? MessageCollectionViewCell else { return } - let selectedCellMessageBubbleFrame = selectedCell.convert(selectedCell.messageContainerView.frame, to: view) - - var messageInputBarFrame: CGRect = .zero - if let messageInputBarSuperview = messageInputBar.superview { - messageInputBarFrame = view.convert(messageInputBar.frame, from: messageInputBarSuperview) - } - - var topNavigationBarFrame: CGRect = navigationBarFrame - if navigationBarFrame != .zero, let navigationBarSuperview = navigationController?.navigationBar.superview { - topNavigationBarFrame = view.convert(navigationController!.navigationBar.frame, from: navigationBarSuperview) - } - - let menuHeight = currentMenuController.menuFrame.height - - let selectedCellMessageBubblePlusMenuFrame = CGRect(selectedCellMessageBubbleFrame.origin.x, selectedCellMessageBubbleFrame.origin.y - menuHeight, selectedCellMessageBubbleFrame.size.width, selectedCellMessageBubbleFrame.size.height + 2 * menuHeight) - - var targetRect: CGRect = selectedCellMessageBubbleFrame - currentMenuController.arrowDirection = .default - - /// Message bubble intersects with navigationBar and keyboard - if selectedCellMessageBubblePlusMenuFrame.intersects(topNavigationBarFrame) && selectedCellMessageBubblePlusMenuFrame.intersects(messageInputBarFrame) { - let centerY = (selectedCellMessageBubblePlusMenuFrame.intersection(messageInputBarFrame).minY + selectedCellMessageBubblePlusMenuFrame.intersection(topNavigationBarFrame).maxY) / 2 - targetRect = CGRect(selectedCellMessageBubblePlusMenuFrame.midX, centerY, 1, 1) - } /// Message bubble only intersects with navigationBar - else if selectedCellMessageBubblePlusMenuFrame.intersects(topNavigationBarFrame) { - currentMenuController.arrowDirection = .up - } - - currentMenuController.setTargetRect(targetRect, in: view) - currentMenuController.setMenuVisible(true, animated: true) - } - } } From 810b58d88d8b9d82ada648e7ebfa50fa4ef82aa2 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Thu, 11 Jan 2018 22:42:50 -0600 Subject: [PATCH 17/22] Add MessageKitError to cleanup strings --- .../MessagesViewController+DataSource.swift | 23 ++++++------ .../MessagesViewController+Delegate.swift | 37 +++++++++++++------ Sources/Extensions/Bundle+Extensions.swift | 4 +- .../MessageIntermediateLayoutAttributes.swift | 4 +- .../MessagesCollectionViewFlowLayout.swift | 10 ++--- Sources/Models/MessageKitError.swift | 36 ++++++++++++++++++ .../Protocols/MessagesDisplayDelegate.swift | 4 +- .../Protocols/MessagesLayoutDelegate.swift | 21 ++++++++--- Sources/Views/Cells/LocationMessageCell.swift | 2 +- .../Cells/MessageCollectionViewCell.swift | 4 +- Sources/Views/Cells/TextMessageCell.swift | 2 +- Sources/Views/MessageLabel.swift | 2 +- 12 files changed, 106 insertions(+), 43 deletions(-) create mode 100644 Sources/Models/MessageKitError.swift diff --git a/Sources/Controllers/MessagesViewController+DataSource.swift b/Sources/Controllers/MessagesViewController+DataSource.swift index b14a6a5e..21c3c3ee 100644 --- a/Sources/Controllers/MessagesViewController+DataSource.swift +++ b/Sources/Controllers/MessagesViewController+DataSource.swift @@ -27,29 +27,30 @@ import UIKit extension MessagesViewController: UICollectionViewDataSource { open func numberOfSections(in collectionView: UICollectionView) -> Int { - guard let collectionView = collectionView as? MessagesCollectionView else { return 0 } - + guard let collectionView = collectionView as? MessagesCollectionView else { + fatalError(MessageKitError.notMessagesCollectionView) + } // Each message is its own section return collectionView.messagesDataSource?.numberOfMessages(in: collectionView) ?? 0 } open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - guard let collectionView = collectionView as? MessagesCollectionView else { return 0 } - + guard let collectionView = collectionView as? MessagesCollectionView else { + fatalError(MessageKitError.notMessagesCollectionView) + } let messageCount = collectionView.messagesDataSource?.numberOfMessages(in: collectionView) ?? 0 // There will only ever be 1 message per section return messageCount > 0 ? 1 : 0 - } open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let messagesCollectionView = collectionView as? MessagesCollectionView else { - fatalError("Managed collectionView: \(collectionView.debugDescription) is not a MessagesCollectionView.") + fatalError(MessageKitError.notMessagesCollectionView) } guard let messagesDataSource = messagesCollectionView.messagesDataSource else { - fatalError("MessagesDataSource has not been set.") + fatalError(MessageKitError.nilMessagesDataSource) } let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) @@ -73,15 +74,15 @@ extension MessagesViewController: UICollectionViewDataSource { open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { guard let messagesCollectionView = collectionView as? MessagesCollectionView else { - fatalError("Managed collectionView: \(collectionView.debugDescription) is not a MessagesCollectionView.") + fatalError(MessageKitError.notMessagesCollectionView) } guard let dataSource = messagesCollectionView.messagesDataSource else { - fatalError("MessagesDataSource has not been set.") + fatalError(MessageKitError.nilMessagesDataSource) } guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { - fatalError("MessagesDisplayDelegate has not been set.") + fatalError(MessageKitError.nilMessagesDisplayDelegate) } let message = dataSource.messageForItem(at: indexPath, in: messagesCollectionView) @@ -92,7 +93,7 @@ extension MessagesViewController: UICollectionViewDataSource { case UICollectionElementKindSectionFooter: return displayDelegate.messageFooterView(for: message, at: indexPath, in: messagesCollectionView) default: - fatalError("Unrecognized element of kind: \(kind)") + fatalError(MessageKitError.unrecognizedSectionKind) } } } diff --git a/Sources/Controllers/MessagesViewController+Delegate.swift b/Sources/Controllers/MessagesViewController+Delegate.swift index 5d5c7235..8d5224ab 100644 --- a/Sources/Controllers/MessagesViewController+Delegate.swift +++ b/Sources/Controllers/MessagesViewController+Delegate.swift @@ -32,23 +32,36 @@ extension MessagesViewController: UICollectionViewDelegateFlowLayout { } open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { - guard let messagesCollectionView = collectionView as? MessagesCollectionView else { return .zero } - guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return .zero } - guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { return .zero } + + guard let messagesCollectionView = collectionView as? MessagesCollectionView else { + fatalError(MessageKitError.notMessagesCollectionView) + } + guard let dataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) + } + guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { + fatalError(MessageKitError.nilMessagesLayoutDeleagte) + } // Could pose a problem if subclass behaviors allows more than one item per section let indexPath = IndexPath(item: 0, section: section) - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - return messagesLayoutDelegate.headerViewSize(for: message, at: indexPath, in: messagesCollectionView) + let message = dataSource.messageForItem(at: indexPath, in: messagesCollectionView) + return layoutDelegate.headerViewSize(for: message, at: indexPath, in: messagesCollectionView) } open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { - guard let messagesCollectionView = collectionView as? MessagesCollectionView else { return .zero } - guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return .zero } - guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { return .zero } + guard let messagesCollectionView = collectionView as? MessagesCollectionView else { + fatalError(MessageKitError.notMessagesCollectionView) + } + guard let dataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) + } + guard let layoutDelegate = messagesCollectionView.messagesLayoutDelegate else { + fatalError(MessageKitError.nilMessagesLayoutDeleagte) + } // Could pose a problem if subclass behaviors allows more than one item per section let indexPath = IndexPath(item: 0, section: section) - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - return messagesLayoutDelegate.footerViewSize(for: message, at: indexPath, in: messagesCollectionView) + let message = dataSource.messageForItem(at: indexPath, in: messagesCollectionView) + return layoutDelegate.footerViewSize(for: message, at: indexPath, in: messagesCollectionView) } open func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool { @@ -69,7 +82,9 @@ extension MessagesViewController: UICollectionViewDelegateFlowLayout { } open func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) { - guard let messagesDataSource = messagesCollectionView.messagesDataSource else { fatalError("Please set messagesDataSource") } + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) + } let pasteBoard = UIPasteboard.general let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) diff --git a/Sources/Extensions/Bundle+Extensions.swift b/Sources/Extensions/Bundle+Extensions.swift index 8c8b88af..ff27d1b2 100644 --- a/Sources/Extensions/Bundle+Extensions.swift +++ b/Sources/Extensions/Bundle+Extensions.swift @@ -30,11 +30,11 @@ extension Bundle { let podBundle = Bundle(for: MessagesViewController.self) guard let resourceBundleUrl = podBundle.url(forResource: "MessageKitAssets", withExtension: "bundle") else { - fatalError("MessageKit: Could not create path to the assets bundle") + fatalError(MessageKitError.couldNotCreateAssetsPath) } guard let resourceBundle = Bundle(url: resourceBundleUrl) else { - fatalError("MessageKit: Could not load the assets bundle") + fatalError(MessageKitError.couldNotLoadAssetsBundle) } return resourceBundle diff --git a/Sources/Layout/MessageIntermediateLayoutAttributes.swift b/Sources/Layout/MessageIntermediateLayoutAttributes.swift index 0164db19..d8ede2a0 100644 --- a/Sources/Layout/MessageIntermediateLayoutAttributes.swift +++ b/Sources/Layout/MessageIntermediateLayoutAttributes.swift @@ -52,7 +52,7 @@ final class MessageIntermediateLayoutAttributes { case .cellTrailing: origin.x = cellFrame.width - avatarSize.width case .natural: - fatalError("AvatarPosition Horizontal.natural needs to be resolved.") + fatalError(MessageKitError.avatarPositionUnresolved) } switch avatarPosition.vertical { @@ -91,7 +91,7 @@ final class MessageIntermediateLayoutAttributes { case .cellTrailing: origin.x = cellFrame.width - avatarSize.width - messageContainerSize.width - messageContainerPadding.right case .natural: - fatalError("AvatarPosition Horizontal.natural needs to be resolved.") + fatalError(MessageKitError.avatarPositionUnresolved) } return CGRect(origin: origin, size: messageContainerSize) diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 0ce2aa66..698c2128 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -66,7 +66,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { /// Convenience property for accessing the layout object's `MessagesCollectionView`. fileprivate var messagesCollectionView: MessagesCollectionView { guard let messagesCollectionView = collectionView as? MessagesCollectionView else { - fatalError("MessagesCollectionViewFlowLayout is being used on a foreign type.") + fatalError(MessageKitError.layoutUsedOnForeignType) } return messagesCollectionView } @@ -74,7 +74,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { /// Convenience property for unwrapping the `MessagesCollectionView`'s `MessagesDataSource`. fileprivate var messagesDataSource: MessagesDataSource { guard let messagesDataSource = messagesCollectionView.messagesDataSource else { - fatalError("MessagesDataSource has not been set.") + fatalError(MessageKitError.nilMessagesDataSource) } return messagesDataSource } @@ -82,7 +82,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { /// Convenience property for unwrapping the `MessagesCollectionView`'s `MessagesLayoutDelegate`. fileprivate var messagesLayoutDelegate: MessagesLayoutDelegate { guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { - fatalError("MessagesLayoutDeleagte has not been set.") + fatalError(MessageKitError.nilMessagesLayoutDeleagte) } return messagesLayoutDelegate } @@ -502,7 +502,7 @@ private extension MessagesCollectionViewFlowLayout { return itemWidth - avatarWidth - attributes.messageContainerPadding.right - attributes.bottomLabelHorizontalPadding case (_, .natural): - fatalError("AvatarPosition Horizontal.natural needs to be resolved.") + fatalError(MessageKitError.avatarPositionUnresolved) } } @@ -575,7 +575,7 @@ private extension MessagesCollectionViewFlowLayout { return itemWidth - avatarWidth - attributes.messageContainerPadding.right - attributes.topLabelHorizontalPadding case (_, .natural): - fatalError("AvatarPosition Horizontal.natural needs to be resolved.") + fatalError(MessageKitError.avatarPositionUnresolved) } } diff --git a/Sources/Models/MessageKitError.swift b/Sources/Models/MessageKitError.swift new file mode 100644 index 00000000..f26797c4 --- /dev/null +++ b/Sources/Models/MessageKitError.swift @@ -0,0 +1,36 @@ +/* + MIT License + + Copyright (c) 2017 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. + */ + +enum MessageKitError { + static let avatarPositionUnresolved = "AvatarPosition Horizontal.natural needs to be resolved." + static let nilMessagesDataSource = "MessagesDataSource has not been set." + static let nilMessagesDisplayDelegate = "MessagesDisplayDelegate has not been set." + static let nilMessagesLayoutDeleagte = "MessagesLayoutDelegate has not been set." + static let notMessagesCollectionView = "The collectionView is not a MessagesCollectionView." + static let layoutUsedOnForeignType = "MessagesCollectionViewFlowLayout is being used on a foreign type." + static let unrecognizedSectionKind = "Received unrecognized element kind:" + static let unrecognizedCheckingResult = "Received an unrecognized NSTextCheckingResult.CheckingType" + static let couldNotLoadAssetsBundle = "MessageKit: Could not load the assets bundle" + static let couldNotCreateAssetsPath = "MessageKit: Could not create path to the assets bundle." +} diff --git a/Sources/Protocols/MessagesDisplayDelegate.swift b/Sources/Protocols/MessagesDisplayDelegate.swift index d2bd79b4..d98d186a 100644 --- a/Sources/Protocols/MessagesDisplayDelegate.swift +++ b/Sources/Protocols/MessagesDisplayDelegate.swift @@ -212,7 +212,9 @@ public extension MessagesDisplayDelegate { // MARK: - Text Messages Defaults func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { - guard let dataSource = messagesCollectionView.messagesDataSource else { return .darkText } + guard let dataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) + } return dataSource.isFromCurrentSender(message: message) ? .white : .darkText } diff --git a/Sources/Protocols/MessagesLayoutDelegate.swift b/Sources/Protocols/MessagesLayoutDelegate.swift index 728f242e..30c207bb 100644 --- a/Sources/Protocols/MessagesLayoutDelegate.swift +++ b/Sources/Protocols/MessagesLayoutDelegate.swift @@ -192,7 +192,9 @@ public extension MessagesLayoutDelegate { // MARK: - All Messages Defaults func messagePadding(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets { - guard let dataSource = messagesCollectionView.messagesDataSource else { return .zero } + guard let dataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) + } if dataSource.isFromCurrentSender(message: message) { return UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) } else { @@ -201,12 +203,16 @@ public extension MessagesLayoutDelegate { } func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment { - guard let dataSource = messagesCollectionView.messagesDataSource else { return .cellCenter(.zero) } + guard let dataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) + } return dataSource.isFromCurrentSender(message: message) ? .messageTrailing(.zero) : .messageLeading(.zero) } func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment { - guard let dataSource = messagesCollectionView.messagesDataSource else { return .cellCenter(.zero) } + guard let dataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) + } return dataSource.isFromCurrentSender(message: message) ? .messageLeading(.zero) : .messageTrailing(.zero) } @@ -219,7 +225,9 @@ public extension MessagesLayoutDelegate { } func headerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { - guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { return .zero } + guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { + fatalError(MessageKitError.nilMessagesDisplayDelegate) + } let shouldDisplay = displayDelegate.shouldDisplayHeader(for: message, at: indexPath, in: messagesCollectionView) return shouldDisplay ? CGSize(width: messagesCollectionView.bounds.width, height: 12) : .zero } @@ -235,7 +243,9 @@ public extension MessagesLayoutDelegate { // MARK: - Text Messages Defaults func messageLabelInset(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets { - guard let dataSource = messagesCollectionView.messagesDataSource else { return .zero } + guard let dataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) + } if dataSource.isFromCurrentSender(message: message) { return UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18) } else { @@ -264,5 +274,4 @@ public extension MessagesLayoutDelegate { func widthForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { return maxWidth } - } diff --git a/Sources/Views/Cells/LocationMessageCell.swift b/Sources/Views/Cells/LocationMessageCell.swift index 2368ad4e..d80ee9e4 100644 --- a/Sources/Views/Cells/LocationMessageCell.swift +++ b/Sources/Views/Cells/LocationMessageCell.swift @@ -51,7 +51,7 @@ open class LocationMessageCell: MessageCollectionViewCell { open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) { super.configure(with: message, at: indexPath, and: messagesCollectionView) guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { - fatalError("MessagesDisplayDelegate is not set.") + fatalError(MessageKitError.nilMessagesDisplayDelegate) } let options = displayDelegate.snapshotOptionsForLocation(message: message, at: indexPath, in: messagesCollectionView) let annotationView = displayDelegate.annotationViewForLocation(message: message, at: indexPath, in: messagesCollectionView) diff --git a/Sources/Views/Cells/MessageCollectionViewCell.swift b/Sources/Views/Cells/MessageCollectionViewCell.swift index df5627f1..592313a6 100644 --- a/Sources/Views/Cells/MessageCollectionViewCell.swift +++ b/Sources/Views/Cells/MessageCollectionViewCell.swift @@ -91,10 +91,10 @@ open class MessageCollectionViewCell: UICollectionViewCell, CollectionViewReusab open func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) { guard let dataSource = messagesCollectionView.messagesDataSource else { - fatalError("MessagesDataSource is not set.") + fatalError(MessageKitError.nilMessagesDataSource) } guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { - fatalError("MessagesDisplayDelegate is not set.") + fatalError(MessageKitError.nilMessagesDisplayDelegate) } delegate = messagesCollectionView.messageCellDelegate diff --git a/Sources/Views/Cells/TextMessageCell.swift b/Sources/Views/Cells/TextMessageCell.swift index bd040ee4..4f63c048 100644 --- a/Sources/Views/Cells/TextMessageCell.swift +++ b/Sources/Views/Cells/TextMessageCell.swift @@ -64,7 +64,7 @@ open class TextMessageCell: MessageCollectionViewCell { super.configure(with: message, at: indexPath, and: messagesCollectionView) guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { - fatalError("MessagesDisplayDelegate not set.") + fatalError(MessageKitError.nilMessagesDisplayDelegate) } let textColor = displayDelegate.textColor(for: message, at: indexPath, in: messagesCollectionView) diff --git a/Sources/Views/MessageLabel.swift b/Sources/Views/MessageLabel.swift index fc7a6b63..ff1f2382 100644 --- a/Sources/Views/MessageLabel.swift +++ b/Sources/Views/MessageLabel.swift @@ -283,7 +283,7 @@ open class MessageLabel: UILabel { case .link: return urlAttributes default: - fatalError("Received an unrecognized NSTextCheckingResult.CheckingType") + fatalError(MessageKitError.unrecognizedCheckingResult) } } From 7a112aa4c43df1167e78755435b4c6acbcd43b7c Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Thu, 11 Jan 2018 23:32:35 -0600 Subject: [PATCH 18/22] Fix failing build and copyrights --- MessageKit.xcodeproj/project.pbxproj | 22 ++++++++++- .../MessagesViewController+DataSource.swift | 37 +++++++++++-------- .../MessagesViewController+Delegate.swift | 2 +- .../MessagesViewController+Keyboard.swift | 2 +- .../MessagesViewController+Menu.swift | 2 +- .../Controllers/MessagesViewController.swift | 2 +- Sources/Extensions/CGRect+Extensions.swift | 1 - .../LocationMessageSnapshotOptions.swift | 2 +- Sources/Views/AvatarView.swift | 3 +- Sources/Views/Cells/LocationMessageCell.swift | 2 - .../Cells/MessageCollectionViewCell.swift | 2 +- Sources/Views/InputTextView.swift | 8 ++-- 12 files changed, 53 insertions(+), 32 deletions(-) diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 4c270ff0..e175a155 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -18,6 +18,11 @@ 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 */; }; + 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A320087C82004FD648 /* MessageKitError.swift */; }; + 1FF377A620087D20004FD648 /* MessagesViewController+DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A520087D20004FD648 /* MessagesViewController+DataSource.swift */; }; + 1FF377A820087D56004FD648 /* MessagesViewController+Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A720087D56004FD648 /* MessagesViewController+Delegate.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 */; }; 88916B2D1CF0DF2F00469F91 /* MessageKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 88916B221CF0DF2F00469F91 /* MessageKit.framework */; }; @@ -115,6 +120,11 @@ 1F7FC8C71FD26F49006CC979 /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Nimble.framework; path = Carthage/Build/iOS/Nimble.framework; sourceTree = ""; }; 1F7FC8CB1FD2700B006CC979 /* MessagesViewControllerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewControllerSpec.swift; sourceTree = ""; }; 1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarPosition.swift; sourceTree = ""; }; + 1FF377A320087C82004FD648 /* MessageKitError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageKitError.swift; sourceTree = ""; }; + 1FF377A520087D20004FD648 /* MessagesViewController+DataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+DataSource.swift"; sourceTree = ""; }; + 1FF377A720087D56004FD648 /* MessagesViewController+Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Delegate.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 = ""; }; 88916B221CF0DF2F00469F91 /* MessageKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MessageKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -373,6 +383,7 @@ B7A03F221F866895006AEF79 /* LabelAlignment.swift */, B7A03F1D1F866895006AEF79 /* LocationMessageSnapshotOptions.swift */, B7A03F231F866895006AEF79 /* MessageData.swift */, + 1FF377A320087C82004FD648 /* MessageKitError.swift */, B7A03F1B1F866895006AEF79 /* MessageKitDateFormatter.swift */, B7A03F1F1F866895006AEF79 /* MessageStyle.swift */, B7A03F1A1F866895006AEF79 /* NSConstraintLayoutSet.swift */, @@ -418,6 +429,10 @@ isa = PBXGroup; children = ( B7A03F4E1F86697C006AEF79 /* MessagesViewController.swift */, + 1FF377A520087D20004FD648 /* MessagesViewController+DataSource.swift */, + 1FF377A720087D56004FD648 /* MessagesViewController+Delegate.swift */, + 1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */, + 1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */, ); path = Controllers; sourceTree = ""; @@ -571,12 +586,14 @@ buildActionMask = 2147483647; files = ( B7A03F3C1F866946006AEF79 /* LocationMessageCell.swift in Sources */, + 1FF377AA20087D78004FD648 /* MessagesViewController+Menu.swift in Sources */, 38C57C7C1F9AE4890043CC03 /* InputStackView.swift in Sources */, B7A03F5B1F8669CA006AEF79 /* MessageType.swift in Sources */, B7A03F6F1F8669EB006AEF79 /* String+Extensions.swift in Sources */, B7A03F491F86694F006AEF79 /* InputBarItem.swift in Sources */, B7A03F601F8669CA006AEF79 /* MessagesDisplayDelegate.swift in Sources */, B7A03F5C1F8669CA006AEF79 /* MessageCellDelegate.swift in Sources */, + 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */, B7A03F4A1F86694F006AEF79 /* MessageInputBar.swift in Sources */, B006FA021F99DE2100509C46 /* MessageIntermediateLayoutAttributes.swift in Sources */, B7A03F4B1F86694F006AEF79 /* MessageContainerView.swift in Sources */, @@ -610,14 +627,17 @@ 0EE91E661FDEC888005420A2 /* CGRect+Extensions.swift in Sources */, B7A03F181F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift in Sources */, B7A03F2A1F866895006AEF79 /* MessageStyle.swift in Sources */, + 1FF377A620087D20004FD648 /* MessagesViewController+DataSource.swift in Sources */, B7A03F4D1F86694F006AEF79 /* MessagesCollectionView.swift in Sources */, B7A03F351F866940006AEF79 /* MessageHeaderView.swift in Sources */, B7A03F731F866A06006AEF79 /* MessageKit+Availability.swift in Sources */, B7A03F2D1F866895006AEF79 /* LabelAlignment.swift in Sources */, 38C57C791F9AE3E50043CC03 /* SeparatorLine.swift in Sources */, B7A03F2C1F866895006AEF79 /* DetectorType.swift in Sources */, + 1FF377A820087D56004FD648 /* MessagesViewController+Delegate.swift in Sources */, B7A03F271F866895006AEF79 /* Avatar.swift in Sources */, 1F82D1431FB1B75B00B81A88 /* AvatarPosition.swift in Sources */, + 1FF377AC20087DA2004FD648 /* MessagesViewController+Keyboard.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -627,7 +647,6 @@ files = ( 8962AC8C1F87AB7D0030B058 /* AvatarViewTests.swift in Sources */, 1F066E141FD90BB700E11013 /* MessageLabelSpec.swift in Sources */, - 8962AC971F87AB860030B058 /* DetectorTypeTests.swift in Sources */, 8962AC941F87AB860030B058 /* MessageKitDateFormatterTests.swift in Sources */, 8962AC8E1F87AB7D0030B058 /* InputTextViewTests.swift in Sources */, 8962AC911F87AB860030B058 /* MessagesViewControllerTests.swift in Sources */, @@ -637,7 +656,6 @@ 8962AC8F1F87AB7D0030B058 /* MessageInputBarTests.swift in Sources */, 1F066E131FD90BB600E11013 /* MessagesViewControllerSpec.swift in Sources */, 1F066E1D1FDA3C1700E11013 /* SenderSpec.swift in Sources */, - 1F7FC8CC1FD2700B006CC979 /* MessagesViewControllerSpec.swift in Sources */, 8962AC8A1F87AB7D0030B058 /* MessagesCollectionViewTests.swift in Sources */, 8962AC8D1F87AB7D0030B058 /* MessageCollectionViewCellTests.swift in Sources */, 8962AC991F87AB860030B058 /* MessagesDisplayDelegateTests.swift in Sources */, diff --git a/Sources/Controllers/MessagesViewController+DataSource.swift b/Sources/Controllers/MessagesViewController+DataSource.swift index 21c3c3ee..fc280553 100644 --- a/Sources/Controllers/MessagesViewController+DataSource.swift +++ b/Sources/Controllers/MessagesViewController+DataSource.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 @@ -40,7 +40,7 @@ extension MessagesViewController: UICollectionViewDataSource { } let messageCount = collectionView.messagesDataSource?.numberOfMessages(in: collectionView) ?? 0 // There will only ever be 1 message per section - return messageCount > 0 ? 1 : 0 + return messageCount > 0 ? 2 : 0 } open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { @@ -53,21 +53,28 @@ extension MessagesViewController: UICollectionViewDataSource { fatalError(MessageKitError.nilMessagesDataSource) } + let isMessageCell = indexPath.row == 0 + let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - switch message.data { - case .text, .attributedText, .emoji: - let cell = messagesCollectionView.dequeueReusableCell(TextMessageCell.self, for: indexPath) - cell.configure(with: message, at: indexPath, and: messagesCollectionView) - return cell - case .photo, .video: - let cell = messagesCollectionView.dequeueReusableCell(MediaMessageCell.self, for: indexPath) - cell.configure(with: message, at: indexPath, and: messagesCollectionView) - return cell - case .location: - let cell = messagesCollectionView.dequeueReusableCell(LocationMessageCell.self, for: indexPath) - cell.configure(with: message, at: indexPath, and: messagesCollectionView) - return cell + if isMessageCell { + + switch message.data { + case .text, .attributedText, .emoji: + let cell = messagesCollectionView.dequeueReusableCell(TextMessageCell.self, for: indexPath) + cell.configure(with: message, at: indexPath, and: messagesCollectionView) + return cell + case .photo, .video: + let cell = messagesCollectionView.dequeueReusableCell(MediaMessageCell.self, for: indexPath) + cell.configure(with: message, at: indexPath, and: messagesCollectionView) + return cell + case .location: + let cell = messagesCollectionView.dequeueReusableCell(LocationMessageCell.self, for: indexPath) + cell.configure(with: message, at: indexPath, and: messagesCollectionView) + return cell + } + } else { + return messagesCollectionView.dequeueReusableCell(TextMessageCell.self, for: indexPath) } } diff --git a/Sources/Controllers/MessagesViewController+Delegate.swift b/Sources/Controllers/MessagesViewController+Delegate.swift index 8d5224ab..cd0d27a1 100644 --- a/Sources/Controllers/MessagesViewController+Delegate.swift +++ b/Sources/Controllers/MessagesViewController+Delegate.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 diff --git a/Sources/Controllers/MessagesViewController+Keyboard.swift b/Sources/Controllers/MessagesViewController+Keyboard.swift index 4ab6d518..ef342068 100644 --- a/Sources/Controllers/MessagesViewController+Keyboard.swift +++ b/Sources/Controllers/MessagesViewController+Keyboard.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 diff --git a/Sources/Controllers/MessagesViewController+Menu.swift b/Sources/Controllers/MessagesViewController+Menu.swift index 3c9c328b..93485d10 100644 --- a/Sources/Controllers/MessagesViewController+Menu.swift +++ b/Sources/Controllers/MessagesViewController+Menu.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 diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift index ca5ab87c..35e3d2b2 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.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 diff --git a/Sources/Extensions/CGRect+Extensions.swift b/Sources/Extensions/CGRect+Extensions.swift index 265e98f8..6dec62eb 100644 --- a/Sources/Extensions/CGRect+Extensions.swift +++ b/Sources/Extensions/CGRect+Extensions.swift @@ -31,4 +31,3 @@ extension CGRect { } } - diff --git a/Sources/Models/LocationMessageSnapshotOptions.swift b/Sources/Models/LocationMessageSnapshotOptions.swift index ef844240..21628fcb 100644 --- a/Sources/Models/LocationMessageSnapshotOptions.swift +++ b/Sources/Models/LocationMessageSnapshotOptions.swift @@ -34,7 +34,7 @@ public struct LocationMessageSnapshotOptions { /// - showsPointsOfInterest: A Boolean value indicating whether the snapshot image should display points of interest. /// - span: The span of the snapshot. /// - scale: The scale of the snapshot. - public init(showsBuildings: Bool = false, showsPointsOfInterest: Bool = false, span: MKCoordinateSpan = MKCoordinateSpan(latitudeDelta: 0, longitudeDelta: 0) , scale: CGFloat = UIScreen.main.scale) { + public init(showsBuildings: Bool = false, showsPointsOfInterest: Bool = false, span: MKCoordinateSpan = MKCoordinateSpan(latitudeDelta: 0, longitudeDelta: 0), scale: CGFloat = UIScreen.main.scale) { self.showsBuildings = showsBuildings self.showsPointsOfInterest = showsPointsOfInterest self.span = span diff --git a/Sources/Views/AvatarView.swift b/Sources/Views/AvatarView.swift index e392b991..35b2f8dd 100644 --- a/Sources/Views/AvatarView.swift +++ b/Sources/Views/AvatarView.swift @@ -169,8 +169,7 @@ open class AvatarView: UIImageView { open func set(avatar: Avatar) { if let image = avatar.image { self.image = image - } - else { + } else { initials = avatar.initials } } diff --git a/Sources/Views/Cells/LocationMessageCell.swift b/Sources/Views/Cells/LocationMessageCell.swift index d80ee9e4..0896ca99 100644 --- a/Sources/Views/Cells/LocationMessageCell.swift +++ b/Sources/Views/Cells/LocationMessageCell.swift @@ -99,7 +99,5 @@ open class LocationMessageCell: MessageCollectionViewCell { self.imageView.image = composedImage animationBlock?(self.imageView) } - - } } diff --git a/Sources/Views/Cells/MessageCollectionViewCell.swift b/Sources/Views/Cells/MessageCollectionViewCell.swift index 592313a6..83aceee2 100644 --- a/Sources/Views/Cells/MessageCollectionViewCell.swift +++ b/Sources/Views/Cells/MessageCollectionViewCell.swift @@ -53,7 +53,7 @@ open class MessageCollectionViewCell: UICollectionViewCell, CollectionViewReusab open weak var delegate: MessageCellDelegate? - override public init(frame: CGRect) { + public override init(frame: CGRect) { super.init(frame: frame) contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] setupSubviews() diff --git a/Sources/Views/InputTextView.swift b/Sources/Views/InputTextView.swift index 9f979f14..95f05ae4 100644 --- a/Sources/Views/InputTextView.swift +++ b/Sources/Views/InputTextView.swift @@ -234,9 +234,9 @@ open class InputTextView: UITextView { 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, + 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)) @@ -298,7 +298,7 @@ open class InputTextView: UITextView { let range = NSRange(location: 0, length: attributedText.length) attributedText.enumerateAttributes(in: range, options: []) { (object, range, stop) in - if object.keys.contains(.attachment){ + if object.keys.contains(.attachment) { if let attachment = object[.attachment] as? NSTextAttachment { if let image = attachment.image { components.append(image) From 077b884e153d75d1ff554e72bdb0db3b80232db5 Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Sun, 14 Jan 2018 16:02:00 +0800 Subject: [PATCH 19/22] code clean && recover message handle logic --- .../Sources/ConversationViewController.swift | 6 ++-- .../MessagesViewController+DataSource.swift | 35 ++++++++----------- .../MessagesViewController+Keyboard.swift | 12 +++---- .../MessagesViewController+Menu.swift | 8 ++--- .../MessagesCollectionViewFlowLayout.swift | 2 +- Sources/Models/Sender.swift | 2 +- 6 files changed, 29 insertions(+), 36 deletions(-) diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index 184b5333..ffd9d70c 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -58,17 +58,17 @@ class ConversationViewController: MessagesViewController { maintainPositionOnKeyboardFrameChanged = true // default false messagesCollectionView.addSubview(refreshControl) - refreshControl.addTarget(self, action: #selector(loadMoreMessages), for: .valueChanged) + refreshControl.addTarget(self, action: #selector(ConversationViewController.loadMoreMessages), for: .valueChanged) navigationItem.rightBarButtonItems = [ UIBarButtonItem(image: UIImage(named: "ic_keyboard"), style: .plain, target: self, - action: #selector(handleKeyboardButton)), + action: #selector(ConversationViewController.handleKeyboardButton)), UIBarButtonItem(image: UIImage(named: "ic_typing"), style: .plain, target: self, - action: #selector(handleTyping)) + action: #selector(ConversationViewController.handleTyping)) ] } diff --git a/Sources/Controllers/MessagesViewController+DataSource.swift b/Sources/Controllers/MessagesViewController+DataSource.swift index fc280553..68abe2e0 100644 --- a/Sources/Controllers/MessagesViewController+DataSource.swift +++ b/Sources/Controllers/MessagesViewController+DataSource.swift @@ -40,7 +40,7 @@ extension MessagesViewController: UICollectionViewDataSource { } let messageCount = collectionView.messagesDataSource?.numberOfMessages(in: collectionView) ?? 0 // There will only ever be 1 message per section - return messageCount > 0 ? 2 : 0 + return messageCount > 0 ? 1 : 0 } open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { @@ -53,28 +53,21 @@ extension MessagesViewController: UICollectionViewDataSource { fatalError(MessageKitError.nilMessagesDataSource) } - let isMessageCell = indexPath.row == 0 - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - if isMessageCell { - - switch message.data { - case .text, .attributedText, .emoji: - let cell = messagesCollectionView.dequeueReusableCell(TextMessageCell.self, for: indexPath) - cell.configure(with: message, at: indexPath, and: messagesCollectionView) - return cell - case .photo, .video: - let cell = messagesCollectionView.dequeueReusableCell(MediaMessageCell.self, for: indexPath) - cell.configure(with: message, at: indexPath, and: messagesCollectionView) - return cell - case .location: - let cell = messagesCollectionView.dequeueReusableCell(LocationMessageCell.self, for: indexPath) - cell.configure(with: message, at: indexPath, and: messagesCollectionView) - return cell - } - } else { - return messagesCollectionView.dequeueReusableCell(TextMessageCell.self, for: indexPath) + switch message.data { + case .text, .attributedText, .emoji: + let cell = messagesCollectionView.dequeueReusableCell(TextMessageCell.self, for: indexPath) + cell.configure(with: message, at: indexPath, and: messagesCollectionView) + return cell + case .photo, .video: + let cell = messagesCollectionView.dequeueReusableCell(MediaMessageCell.self, for: indexPath) + cell.configure(with: message, at: indexPath, and: messagesCollectionView) + return cell + case .location: + let cell = messagesCollectionView.dequeueReusableCell(LocationMessageCell.self, for: indexPath) + cell.configure(with: message, at: indexPath, and: messagesCollectionView) + return cell } } diff --git a/Sources/Controllers/MessagesViewController+Keyboard.swift b/Sources/Controllers/MessagesViewController+Keyboard.swift index ef342068..a3535a56 100644 --- a/Sources/Controllers/MessagesViewController+Keyboard.swift +++ b/Sources/Controllers/MessagesViewController+Keyboard.swift @@ -29,9 +29,9 @@ extension MessagesViewController { // MARK: - Register / Unregister Observers func addKeyboardObservers() { - NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardDidChangeState), name: .UIKeyboardWillChangeFrame, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(handleTextViewDidBeginEditing), name: .UITextViewTextDidBeginEditing, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(adjustScrollViewInset), name: .UIDeviceOrientationDidChange, object: nil) + 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) } func removeKeyboardObservers() { @@ -43,7 +43,7 @@ extension MessagesViewController { // MARK: - Notification Handlers @objc - fileprivate func handleTextViewDidBeginEditing(_ notification: Notification) { + private func handleTextViewDidBeginEditing(_ notification: Notification) { if scrollsToBottomOnKeybordBeginsEditing { guard let inputTextView = notification.object as? InputTextView, inputTextView === messageInputBar.inputTextView else { return } messagesCollectionView.scrollToBottom(animated: true) @@ -51,7 +51,7 @@ extension MessagesViewController { } @objc - fileprivate func handleKeyboardDidChangeState(_ notification: Notification) { + private func handleKeyboardDidChangeState(_ notification: Notification) { guard let keyboardEndFrame = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? CGRect else { return } if (keyboardEndFrame.origin.y + keyboardEndFrame.size.height) > UIScreen.main.bounds.height { @@ -96,7 +96,7 @@ extension MessagesViewController { /// for the MessagesCollectionView. /// /// - Returns: The safeAreaInsets.bottom if its an iPhoneX, else 0 - fileprivate var iPhoneXBottomInset: CGFloat { + private var iPhoneXBottomInset: CGFloat { if #available(iOS 11.0, *) { guard UIScreen.main.nativeBounds.height == 2436 else { return 0 } return view.safeAreaInsets.bottom diff --git a/Sources/Controllers/MessagesViewController+Menu.swift b/Sources/Controllers/MessagesViewController+Menu.swift index 93485d10..a1fa9de6 100644 --- a/Sources/Controllers/MessagesViewController+Menu.swift +++ b/Sources/Controllers/MessagesViewController+Menu.swift @@ -30,7 +30,7 @@ extension MessagesViewController { /// Add observer for `UIMenuControllerWillShowMenu` notification func addMenuControllerObservers() { - NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(notification:)), name: .UIMenuControllerWillShowMenu, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(_:)), name: .UIMenuControllerWillShowMenu, object: nil) } /// Remove observer for `UIMenuControllerWillShowMenu` notification @@ -42,7 +42,7 @@ extension MessagesViewController { /// Show menuController and set target rect to selected bubble @objc - fileprivate func menuControllerWillShow(notification: Notification) { + private func menuControllerWillShow(_ notification: Notification) { guard let currentMenuController = notification.object as? UIMenuController, let selectedIndexPath = selectedIndexPathForMenu else { return } @@ -50,7 +50,7 @@ extension MessagesViewController { NotificationCenter.default.removeObserver(self, name: .UIMenuControllerWillShowMenu, object: nil) defer { NotificationCenter.default.addObserver(self, - selector: #selector(MessagesViewController.menuControllerWillShow(notification:)), + selector: #selector(MessagesViewController.menuControllerWillShow(_:)), name: .UIMenuControllerWillShowMenu, object: nil) selectedIndexPathForMenu = nil } @@ -92,7 +92,7 @@ extension MessagesViewController { // MARK: - Helpers - fileprivate var navigationBarFrame: CGRect { + private var navigationBarFrame: CGRect { guard let navigationController = navigationController, !navigationController.navigationBar.isHidden else { return .zero } diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 698c2128..5053c470 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -104,7 +104,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) - NotificationCenter.default.addObserver(self, selector: #selector(handleOrientationChange), name: .UIDeviceOrientationDidChange, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(MessagesCollectionViewFlowLayout.handleOrientationChange(_:)), name: .UIDeviceOrientationDidChange, object: nil) } required public init?(coder aDecoder: NSCoder) { diff --git a/Sources/Models/Sender.swift b/Sources/Models/Sender.swift index 575fe42b..c937ac5f 100644 --- a/Sources/Models/Sender.swift +++ b/Sources/Models/Sender.swift @@ -50,7 +50,7 @@ public struct Sender { extension Sender: Equatable { /// Two senders are considered equal if they have the same id. - static public func == (left: Sender, right: Sender) -> Bool { + public static func == (left: Sender, right: Sender) -> Bool { return left.id == right.id } From 71e644cf626b327f9cee3e9e29d592dcf9a05402 Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Sun, 14 Jan 2018 16:33:41 +0800 Subject: [PATCH 20/22] Fix tests error --- Sources/Views/InputTextView.swift | 6 +++--- Sources/Views/MessageLabel.swift | 2 +- Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/Views/InputTextView.swift b/Sources/Views/InputTextView.swift index 95f05ae4..55f5786a 100644 --- a/Sources/Views/InputTextView.swift +++ b/Sources/Views/InputTextView.swift @@ -273,7 +273,7 @@ open class InputTextView: UITextView { var images = [UIImage]() let range = NSRange(location: 0, length: attributedText.length) - attributedText.enumerateAttribute(.attachment, in: range, options: [], using: { value, range, stop -> Void in + attributedText.enumerateAttribute(.attachment, in: range, options: [], using: { value, range, _ -> Void in if let attachment = value as? NSTextAttachment { if let image = attachment.image { @@ -296,7 +296,7 @@ open class InputTextView: UITextView { var components = [Any]() let range = NSRange(location: 0, length: attributedText.length) - attributedText.enumerateAttributes(in: range, options: []) { (object, range, stop) in + attributedText.enumerateAttributes(in: range, options: []) { (object, range, _) in if object.keys.contains(.attachment) { if let attachment = object[.attachment] as? NSTextAttachment { @@ -324,7 +324,7 @@ open class InputTextView: UITextView { guard images.count > 0 else { return } let range = NSRange(location: 0, length: attributedText.length) - attributedText.enumerateAttribute(.attachment, in: range, options: [], using: { value, range, stop -> Void in + 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 diff --git a/Sources/Views/MessageLabel.swift b/Sources/Views/MessageLabel.swift index ff1f2382..dff0b2ae 100644 --- a/Sources/Views/MessageLabel.swift +++ b/Sources/Views/MessageLabel.swift @@ -412,7 +412,7 @@ open class MessageLabel: UILabel { } -fileprivate enum MessageTextCheckingType { +private enum MessageTextCheckingType { case addressComponents([NSTextCheckingKey: String]?) case date(Date?) case phoneNumber(String?) diff --git a/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift b/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift index 1db060e5..89a575dd 100644 --- a/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift +++ b/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift @@ -212,7 +212,8 @@ class TextMessageDisplayDelegateTests: XCTestCase { } func testTextColorWithoutDataSource_returnsDarkTextForDefault() { - sut.messagesCollectionView.messagesDataSource = nil + let dataSource = sut.makeDataSource() + sut.messagesCollectionView.messagesDataSource = dataSource let textColor = sut.textColor(for: sut.dataProvider.messages[1], at: IndexPath(item: 0, section: 0), in: sut.messagesCollectionView) @@ -249,7 +250,7 @@ private class MockMessagesViewController: MessagesViewController, MessagesDispla } - private func makeDataSource() -> MockMessagesDataSource { + fileprivate func makeDataSource() -> MockMessagesDataSource { let dataSource = MockMessagesDataSource() dataSource.messages.append(MockMessage(text: "Text 1", sender: dataSource.senders[0], From 370c7c6e79c326da8816ae8dfb1452cece0e0406 Mon Sep 17 00:00:00 2001 From: Alessio Arsuffi Date: Sun, 14 Jan 2018 19:43:25 +0100 Subject: [PATCH 21/22] Feature/mock messages count settings (#458) * Added UserDefaults Extension to easily save and fetch settings values; * added messages to fetch in settings Added mock messages count to fetch in settings; * Reverted to com.messagekit.ChatExample * Fix copyright * fix copyright * fix copyright 2 * removed unused mark & fix init(coder:) * remove unused cell height reference --- Example/ChatExample.xcodeproj/project.pbxproj | 8 ++ .../Sources/ConversationViewController.swift | 4 +- Example/Sources/Settings+UserDefaults.swift | 44 ++++++++ Example/Sources/SettingsViewController.swift | 102 +++++++++++++++++- Example/Sources/TableViewCells.swift | 62 +++++++++++ 5 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 Example/Sources/Settings+UserDefaults.swift create mode 100644 Example/Sources/TableViewCells.swift diff --git a/Example/ChatExample.xcodeproj/project.pbxproj b/Example/ChatExample.xcodeproj/project.pbxproj index 557d3e75..863a18e6 100644 --- a/Example/ChatExample.xcodeproj/project.pbxproj +++ b/Example/ChatExample.xcodeproj/project.pbxproj @@ -20,6 +20,8 @@ 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 */ @@ -79,6 +81,8 @@ 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 */ @@ -157,6 +161,8 @@ 882B5E791CF7D53600B6E160 /* Assets.xcassets */, 882B5E7F1CF7D53600B6E160 /* Info.plist */, 882B5E7A1CF7D53600B6E160 /* LaunchScreen.storyboard */, + CAB36EA02007A573009995ED /* TableViewCells.swift */, + CAB36EA22007B1B7009995ED /* Settings+UserDefaults.swift */, ); path = Sources; sourceTree = ""; @@ -485,6 +491,8 @@ 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 */, diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index ffd9d70c..e7b7910b 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -37,8 +37,10 @@ class ConversationViewController: MessagesViewController { override func viewDidLoad() { super.viewDidLoad() + let messagesToFetch = UserDefaults.standard.mockMessagesCount() + DispatchQueue.global(qos: .userInitiated).async { - SampleData.shared.getMessages(count: 10) { messages in + SampleData.shared.getMessages(count: messagesToFetch) { messages in DispatchQueue.main.async { self.messageList = messages self.messagesCollectionView.reloadData() diff --git a/Example/Sources/Settings+UserDefaults.swift b/Example/Sources/Settings+UserDefaults.swift new file mode 100644 index 00000000..620a0241 --- /dev/null +++ b/Example/Sources/Settings+UserDefaults.swift @@ -0,0 +1,44 @@ +/* + MIT License + + Copyright (c) 2017 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 + +extension UserDefaults { + + static let messagesKey = "mockMessages" + + // MARK: - Mock Messages + + func setMockMessages(count: Int) { + set(count, forKey: "mockMessages") + synchronize() + } + + func mockMessagesCount() -> Int { + if let value = object(forKey: "mockMessages") as? Int { + return value + } + return 20 + } +} diff --git a/Example/Sources/SettingsViewController.swift b/Example/Sources/SettingsViewController.swift index 211b36a9..61bb813d 100644 --- a/Example/Sources/SettingsViewController.swift +++ b/Example/Sources/SettingsViewController.swift @@ -27,8 +27,108 @@ import MessageKit final class SettingsViewController: UITableViewController { + // MARK: - Properties + + var selectedMockMessagesCount: Int = 20 + + // MARK: - Picker + + var messagesPicker = UIPickerView() + + @objc func onDoneWithPickerView() { + let selectedMessagesCount = messagesPicker.selectedRow(inComponent: 0) + UserDefaults.standard.setMockMessages(count: selectedMessagesCount) + view.endEditing(false) + tableView.reloadData() + } + + @objc func dismissPickerView() { + view.endEditing(false) + } + + private func configurePickerView() { + messagesPicker.dataSource = self + messagesPicker.delegate = self + messagesPicker.backgroundColor = .white + } + + // MARK: - Toolbar + + var messagesToolbar = UIToolbar() + + private func configureToolbar() { + let doneButton = UIBarButtonItem(title: "Done", style: .plain, target: self, action: #selector(onDoneWithPickerView)) + let spaceButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + let cancelButton = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(dismissPickerView)) + messagesToolbar.items = [cancelButton, spaceButton, doneButton] + messagesToolbar.sizeToFit() + } + + // MARK: - View lifecycle + override func viewDidLoad() { super.viewDidLoad() + tableView.register(TextFieldTableViewCell.self, forCellReuseIdentifier: TextFieldTableViewCell.identifier) + + configurePickerView() + configureToolbar() + } + + // MARK: - TableViewDelegate & TableViewDataSource + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + return indexPath.row == 0 ? configureTextFieldTableViewCell(at: indexPath) : UITableViewCell() + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + let cell = tableView.cellForRow(at: indexPath) + + cell?.contentView.subviews.forEach { + if $0 is UITextField { + $0.becomeFirstResponder() + } + } + } + + // MARK: - Helper + + private func configureTextFieldTableViewCell(at indexPath: IndexPath) -> TextFieldTableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: TextFieldTableViewCell.identifier, for: indexPath) as! TextFieldTableViewCell + cell.mainLabel.text = "Mock messages count:" + + let messagesCount = UserDefaults.standard.mockMessagesCount() + cell.textField.text = "\(messagesCount)" + + cell.textField.inputView = messagesPicker + cell.textField.inputAccessoryView = messagesToolbar + + return cell + } +} + +// MARK: - UIPickerViewDelegate, UIPickerViewDataSource +extension SettingsViewController: UIPickerViewDelegate, UIPickerViewDataSource { + + func numberOfComponents(in pickerView: UIPickerView) -> Int { + return 1 + } + + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + return 100 + } + + 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/Example/Sources/TableViewCells.swift b/Example/Sources/TableViewCells.swift new file mode 100644 index 00000000..b3e114e5 --- /dev/null +++ b/Example/Sources/TableViewCells.swift @@ -0,0 +1,62 @@ +/* + MIT License + + Copyright (c) 2017 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 + +class TextFieldTableViewCell: UITableViewCell { + + static let identifier = "TextFieldTableViewCellIdentifier" + + var mainLabel = UILabel() + var textField = UITextField() + + // MARK: - View lifecycle + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + mainLabel.translatesAutoresizingMaskIntoConstraints = false + textField.translatesAutoresizingMaskIntoConstraints = false + + contentView.addSubview(mainLabel) + contentView.addSubview(textField) + + NSLayoutConstraint.activate([ + mainLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + mainLabel.widthAnchor.constraint(equalToConstant: 200), + mainLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + + textField.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + + textField.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + textField.widthAnchor.constraint(equalToConstant: 50) + ]) + + textField.textAlignment = .right + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} From f9e7172e8abfebe1292283f2e56474d992485e65 Mon Sep 17 00:00:00 2001 From: zhongwuzw Date: Wed, 17 Jan 2018 13:11:07 +0800 Subject: [PATCH 22/22] Release for 0.13.0 --- CHANGELOG.md | 2 ++ MessageKit.podspec | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 370c254a..0e337b37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa ## Upcoming release +## [[Prerelease] 0.13.0](https://github.com/MessageKit/MessageKit/releases/tag/0.13.0) + ### Fixed - Fixed message rendering when `MessagesViewController` sliding back. diff --git a/MessageKit.podspec b/MessageKit.podspec index bc1174dc..440cef6b 100644 --- a/MessageKit.podspec +++ b/MessageKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'MessageKit' - s.version = '0.12.1' + s.version = '0.13.0' s.license = { :type => "MIT", :file => "LICENSE.md" } s.summary = 'An elegant messages UI library for iOS.'