diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e0b1329..b1ae3f1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,15 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa -------------------------------------- -## Upcoming release +## [2.0.0-beta.1](https://github.com/MessageKit/MessageKit/releases/tag/2.0.0-beta.1) ### Changed +- **Breaking Change** Updated codebase to Swift 4.2 [#883](https://github.com/MessageKit/MessageKit/pull/883) by [@nathantannar4](https://github.com/nathantannar4) + - Fixed the way that the Strings and UIImages are parsed in the `InputTextView` to prevent crashes in `parseForComponents()`. [#791](https://github.com/MessageKit/MessageKit/pull/791) by [@nathantannar4](https://github.com/nathantannar4) -- **Breaking Change** Updated codebase to Swift 4.2 [#883](https://github.com/MessageKit/MessageKit/pull/883) by [@nathantannar4](https://github.com/nathantannar4) - ### Added - **Breaking Change** Added `.hashtag`, .`mention` to detect theses pattern inside the `messageLabel`. We also add `.custom(pattern: YOUR_PATTERN)` to `DetectorType` to manage and deal with your own regular expression. @@ -21,17 +21,26 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa - Added support for detection and handling of `NSLink`s inside of messages. [#815](https://github.com/MessageKit/MessageKit/pull/815) by [@jnic](https://github.com/jnic) -- Added customizable `accessoryView`, with a new `MessagesDisplayDelegate` function `configureAccessoryView`, and corresponding size & padding properties in `MessageSizeCalculator`. +- Added customizable `accessoryView`, with a new `MessagesDisplayDelegate` function `configureAccessoryView`, and corresponding size & padding properties in `MessageSizeCalculator`. The `accessoryView` is aligned to the center of the `messageContainerView`. [#710](https://github.com/MessageKit/MessageKit/pull/710) by [@hyouuu](https://github.com/hyouuu) +- Added a tap gesture recognition to the `accessoryView` which calls the `MessageCellDelagate` function `didTapAccessoryView(in:)`. +[#834](https://github.com/MessageKit/MessageKit/pull/834) by [@nathantannar4](https://github.com/nathantannar4) + - Added `additionalBottomInset` property that allows to adjust the bottom content inset automatically set on the messages collection view by the view controller. [#787](https://github.com/MessageKit/MessageKit/pull/787) by [@andreyvit](https://github.com/andreyvit) +- Added new methods to simplify using of custom messages: `customCellSizeCalculator(for:at:in:)` for `MessagesLayoutDelegate` and `customCell(for:at:in:)` for `MessagesDataSource`. +[#879](https://github.com/MessageKit/MessageKit/pull/879) by [@realbonus](https://github.com/RealBonus) + ### Fixed - **Breaking Change** Fixed typo of `scrollsToBottomOnKeybordBeginsEditing` to `scrollsToBottomOnKeyboardBeginsEditing`. [#856](https://github.com/MessageKit/MessageKit/pull/856) by [@p-petrenko](https://github.com/p-petrenko) +- Fixed a bug that prevented `MessageLabel` from laying out properly when contained by superviews using autolayout. +[#889](https://github.com/MessageKit/MessageKit/pull/889) by [@marius-serban](https://github.com/marius-serban). + - Fixed bottom content inset adjustment when using an undocked keyboard on iPad, or when `edgesForExtendedLayout` does not include `.top`, or when a parent container view controller adds extra views at the top of the screen. [#787](https://github.com/MessageKit/MessageKit/pull/787) by [@andreyvit](https://github.com/andreyvit) @@ -43,6 +52,9 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa - Fixed `MessagesCollectionView` to allow to use nibs with `MessageReusableView`. [#832](https://github.com/MessageKit/MessageKit/pull/832) by [@maxxx777](https://github.com/maxxx777). +- Fixed multiple crashes at views, when views are being called from another XIB. +[#905](https://github.com/MessageKit/MessageKit/pull/905) by [@talanov](https://github.com/talanov). + ## [1.0.0](https://github.com/MessageKit/MessageKit/releases/tag/1.0.0) - First major release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6b37d5ae..eb2c5b40 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ ### Code of Conduct -Please read our [Code of Conduct](https://github.com/MessageKit/MessageKit/blob/master/Code_of_Conduct.md). +Please read our [Code of Conduct](https://github.com/MessageKit/MessageKit/blob/master/CODE_OF_CONDUCT.md). The MessageKit maintainers take this Code of Conduct very seriously. Intolerance, disrespect, harassment, and any form of negativity will not be tolerated. ### Ways to Contribute diff --git a/Documentation/FAQs.md b/Documentation/FAQs.md index a112794d..5ed0760f 100644 --- a/Documentation/FAQs.md +++ b/Documentation/FAQs.md @@ -1,6 +1,13 @@ # MessageKit Frequently Asked Questions -**Why doesn't the `MessageInputBar` appear in my controller?** +- [Why doesn't the `MessageInputBar` appear in my controller?](#why-doesnt-the-messageinputbar-appear-in-my-controller) +- [How can I remove the `AvatarView` from the cell?](#how-can-i-remove-the-avatarview-from-the-cell) +- [How can I move the `AvatarView` to prevent it from overlapping text in the `MessageBottomLabel` or `CellTopLabel`?](#how-can-i-move-the-avatarview-to-prevent-it-from-overlapping-text-in-the-messagebottomlabel-or-celltoplabel) +- [How can I dismiss the keyboard?](#how-can-i-dismiss-the-keyboard) +- [How can I get a reference to the `MessageType` in the `MessageCellDelegate` methods?](#how-can-i-get-a-reference-to-the-messagetype-in-the-messagecelldelegate-methods) + + +## Why doesn't the `MessageInputBar` appear in my controller? If you're using the `MessagesViewController` as a child view controller then you have to call `becomeFirstResponder()` on your view controller. The @@ -21,7 +28,7 @@ class ParentVC: UIViewController { } ``` -**How can I remove the `AvatarView` from the cell?** +## How can I remove the `AvatarView` from the cell? You can set the `AvatarView` to hidden through the `configureAvatarView(_:AvatarView,for:MessageType,at:IndexPath,in:MessagesCollectionView)` method of `MessagesDisplayDelegate`. @@ -52,7 +59,7 @@ if let layout = messagesCollectionView.collectionViewLayout as? MessagesCollecti } ``` -**How can I move the `AvatarView` to prevent it from overlapping text in the `MessageBottomLabel` or `CellTopLabel`?** +## How can I move the `AvatarView` to prevent it from overlapping text in the `MessageBottomLabel` or `CellTopLabel`? If you have resized the `AvatarView` to be larger than the default size in MessageKit then you may notice that the `AvatarView` overlaps text either in the `MessageBottomLabel` or `CallTopLabel`. MessageKit allows the `AvatarView` @@ -75,7 +82,7 @@ if let layout = messagesCollectionView.collectionViewLayout as? MessagesCollecti There are other options provided as well so take a look at the `AvatarPosition` struct to see what they are. -**How can I dismiss the keyboard?** +## How can I dismiss the keyboard? The `MessagesViewController` needs to resign the first responder. @@ -84,7 +91,7 @@ let controller = MessagesViewController() controller.resignFirstResponder() ``` -**How can I get a reference to the `MessageType` in the `MessageCellDelegate` methods?** +## How can I get a reference to the `MessageType` in the `MessageCellDelegate` methods? You can use `UICollectionView`'s method `indexPath(for: UICollectionViewCell)` to get the `IndexPath` for the `MessageCollectionViewCell` argument. Using this `IndexPath`, you can diff --git a/Example/Podfile.lock b/Example/Podfile.lock index a6ab60f8..6d8fef54 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,7 +1,7 @@ PODS: - - MessageInputBar (0.4.0): - - MessageInputBar/Core (= 0.4.0) - - MessageInputBar/Core (0.4.0) + - MessageInputBar (0.4.1): + - MessageInputBar/Core (= 0.4.1) + - MessageInputBar/Core (0.4.1) - MessageKit (1.0.0): - MessageInputBar/Core @@ -18,11 +18,11 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: MessageInputBar: - :commit: 0d2ea1b85a708f37d6b720a3b316e63a39ace4b8 + :commit: faebe27f2dd8f39ea145e75b7296ef48133c099a :git: https://github.com/MessageKit/MessageInputBar.git SPEC CHECKSUMS: - MessageInputBar: d46ecc5b355d13433b8367dcd200e05a6564f0ed + MessageInputBar: e81c7535347f1f7b923de7080409a535a004b6e4 MessageKit: 2bbd13dd6a7c06f42f2d13ed8871d1fe5383b477 PODFILE CHECKSUM: 04c1a805e1997e83bacab1a34787e71e9fe4432b diff --git a/Example/Sources/AppDelegate.swift b/Example/Sources/AppDelegate.swift index 5f31cfe2..ea28caaa 100644 --- a/Example/Sources/AppDelegate.swift +++ b/Example/Sources/AppDelegate.swift @@ -35,6 +35,11 @@ final internal class AppDelegate: UIResponder, UIApplicationDelegate { window?.rootViewController = NavigationController(rootViewController: LaunchViewController()) window?.makeKeyAndVisible() + if UserDefaults.isFirstLaunch() { + // Enable Text Messages + UserDefaults.standard.set(true, forKey: "Text Messages") + } + return true } diff --git a/Example/Sources/Data Generation/SampleData.swift b/Example/Sources/Data Generation/SampleData.swift index 50b3dfa3..14b24ed4 100644 --- a/Example/Sources/Data Generation/SampleData.swift +++ b/Example/Sources/Data Generation/SampleData.swift @@ -30,6 +30,28 @@ final internal class SampleData { static let shared = SampleData() private init() {} + + enum MessageTypes: UInt32, CaseIterable { + case Text = 0 + case AttributedText = 1 + case Photo = 2 + case Video = 3 + case Emoji = 4 + case Location = 5 + case Url = 6 + case Phone = 7 + case Hashtag = 8 + case Mention = 9 + case Custom = 10 + + static func random() -> MessageTypes { + // Update as new enumerations are added + let maxValue = Custom.rawValue + + let rand = arc4random_uniform(maxValue+1) + return MessageTypes(rawValue: rand)! + } + } let system = Sender(id: "000000", displayName: "System") let nathan = Sender(id: "000001", displayName: "Nathan Tannar") @@ -46,8 +68,6 @@ final internal class SampleData { let messageImages: [UIImage] = [#imageLiteral(resourceName: "img1"), #imageLiteral(resourceName: "img2")] - let messageTypes = ["Text", "Text", "Text", "AttributedText", "Location", "Photo", "Emoji", "Video", "URL", "Phone", "Mention", "Hashtag", "Custom"] - let emojis = [ "👍", "😂😂😂", @@ -116,55 +136,66 @@ final internal class SampleData { return date } } + + func randomMessageType() -> MessageTypes { + let messageType = MessageTypes.random() - func randomMessage(isCustomEnabled: Bool, allowedSenders: [Sender]) -> MockMessage { + if !UserDefaults.standard.bool(forKey: "\(messageType)" + " Messages") { + return randomMessageType() + } + + return messageType + } + + func randomMessage(allowedSenders: [Sender]) -> MockMessage { let randomNumberSender = Int(arc4random_uniform(UInt32(allowedSenders.count))) - let randomNumberImage = Int(arc4random_uniform(UInt32(messageImages.count))) - let randomMessageType = Int(arc4random_uniform(UInt32(messageTypes.count - (isCustomEnabled ? 0 : 1)))) - let randomNumberLocation = Int(arc4random_uniform(UInt32(locations.count))) - let randomNumberEmoji = Int(arc4random_uniform(UInt32(emojis.count))) - let randomSentance = Lorem.sentence() let uniqueID = NSUUID().uuidString let sender = allowedSenders[randomNumberSender] let date = dateAddingRandomTime() - switch messageTypes[randomMessageType] { - case "Text": - return MockMessage(text: randomSentance, sender: sender, messageId: uniqueID, date: date) - case "AttributedText": - let attributedText = attributedString(with: randomSentance) + switch randomMessageType() { + case .Text: + let randomSentence = Lorem.sentence() + return MockMessage(text: randomSentence, sender: sender, messageId: uniqueID, date: date) + case .AttributedText: + let randomSentence = Lorem.sentence() + let attributedText = attributedString(with: randomSentence) return MockMessage(attributedText: attributedText, sender: senders[randomNumberSender], messageId: uniqueID, date: date) - case "Photo": + case .Photo: + let randomNumberImage = Int(arc4random_uniform(UInt32(messageImages.count))) let image = messageImages[randomNumberImage] return MockMessage(image: image, sender: sender, messageId: uniqueID, date: date) - case "Video": + case .Video: + let randomNumberImage = Int(arc4random_uniform(UInt32(messageImages.count))) let image = messageImages[randomNumberImage] return MockMessage(thumbnail: image, sender: sender, messageId: uniqueID, date: date) - case "Emoji": + case .Emoji: + let randomNumberEmoji = Int(arc4random_uniform(UInt32(emojis.count))) return MockMessage(emoji: emojis[randomNumberEmoji], sender: sender, messageId: uniqueID, date: date) - case "Location": + case .Location: + let randomNumberLocation = Int(arc4random_uniform(UInt32(locations.count))) return MockMessage(location: locations[randomNumberLocation], sender: sender, messageId: uniqueID, date: date) - case "URL": + case .Url: return MockMessage(text: "https://github.com/MessageKit", sender: sender, messageId: uniqueID, date: date) - case "Phone": + case .Phone: return MockMessage(text: "123-456-7890", sender: sender, messageId: uniqueID, date: date) - case "Mention": + case .Mention: return MockMessage(text: "@messagekit", sender: sender, messageId: uniqueID, date: date) - case "Hashtag": + case .Hashtag: return MockMessage(text: "#messagekit", sender: sender, messageId: uniqueID, date: date) - case "Custom": + case .Custom: return MockMessage(custom: "Someone left the conversation", sender: system, messageId: uniqueID, date: date) - default: - fatalError("Unrecognized mock message type") } } func getMessages(count: Int, completion: ([MockMessage]) -> Void) { var messages: [MockMessage] = [] + // Disable Custom Messages + UserDefaults.standard.set(false, forKey: "Custom Messages") for _ in 0.. Void) { var messages: [MockMessage] = [] + // Enable Custom Messages + UserDefaults.standard.set(true, forKey: "Custom Messages") for _ in 0.. Void) { var messages: [MockMessage] = [] + // Disable Custom Messages + UserDefaults.standard.set(false, forKey: "Custom Messages") for _ in 0.. Bool { + let hasBeenLaunchedBeforeFlag = "hasBeenLaunchedBeforeFlag" + let isFirstLaunch = !UserDefaults.standard.bool(forKey: hasBeenLaunchedBeforeFlag) + if isFirstLaunch { + UserDefaults.standard.set(true, forKey: hasBeenLaunchedBeforeFlag) + UserDefaults.standard.synchronize() + } + return isFirstLaunch + } } diff --git a/Example/Sources/View Controllers/AdvancedExampleViewController.swift b/Example/Sources/View Controllers/AdvancedExampleViewController.swift index 3f3c8432..29587813 100644 --- a/Example/Sources/View Controllers/AdvancedExampleViewController.swift +++ b/Example/Sources/View Controllers/AdvancedExampleViewController.swift @@ -98,6 +98,11 @@ final class AdvancedExampleViewController: ChatViewController { layout?.setMessageIncomingAvatarSize(CGSize(width: 30, height: 30)) layout?.setMessageIncomingMessagePadding(UIEdgeInsets(top: -outgoingAvatarOverlap, left: -18, bottom: outgoingAvatarOverlap, right: 18)) + layout?.setMessageIncomingAccessoryViewSize(CGSize(width: 30, height: 30)) + layout?.setMessageIncomingAccessoryViewPadding(HorizontalEdgeInsets(left: 8, right: 0)) + layout?.setMessageOutgoingAccessoryViewSize(CGSize(width: 30, height: 30)) + layout?.setMessageOutgoingAccessoryViewPadding(HorizontalEdgeInsets(left: 0, right: 8)) + messagesCollectionView.messagesLayoutDelegate = self messagesCollectionView.messagesDisplayDelegate = self } @@ -117,6 +122,10 @@ final class AdvancedExampleViewController: ChatViewController { messageInputBar.inputTextView.layer.cornerRadius = 16.0 messageInputBar.inputTextView.layer.masksToBounds = true messageInputBar.inputTextView.scrollIndicatorInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0) + configureInputBarItems() + } + + private func configureInputBarItems() { messageInputBar.setRightStackViewWidthConstant(to: 36, animated: false) messageInputBar.sendButton.imageView?.backgroundColor = UIColor(white: 0.85, alpha: 1) messageInputBar.sendButton.contentEdgeInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) @@ -136,14 +145,16 @@ final class AdvancedExampleViewController: ChatViewController { item.title = "\(textView.text.count)/140" let isOverLimit = textView.text.count > 140 item.messageInputBar?.shouldManageSendButtonEnabledState = !isOverLimit // Disable automated management when over limit - item.messageInputBar?.sendButton.isEnabled = !isOverLimit + if isOverLimit { + item.messageInputBar?.sendButton.isEnabled = false + } let color = isOverLimit ? .red : UIColor(white: 0.6, alpha: 1) item.setTitleColor(color, for: .normal) } let bottomItems = [makeButton(named: "ic_at"), makeButton(named: "ic_hashtag"), makeButton(named: "ic_library"), .flexibleSpace, charCountButton] messageInputBar.textViewPadding.bottom = 8 messageInputBar.setStackViewItems(bottomItems, forStack: .bottom, animated: false) - + // This just adds some more flare messageInputBar.sendButton .onEnabled { item in @@ -180,7 +191,7 @@ final class AdvancedExampleViewController: ChatViewController { // self?.messagesCollectionView.scrollToBottom(animated: true) // } // } - messagesCollectionView.scrollToBottom(animated: true) +// messagesCollectionView.scrollToBottom(animated: true) } private func makeButton(named: String) -> InputBarButtonItem { @@ -315,6 +326,19 @@ extension AdvancedExampleViewController: MessagesDisplayDelegate { avatarView.layer.borderColor = UIColor.primaryColor.cgColor } + func configureAccessoryView(_ accessoryView: UIView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { + // Cells are reused, so only add a button here once. For real use you would need to + // ensure any subviews are removed if not needed + guard accessoryView.subviews.isEmpty else { return } + let button = UIButton(type: .infoLight) + button.tintColor = .primaryColor + accessoryView.addSubview(button) + button.frame = accessoryView.bounds + button.isUserInteractionEnabled = false // respond to accessoryView tap through `MessageCellDelegate` + accessoryView.layer.cornerRadius = accessoryView.frame.height / 2 + accessoryView.backgroundColor = UIColor.primaryColor.withAlphaComponent(0.3) + } + // MARK: - Location Messages func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? { diff --git a/Example/Sources/View Controllers/ChatViewController.swift b/Example/Sources/View Controllers/ChatViewController.swift index c7f26b6f..a8de9166 100644 --- a/Example/Sources/View Controllers/ChatViewController.swift +++ b/Example/Sources/View Controllers/ChatViewController.swift @@ -133,18 +133,7 @@ class ChatViewController: MessagesViewController, MessagesDataSource { let lastIndexPath = IndexPath(item: 0, section: messageList.count - 1) - let frame = messagesCollectionView.layoutAttributesForItem(at: lastIndexPath)?.frame ?? .zero - var rect = messagesCollectionView.convert(frame, to: view) - - // substract 100 to make the "visible" area of a cell bigger - rect.origin.y -= 100 - - var visibleRect = CGRect(x: messagesCollectionView.bounds.origin.x, y: messagesCollectionView.bounds.origin.y, width: - messagesCollectionView.bounds.size.width, height: - messagesCollectionView.bounds.size.height - messagesCollectionView.contentInset.bottom) - - visibleRect = messagesCollectionView.convert(visibleRect, to: view) - return visibleRect.contains(rect) + return messagesCollectionView.indexPathsForVisibleItems.contains(lastIndexPath) } // MARK: - MessagesDataSource @@ -205,6 +194,10 @@ extension ChatViewController: MessageCellDelegate { print("Bottom label tapped") } + func didTapAccessoryView(in cell: MessageCollectionViewCell) { + print("Accessory view tapped") + } + } // MARK: - MessageLabelDelegate diff --git a/Example/Sources/View Controllers/SettingsViewController.swift b/Example/Sources/View Controllers/SettingsViewController.swift index f1c6a214..882e0f9e 100644 --- a/Example/Sources/View Controllers/SettingsViewController.swift +++ b/Example/Sources/View Controllers/SettingsViewController.swift @@ -30,12 +30,12 @@ final internal class SettingsViewController: UITableViewController { // MARK: - Properties - var selectedMockMessagesCount: Int = 20 - override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent } + let cells = ["Mock messages count", "Text Messages", "AttributedText Messages", "Photo Messages", "Video Messages", "Emoji Messages", "Location Messages", "Url Messages", "Phone Messages"] + // MARK: - Picker var messagesPicker = UIPickerView() @@ -55,6 +55,8 @@ final internal class SettingsViewController: UITableViewController { messagesPicker.dataSource = self messagesPicker.delegate = self messagesPicker.backgroundColor = .white + + messagesPicker.selectRow(UserDefaults.standard.mockMessagesCount(), inComponent: 0, animated: false) } // MARK: - Toolbar @@ -82,12 +84,25 @@ final internal class SettingsViewController: UITableViewController { // MARK: - TableViewDelegate & TableViewDataSource override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 1 + return cells.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cellValue = cells[indexPath.row] + let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell() + cell.textLabel?.text = cells[indexPath.row] - return indexPath.row == 0 ? configureTextFieldTableViewCell(at: indexPath) : UITableViewCell() + switch cellValue { + case "Mock messages count": + return configureTextFieldTableViewCell(at: indexPath) + default: + let switchView = UISwitch(frame: .zero) + switchView.isOn = UserDefaults.standard.bool(forKey: cellValue) + switchView.tag = indexPath.row + switchView.addTarget(self, action: #selector(self.switchChanged(_:)), for: .valueChanged) + cell.accessoryView = switchView + } + return cell } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { @@ -118,6 +133,12 @@ final internal class SettingsViewController: UITableViewController { } return TextFieldTableViewCell() } + + @objc func switchChanged(_ sender: UISwitch!) { + let cell = cells[sender.tag] + + UserDefaults.standard.set(sender.isOn, forKey: cell) + } } // MARK: - UIPickerViewDelegate, UIPickerViewDataSource @@ -134,8 +155,4 @@ extension SettingsViewController: UIPickerViewDelegate, UIPickerViewDataSource { func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { return "\(row)" } - - func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { - selectedMockMessagesCount = row - } } diff --git a/MessageKit.podspec b/MessageKit.podspec index f95895ee..b013f0d8 100644 --- a/MessageKit.podspec +++ b/MessageKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'MessageKit' - s.version = '1.0.0' + s.version = '2.0.0-beta.1' s.license = { :type => "MIT", :file => "LICENSE.md" } s.summary = 'An elegant messages UI library for iOS.' diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index fe5a2e07..8e056eb6 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -34,6 +34,7 @@ 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A320087C82004FD648 /* MessageKitError.swift */; }; 1FF377AA20087D78004FD648 /* MessagesViewController+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */; }; 1FF377AC20087DA2004FD648 /* MessagesViewController+Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */; }; + 382C794221705D2000F4FAF5 /* HorizontalEdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382C794121705D2000F4FAF5 /* HorizontalEdgeInsets.swift */; }; 38C2AE7C20D4878D00F8079E /* MessageInputBar.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38C2AE7B20D4878D00F8079E /* MessageInputBar.framework */; }; 88916B2D1CF0DF2F00469F91 /* MessageKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 88916B221CF0DF2F00469F91 /* MessageKit.framework */; }; 8962AC8A1F87AB7D0030B058 /* MessagesCollectionViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC831F87AB230030B058 /* MessagesCollectionViewTests.swift */; }; @@ -128,6 +129,7 @@ 1FF377A320087C82004FD648 /* MessageKitError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageKitError.swift; sourceTree = ""; }; 1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Menu.swift"; sourceTree = ""; }; 1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Keyboard.swift"; sourceTree = ""; }; + 382C794121705D2000F4FAF5 /* HorizontalEdgeInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalEdgeInsets.swift; sourceTree = ""; }; 38C2AE7B20D4878D00F8079E /* MessageInputBar.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageInputBar.framework; path = Carthage/Build/iOS/MessageInputBar.framework; sourceTree = ""; }; 88916B221CF0DF2F00469F91 /* MessageKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MessageKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 88916B2C1CF0DF2F00469F91 /* MessageKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MessageKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -364,11 +366,12 @@ B7A03F1C1F866895006AEF79 /* Avatar.swift */, 1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */, B7A03F211F866895006AEF79 /* DetectorType.swift */, + 382C794121705D2000F4FAF5 /* HorizontalEdgeInsets.swift */, B7A03F221F866895006AEF79 /* LabelAlignment.swift */, B7A03F1D1F866895006AEF79 /* LocationMessageSnapshotOptions.swift */, B7A03F231F866895006AEF79 /* MessageKind.swift */, - 1FF377A320087C82004FD648 /* MessageKitError.swift */, B7A03F1B1F866895006AEF79 /* MessageKitDateFormatter.swift */, + 1FF377A320087C82004FD648 /* MessageKitError.swift */, B7A03F1F1F866895006AEF79 /* MessageStyle.swift */, B7A03F1A1F866895006AEF79 /* NSConstraintLayoutSet.swift */, B7A03F1E1F866895006AEF79 /* Sender.swift */, @@ -568,6 +571,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 382C794221705D2000F4FAF5 /* HorizontalEdgeInsets.swift in Sources */, B7A03F3C1F866946006AEF79 /* LocationMessageCell.swift in Sources */, 1FF377AA20087D78004FD648 /* MessagesViewController+Menu.swift in Sources */, B7A03F5B1F8669CA006AEF79 /* MessageType.swift in Sources */, diff --git a/README.md b/README.md index ceaec455..55b22a7b 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ github "MessageKit/MessageKit" ## Contributing Great! Look over these things first. -- Please read our [Code of Conduct](https://github.com/MessageKit/MessageKit/blob/master/Code_of_Conduct.md) +- Please read our [Code of Conduct](https://github.com/MessageKit/MessageKit/blob/master/CODE_OF_CONDUCT.md) - Check the [Contributing Guide Lines](https://github.com/MessageKit/MessageKit/blob/master/CONTRIBUTING.md). - Come join us on [Slack](https://join.slack.com/t/messagekit/shared_invite/MjI4NzIzNzMyMzU0LTE1MDMwODIzMDUtYzllYzIyNTU4MA) and 🗣 don't be a stranger. - Check out the [current issues](https://github.com/MessageKit/MessageKit/issues) and see if you can tackle any of those. @@ -116,6 +116,8 @@ Interested in contributing to MessageKit? Click here to join our [Slack](https:/ Add your app to the list of apps using this library and make a pull request. - [MediQuo](https://www.mediquo.com) +- [RappresentaMe](https://itunes.apple.com/it/app/rappresentame/id1330914443) +- [WiseEyes](https://itunes.apple.com/us/app/wiseeyes/id1391408511?mt=8) *Please provide attribution, it is greatly appreciated.* diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift index 545fedda..ff43ffeb 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.swift @@ -181,6 +181,8 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { return collectionView.messagesDataSource?.numberOfItems(inSection: section, in: collectionView) ?? 0 } + /// Note: + /// If you override this method, remember to call MessagesDataSource's customCell(for:at:in:) for MessageKind.custom messages, if necessary open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let messagesCollectionView = collectionView as? MessagesCollectionView else { @@ -207,7 +209,7 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { cell.configure(with: message, at: indexPath, and: messagesCollectionView) return cell case .custom: - fatalError(MessageKitError.customDataUnresolvedCell) + return messagesDataSource.customCell(for: message, at: indexPath, in: messagesCollectionView) } } diff --git a/Sources/Layout/MessageSizeCalculator.swift b/Sources/Layout/MessageSizeCalculator.swift index 1556ed85..f83f826e 100644 --- a/Sources/Layout/MessageSizeCalculator.swift +++ b/Sources/Layout/MessageSizeCalculator.swift @@ -53,8 +53,8 @@ open class MessageSizeCalculator: CellSizeCalculator { public var incomingAccessoryViewSize = CGSize.zero public var outgoingAccessoryViewSize = CGSize.zero - public var incomingAccessoryViewPadding = UIEdgeInsets.zero - public var outgoingAccessoryViewPadding = UIEdgeInsets.zero + public var incomingAccessoryViewPadding = HorizontalEdgeInsets.zero + public var outgoingAccessoryViewPadding = HorizontalEdgeInsets.zero open override func configure(attributes: UICollectionViewLayoutAttributes) { guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return } @@ -96,45 +96,43 @@ open class MessageSizeCalculator: CellSizeCalculator { let avatarHeight = avatarSize(for: message).height let avatarVerticalPosition = avatarPosition(for: message).vertical let accessoryViewHeight = accessoryViewSize(for: message).height - let accessoryViewVerticalPadding = accessoryViewPadding(for: message).vertical - let accessoryViewTotalHeight = accessoryViewHeight + accessoryViewVerticalPadding switch avatarVerticalPosition { case .messageCenter: let totalLabelHeight: CGFloat = cellTopLabelHeight + messageTopLabelHeight + messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight let cellHeight = max(avatarHeight, totalLabelHeight) - return max(accessoryViewTotalHeight, cellHeight) + return max(cellHeight, accessoryViewHeight) case .messageBottom: var cellHeight: CGFloat = 0 cellHeight += messageBottomLabelHeight let labelsHeight = messageContainerHeight + messageVerticalPadding + cellTopLabelHeight + messageTopLabelHeight cellHeight += max(labelsHeight, avatarHeight) - return max(accessoryViewTotalHeight, cellHeight) + return max(cellHeight, accessoryViewHeight) case .messageTop: var cellHeight: CGFloat = 0 cellHeight += cellTopLabelHeight cellHeight += messageTopLabelHeight let labelsHeight = messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight cellHeight += max(labelsHeight, avatarHeight) - return max(accessoryViewTotalHeight, cellHeight) + return max(cellHeight, accessoryViewHeight) case .messageLabelTop: var cellHeight: CGFloat = 0 cellHeight += cellTopLabelHeight let messageLabelsHeight = messageContainerHeight + messageBottomLabelHeight + messageVerticalPadding + messageTopLabelHeight cellHeight += max(messageLabelsHeight, avatarHeight) - return max(accessoryViewTotalHeight, cellHeight) + return max(cellHeight, accessoryViewHeight) case .cellTop, .cellBottom: let totalLabelHeight: CGFloat = cellTopLabelHeight + messageTopLabelHeight + messageContainerHeight + messageVerticalPadding + messageBottomLabelHeight let cellHeight = max(avatarHeight, totalLabelHeight) - return max(accessoryViewTotalHeight, cellHeight) + return max(cellHeight, accessoryViewHeight) } } // MARK: - Avatar - public func avatarPosition(for message: MessageType) -> AvatarPosition { + open func avatarPosition(for message: MessageType) -> AvatarPosition { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) var position = isFromCurrentSender ? outgoingAvatarPosition : incomingAvatarPosition @@ -148,7 +146,7 @@ open class MessageSizeCalculator: CellSizeCalculator { return position } - public func avatarSize(for message: MessageType) -> CGSize { + open func avatarSize(for message: MessageType) -> CGSize { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) return isFromCurrentSender ? outgoingAvatarSize : incomingAvatarSize @@ -156,14 +154,14 @@ open class MessageSizeCalculator: CellSizeCalculator { // MARK: - Top cell Label - public func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + open func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { let layoutDelegate = messagesLayout.messagesLayoutDelegate let collectionView = messagesLayout.messagesCollectionView let height = layoutDelegate.cellTopLabelHeight(for: message, at: indexPath, in: collectionView) return CGSize(width: messagesLayout.itemWidth, height: height) } - public func cellTopLabelAlignment(for message: MessageType) -> LabelAlignment { + open func cellTopLabelAlignment(for message: MessageType) -> LabelAlignment { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) return isFromCurrentSender ? outgoingCellTopLabelAlignment : incomingCellTopLabelAlignment @@ -171,14 +169,14 @@ open class MessageSizeCalculator: CellSizeCalculator { // MARK: - Top message Label - public func messageTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + open func messageTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { let layoutDelegate = messagesLayout.messagesLayoutDelegate let collectionView = messagesLayout.messagesCollectionView let height = layoutDelegate.messageTopLabelHeight(for: message, at: indexPath, in: collectionView) return CGSize(width: messagesLayout.itemWidth, height: height) } - public func messageTopLabelAlignment(for message: MessageType) -> LabelAlignment { + open func messageTopLabelAlignment(for message: MessageType) -> LabelAlignment { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) return isFromCurrentSender ? outgoingMessageTopLabelAlignment : incomingMessageTopLabelAlignment @@ -186,14 +184,14 @@ open class MessageSizeCalculator: CellSizeCalculator { // MARK: - Bottom Label - public func messageBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + open func messageBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { let layoutDelegate = messagesLayout.messagesLayoutDelegate let collectionView = messagesLayout.messagesCollectionView let height = layoutDelegate.messageBottomLabelHeight(for: message, at: indexPath, in: collectionView) return CGSize(width: messagesLayout.itemWidth, height: height) } - public func messageBottomLabelAlignment(for message: MessageType) -> LabelAlignment { + open func messageBottomLabelAlignment(for message: MessageType) -> LabelAlignment { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) return isFromCurrentSender ? outgoingMessageBottomLabelAlignment : incomingMessageBottomLabelAlignment @@ -207,7 +205,7 @@ open class MessageSizeCalculator: CellSizeCalculator { return isFromCurrentSender ? outgoingAccessoryViewSize : incomingAccessoryViewSize } - public func accessoryViewPadding(for message: MessageType) -> UIEdgeInsets { + public func accessoryViewPadding(for message: MessageType) -> HorizontalEdgeInsets { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) return isFromCurrentSender ? outgoingAccessoryViewPadding : incomingAccessoryViewPadding @@ -215,7 +213,7 @@ open class MessageSizeCalculator: CellSizeCalculator { // MARK: - MessageContainer - public func messageContainerPadding(for message: MessageType) -> UIEdgeInsets { + open func messageContainerPadding(for message: MessageType) -> UIEdgeInsets { let dataSource = messagesLayout.messagesDataSource let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) return isFromCurrentSender ? outgoingMessagePadding : incomingMessagePadding diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index 4e3d71b7..cd5fe5af 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -66,19 +66,31 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { public override init() { super.init() - - sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) - - NotificationCenter.default.addObserver(self, selector: #selector(MessagesCollectionViewFlowLayout.handleOrientationChange(_:)), name: UIDevice.orientationDidChangeNotification, object: nil) + + setupView() + setupObserver() } required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: aDecoder) + + setupView() + setupObserver() } deinit { NotificationCenter.default.removeObserver(self) } + + // MARK: - Methods + + private func setupView() { + sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) + } + + private func setupObserver() { + NotificationCenter.default.addObserver(self, selector: #selector(MessagesCollectionViewFlowLayout.handleOrientationChange(_:)), name: UIDevice.orientationDidChangeNotification, object: nil) + } // MARK: - Attributes @@ -135,6 +147,8 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { lazy open var videoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self) lazy open var locationMessageSizeCalculator = LocationMessageSizeCalculator(layout: self) + /// - Note: + /// If you override this method, remember to call MessageLayoutDelegate's customCellSizeCalculator(for:at:in:) method for MessageKind.custom messages, if necessary open func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator { let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) switch message.kind { @@ -151,7 +165,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { case .location: return locationMessageSizeCalculator case .custom: - fatalError("Must return a CellSizeCalculator for MessageKind.custom(Any?)") + return messagesLayoutDelegate.customCellSizeCalculator(for: message, at: indexPath, in: messagesCollectionView) } } @@ -231,12 +245,12 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { } /// Set `incomingAccessoryViewSize` of all `MessageSizeCalculator`s - public func setMessageIncomingAccessoryViewPadding(_ newPadding: UIEdgeInsets) { + public func setMessageIncomingAccessoryViewPadding(_ newPadding: HorizontalEdgeInsets) { messageSizeCalculators().forEach { $0.incomingAccessoryViewPadding = newPadding } } /// Set `outgoingAvatarSize` of all `MessageSizeCalculator`s - public func setMessageOutgoingAccessoryViewPadding(_ newPadding: UIEdgeInsets) { + public func setMessageOutgoingAccessoryViewPadding(_ newPadding: HorizontalEdgeInsets) { messageSizeCalculators().forEach { $0.outgoingAccessoryViewPadding = newPadding } } diff --git a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift index 9dcde3cf..e055ac57 100644 --- a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift +++ b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift @@ -47,7 +47,7 @@ open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttribu public var messageBottomLabelSize: CGSize = .zero public var accessoryViewSize: CGSize = .zero - public var accessoryViewPadding: UIEdgeInsets = .zero + public var accessoryViewPadding: HorizontalEdgeInsets = .zero // MARK: - Methods diff --git a/Sources/Models/HorizontalEdgeInsets.swift b/Sources/Models/HorizontalEdgeInsets.swift new file mode 100644 index 00000000..9ec2f0f5 --- /dev/null +++ b/Sources/Models/HorizontalEdgeInsets.swift @@ -0,0 +1,55 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import Foundation + +/// A varient of `UIEdgeInsets` that only has horizontal inset properties +public struct HorizontalEdgeInsets { + + public var left: CGFloat + public var right: CGFloat + + public init(left: CGFloat, right: CGFloat) { + self.left = left + self.right = right + } + + public static var zero: HorizontalEdgeInsets { + return HorizontalEdgeInsets(left: 0, right: 0) + } +} + +extension HorizontalEdgeInsets: Equatable { + + public static func == (lhs: HorizontalEdgeInsets, rhs: HorizontalEdgeInsets) -> Bool { + return lhs.left == rhs.left && lhs.right == rhs.right + } +} + +extension HorizontalEdgeInsets { + + internal var horizontal: CGFloat { + return left + right + } +} diff --git a/Sources/Models/MessageKind.swift b/Sources/Models/MessageKind.swift index fc130f52..a29d76d5 100644 --- a/Sources/Models/MessageKind.swift +++ b/Sources/Models/MessageKind.swift @@ -52,9 +52,9 @@ public enum MessageKind { case emoji(String) /// A custom message. - /// - Note: Using this case requires that you override the following methods and handle this case: - /// - `collectionView(_:cellForItemAt indexPath: IndexPath) -> UICollectionViewCell` - /// - `cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator` + /// - Note: Using this case requires that you implement the following methods and handle this case: + /// - MessagesDataSource: customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell + /// - MessagesLayoutDelegate: customCellSizeCalculator(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator case custom(Any?) // MARK: - Not supported yet diff --git a/Sources/Protocols/MessageCellDelegate.swift b/Sources/Protocols/MessageCellDelegate.swift index 1300a223..4fbe9da9 100644 --- a/Sources/Protocols/MessageCellDelegate.swift +++ b/Sources/Protocols/MessageCellDelegate.swift @@ -77,6 +77,16 @@ public protocol MessageCellDelegate: MessageLabelDelegate { /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` /// method `messageForItem(at:indexPath:messagesCollectionView)`. func didTapMessageBottomLabel(in cell: MessageCollectionViewCell) + + /// Triggered when a tap occurs in the accessoryView. + /// + /// - Parameters: + /// - cell: The cell where the tap occurred. + /// + /// You can get a reference to the `MessageType` for the cell by using `UICollectionView`'s + /// `indexPath(for: cell)` method. Then using the returned `IndexPath` with the `MessagesDataSource` + /// method `messageForItem(at:indexPath:messagesCollectionView)`. + func didTapAccessoryView(in cell: MessageCollectionViewCell) } @@ -91,4 +101,6 @@ public extension MessageCellDelegate { func didTapMessageTopLabel(in cell: MessageCollectionViewCell) {} func didTapMessageBottomLabel(in cell: MessageCollectionViewCell) {} + + func didTapAccessoryView(in cell: MessageCollectionViewCell) {} } diff --git a/Sources/Protocols/MessagesDataSource.swift b/Sources/Protocols/MessagesDataSource.swift index 1763a8c8..3caba08b 100644 --- a/Sources/Protocols/MessagesDataSource.swift +++ b/Sources/Protocols/MessagesDataSource.swift @@ -92,6 +92,17 @@ public protocol MessagesDataSource: AnyObject { /// /// The default value returned by this method is `nil`. func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? + + /// Custom collectionView cell for message with `custom` message type. + /// + /// - Parameters: + /// - message: The `custom` message type + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// This method will call fatalError() on default. You must override this method if you are using MessageType.custom messages. + func customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell } public extension MessagesDataSource { @@ -115,4 +126,8 @@ public extension MessagesDataSource { func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { return nil } + + func customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell { + fatalError(MessageKitError.customDataUnresolvedCell) + } } diff --git a/Sources/Protocols/MessagesLayoutDelegate.swift b/Sources/Protocols/MessagesLayoutDelegate.swift index 24a13c7a..4dd75743 100644 --- a/Sources/Protocols/MessagesLayoutDelegate.swift +++ b/Sources/Protocols/MessagesLayoutDelegate.swift @@ -80,6 +80,17 @@ public protocol MessagesLayoutDelegate: AnyObject { /// - Note: /// The default value returned by this method is zero. func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat + + /// Custom cell size calculator for messages with MessageType.custom. + /// + /// - Parameters: + /// - message: The custom message + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// - Note: + /// The default implementation will throw fatalError(). You must override this method if you are using messages with MessageType.custom. + func customCellSizeCalculator(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator } public extension MessagesLayoutDelegate { @@ -103,4 +114,8 @@ public extension MessagesLayoutDelegate { func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { return 0 } + + func customCellSizeCalculator(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator { + fatalError("Must return a CellSizeCalculator for MessageKind.custom(Any?)") + } } diff --git a/Sources/Views/AvatarView.swift b/Sources/Views/AvatarView.swift index ccc16184..cf3c1456 100644 --- a/Sources/Views/AvatarView.swift +++ b/Sources/Views/AvatarView.swift @@ -74,9 +74,15 @@ open class AvatarView: UIImageView { super.init(frame: frame) prepareView() } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + prepareView() + } convenience public init() { self.init(frame: .zero) + prepareView() } private func setImageFrom(initials: String?) { @@ -152,10 +158,6 @@ open class AvatarView: UIImageView { return CGRect(startX+2, startY, w-4, h) } - required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - // MARK: - Internal methods internal func prepareView() { diff --git a/Sources/Views/Cells/MessageCollectionViewCell.swift b/Sources/Views/Cells/MessageCollectionViewCell.swift index 8d99ab90..08114cb5 100644 --- a/Sources/Views/Cells/MessageCollectionViewCell.swift +++ b/Sources/Views/Cells/MessageCollectionViewCell.swift @@ -34,7 +34,7 @@ open class MessageCollectionViewCell: UICollectionViewCell { } public required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: aDecoder) } } diff --git a/Sources/Views/Cells/MessageContentCell.swift b/Sources/Views/Cells/MessageContentCell.swift index be7a5a9e..3a194a68 100644 --- a/Sources/Views/Cells/MessageContentCell.swift +++ b/Sources/Views/Cells/MessageContentCell.swift @@ -73,7 +73,9 @@ open class MessageContentCell: MessageCollectionViewCell { } required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: aDecoder) + contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + setupSubviews() } open func setupSubviews() { @@ -156,6 +158,8 @@ open class MessageContentCell: MessageCollectionViewCell { delegate?.didTapMessageTopLabel(in: self) case messageBottomLabel.frame.contains(touchLocation): delegate?.didTapMessageBottomLabel(in: self) + case accessoryView.frame.contains(touchLocation): + delegate?.didTapAccessoryView(in: self) default: break } @@ -223,7 +227,12 @@ open class MessageContentCell: MessageCollectionViewCell { fallthrough } default: - origin.y = attributes.cellTopLabelSize.height + attributes.messageTopLabelSize.height + attributes.messageContainerPadding.top + if attributes.accessoryViewSize.height > attributes.messageContainerSize.height { + let messageHeight = attributes.messageContainerSize.height + attributes.messageContainerPadding.vertical + origin.y = (attributes.size.height / 2) - (messageHeight / 2) + } else { + origin.y = attributes.cellTopLabelSize.height + attributes.messageTopLabelSize.height + attributes.messageContainerPadding.top + } } switch attributes.avatarPosition.horizontal { @@ -271,8 +280,9 @@ open class MessageContentCell: MessageCollectionViewCell { /// Positions the cell's accessory view. /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. open func layoutAccessoryView(with attributes: MessagesCollectionViewLayoutAttributes) { - var y = (bounds.height - attributes.accessoryViewSize.height) / 2 - y -= attributes.accessoryViewPadding.vertical + attributes.accessoryViewPadding.top + + // Accessory view aligned to the middle of the messageContainerView + let y = messageContainerView.frame.midY - (attributes.accessoryViewSize.height / 2) var origin = CGPoint(x: 0, y: y) diff --git a/Sources/Views/MessageLabel.swift b/Sources/Views/MessageLabel.swift index d72a0ffc..6f37e1f1 100644 --- a/Sources/Views/MessageLabel.swift +++ b/Sources/Views/MessageLabel.swift @@ -112,6 +112,13 @@ open class MessageLabel: UILabel { if !isConfiguring { setNeedsDisplay() } } } + + open override var intrinsicContentSize: CGSize { + var size = super.intrinsicContentSize + size.width += textInsets.horizontal + size.height += textInsets.vertical + return size + } internal var messageLabelFont: UIFont? @@ -171,12 +178,12 @@ open class MessageLabel: UILabel { public override init(frame: CGRect) { super.init(frame: frame) - self.numberOfLines = 0 - self.lineBreakMode = .byWordWrapping + setupView() } public required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: aDecoder) + setupView() } // MARK: - Open Methods @@ -314,6 +321,11 @@ open class MessageLabel: UILabel { fatalError(MessageKitError.unrecognizedCheckingResult) } } + + private func setupView() { + numberOfLines = 0 + lineBreakMode = .byWordWrapping + } // MARK: - Parsing Text diff --git a/Sources/Views/MessagesCollectionView.swift b/Sources/Views/MessagesCollectionView.swift index 487348d0..ee1e4cd3 100644 --- a/Sources/Views/MessagesCollectionView.swift +++ b/Sources/Views/MessagesCollectionView.swift @@ -52,7 +52,7 @@ open class MessagesCollectionView: UICollectionView { } required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(frame: .zero, collectionViewLayout: MessagesCollectionViewFlowLayout()) } public convenience init() { diff --git a/Sources/Views/PlayButtonView.swift b/Sources/Views/PlayButtonView.swift index 2b6dee76..b446ccfd 100644 --- a/Sources/Views/PlayButtonView.swift +++ b/Sources/Views/PlayButtonView.swift @@ -40,14 +40,15 @@ open class PlayButtonView: UIView { setupSubviews() setupConstraints() - - triangleView.clipsToBounds = true - triangleView.backgroundColor = .black - backgroundColor = .playButtonLightGray + setupView() } required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: aDecoder) + + setupSubviews() + setupConstraints() + setupView() } // MARK: - Methods @@ -66,6 +67,13 @@ open class PlayButtonView: UIView { private func setupSubviews() { addSubview(triangleView) } + + private func setupView() { + triangleView.clipsToBounds = true + triangleView.backgroundColor = .black + + backgroundColor = .playButtonLightGray + } private func setupConstraints() { triangleView.translatesAutoresizingMaskIntoConstraints = false