diff --git a/.swiftlint.yml b/.swiftlint.yml index 0189737e..8bf8a3da 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -11,6 +11,10 @@ custom_rules: regex: "override (open|public|private|internal|fileprivate)" # matching pattern message: "Use like open override or public override instead" # violation message. optional. severity: warning # violation severity. optional. +opt_in_rules: + - explicit_acl + - explicit_top_level_acl +explicit_acl: error +explicit_top_level_acl: error included: - - Sources - - Tests \ No newline at end of file + - Sources \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f80f0f3..18dfeca6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,38 +8,113 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa ### Added -- **Breaking Change** Adds new `DetectorType` called `.transitInformation` to message label. +- **Breaking Change** Added new `MessagesDataSource` delegate methods, `numberOfSections(in)` and `numberOfItems(inSection:in)`. +[#601](https://github.com/MessageKit/MessageKit/pull/601) by [@SD10](https://github.com/sd10), [@zhongwuzw](https://github.com/zhongwuzw). + +- **Breaking Change** Added new protocol `MediaItem` as the associated value for the +`MessageData.video` and `MessageData.photo` cases. +[#587](https://github.com/MessageKit/MessageKit/pull/587) by [@SD10](https://github.com/sd10). + +- **Breaking Change** Added new protocol `LocationItem` as the associated value for +the `MessageData.location` case. +[#587](https://github.com/MessageKit/MessageKit/pull/587) by [@SD10](https://github.com/sd10). + +- **Breaking Change** Added new `DetectorType` called `.transitInformation` to message label. [#520](https://github.com/MessageKit/MessageKit/pull/520) by [@nosarj](https://github.com/nosarj). -- **Breaking Change** Adds `.custom(Any?)` case to `MessageData`. +- **Breaking Change** Added `.custom(Any?)` case to `MessageData`. [#498](https://github.com/MessageKit/MessageKit/pull/498) by [@SD10](https://github.com/SD10). -- Adds the following methods to `MessagesCollectionViewFlowLayout`: - - `messageLabelInsets(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets` - - `messageContainerPadding(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets` - - `messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat` - - `messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize` - - `cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment` - - `cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize` - - `cellTopLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat` - - `cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment` - - `cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize` - - `cellBottomLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat` - - `avatarPosition(for message: MessageType, at indexPath: IndexPath) -> AvatarPosition` - - `avatarSize(for message: MessageType, at indexPath: IndexPath) -> CGSize` - - `cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat` -[#491](https://github.com/MessageKit/MessageKit/pull/491) by [@SD10](https://github.com/SD10). +- Added `CellSizeCalculator` protocol that is responsible for sizing and configuring attributes of a `MessageCollectionViewCell`. +[#579](https://github.com/MessageKit/MessageKit/pull/579) by [@SD10](https://github.com/SD10). -- Adds the following methods to `MessageCollectionViewCell`: - - `layoutAvatarView(with attributes: MessagesCollectionViewLayoutAttributes)` - - `layoutMessageContainerView(with attributes: MessagesCollectionViewLayoutAttributes)` - - `layoutTopLabel(with attributes: MessagesCollectionViewLayoutAttributes)` - - `layoutBottomLabel(with attributes: MessagesCollectionViewLayoutAttributes)` -[#491](https://github.com/MessageKit/MessageKit/pull/491) by [@SD10](https://github.com/SD10). +- Added `MessageSizeCalculator`, `MediaMessageSizeCalculator`, `TextMessageSizeCalculator`, and `LocationMessageSizeCalculator` + classes that are responsible for sizing the `MessagesCollectionViewCell` types provided by MessageKit. +[#579](https://github.com/MessageKit/MessageKit/pull/579) by [@SD10](https://github.com/sd10). -- Added `shouldManageSendButtonEnabledState` to `MessageInputBar` to disable automatically managing `MessageInputBar.sendButton`'s `isEnabled` state when text changes. (Default value is `true`). +- Added `shouldManageSendButtonEnabledState` to `MessageInputBar` to disable automatically managing `MessageInputBar.sendButton`'s + `isEnabled` state when text changes. (Default value is `true`). [#530](https://github.com/MessageKit/MessageKit/pull/530) by [@clayellis](https://github.com/clayellis). +- Added two new methods `cellTopLabelHeight(for:at:in)` and `cellBottomLabelHeight(for:at:in)` to `MessagesLayoutDelegate` +[#580](https://github.com/MessageKit/MessageKit/pull/580) by [@SD10](https://github.com/sd10). + +- Added new class `InsetLabel`. +[#580](https://github.com/MessageKit/MessageKit/pull/580) by [@SD10](https://github.com/sd10). + +- Added new method `configureMediaMessageImageView(_:for:at:in)` to configure the `UIImageView` of a +`MediaMessageCell` asynchronously. +[#592](https://github.com/MessageKit/MessageKit/pull/592) by [@zhongwuzw](https://github.com/zhongwuzw), [@SD10](https://github.com/sd10) + +- Added new `MessageContentCell` class and `MessageReusableView` class. +[#596](https://github.com/MessageKit/MessageKit/pull/596) by [@SD10](https://github.com/sd10). + +### Changed + +- **Breaking Change** Changed the `messageFooterView(for:in)` and `messageHeaderView(for:in)` methods of +`MessagesDisplayDelegate` by removing the `message` parameter. +[#615](https://github.com/MessageKit/MessageKit/pull/615) by [@SD10](https://github.com/sd10). + +- **Breaking Change** Changed the `footerViewSize(for:in)` and `headerViewSize(for:in)` methods of `MessagesLayoutDelegate` +by removing the `MessageType` and `IndexPath` parameters and replacing them with a new `section: Int` parameter. +[#615](https://github.com/MessageKit/MessageKit/pull/615) by [@SD10](https://github.com/sd10). + +- **Breaking Change** The reuse identifiers of all `MessageReusableView`s and `MessageCollectionViewCell`s have been +changed to match their class name exactly. +[#615](https://github.com/MessageKit/MessageKit/pull/615) by [@SD10](https://github.com/sd10). + +- **Breaking Change** Changed the superclass of `MessageDateHeaderView` from `MessageHeaderView` to `MessageReusableView`. +[#615](https://github.com/MessageKit/MessageKit/pull/615) by [@SD10](https://github.com/sd10). + +- **Breaking Change** Change `NSLayoutConstraintSet` class from `public` to `internal`. +[#607](https://github.com/MessageKit/MessageKit/pull/607) by [@zhongwuzw](https://github.com/zhongwuzw). + +- **Breaking Change** `MessageHeaderView` and `MessageFooterView` now subclass `MessageReusableView` class. +[#596](https://github.com/MessageKit/MessageKit/pull/596) by [@SD10](https://github.com/sd10). + +- **Breaking Change** Renamed `MessageCollectionViewCell` to `MessageContentCell`. +The `MessageCollectionViewCell` class is now a bare bones subclass. +[#596](https://github.com/MessageKit/MessageKit/pull/596) by [@SD10](https://github.com/sd10). + +- **Breaking Change** Changed `LabelAlignment` to be a `struct` with properties of +`textAlignment: NSTextAlignment` and `textInsets: UIEdgeInsets` to position the text in the `cellTopLabel` and `cellBottomLabel`. +[#580](https://github.com/MessageKit/MessageKit/pull/580) by [@SD10](https://github.com/sd10). + +- **Breaking Change** The type of `cellTopLabel` and `cellBottomLabel` has been changed to `InsetLabel`. +[#580](https://github.com/MessageKit/MessageKit/pull/580) by [@SD10](https://github.com/sd10). + +- The `MessageData.emoji` case no longer uses a default font of 2x the `messageLabelFont` size. +You must now set this font explicitly through the `emojiMessageSizeCalculator` on `MessagesCollectionViewFlowLayout`. +[#530](https://github.com/MessageKit/MessageKit/pull/579) by [@SD10](https://github.com/sd10). + +- Changed the `contentMode` of the `UIImageView` for `MediaMessageCell` to be `.scaleAspectFill`. +[#587](https://github.com/MessageKit/MessageKit/pull/587) by [@SD10](https://github.com/sd10). + +- The result of the `MessagesDisplayDelegate` method `textColor(for message:...)` no longer applies to `.attributedText` case of `MessageData`. +[#625](https://github.com/MessageKit/MessageKit/pull/625) by [@cwalo](https://github.com/cwalo). + +### Removed + +- **Breaking Change** Removed the `showsDateHeaderAfterTimeInterval` property of `MessagesCollectionView`. +[#615](https://github.com/MessageKit/MessageKit/pull/615) by [@SD10](https://github.com/sd10). + +- **Breaking Change** Removed the `reuseIdentifer` method from `MessageCollectionViewCell`, `TextMessageCell`, +`LocationMessageCell`, `MediaMessageCell`, and `MessageContentCell`. +[#615](https://github.com/MessageKit/MessageKit/pull/615) by [@SD10](https://github.com/sd10). + +- **Breaking Change** Removed the `CollectionViewReusable` protocol. +[#615](https://github.com/MessageKit/MessageKit/pull/615) by [@SD10](https://github.com/sd10). + +- **Breaking Change** Removed `MessageHeaderView` and `MessageFooterView` in favor of `MessageReusableView`. +[#615](https://github.com/MessageKit/MessageKit/pull/615) by [@SD10](https://github.com/sd10). + +- **Breaking Change** Removed `numberOfMessages(in)` method of `MessagesDataSource`, instead please use `numberOfSections(in)` and `numberOfItems(inSection:in)`. +[#601](https://github.com/MessageKit/MessageKit/pull/601) by [@SD10](https://github.com/sd10), [@zhongwuzw](https://github.com/zhongwuzw). + +- **Breaking Change** Removed the `messageLabelFont` property from `MessagesCollectionViewFlowLayout`. +You can now set this property through `textMessageSizeCalculator` property. +[#579](https://github.com/MessageKit/MessageKit/pull/579) by [@SD10](https://github.com/sd10). + ## [[Prerelease] 0.13.1](https://github.com/MessageKit/MessageKit/releases/tag/0.13.1) ### Fixed @@ -53,6 +128,9 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa - Fixed message bubble tail orientation invalidation in `iOS9`. [#469](https://github.com/MessageKit/MessageKit/pull/469) by [@zhongwuzw](https://github.com/zhongwuzw). +- Fixed equality checking on `MessagesCollectionViewLayoutAttributes`. +[#593](https://github.com/MessageKit/MessageKit/pull/593) by [@zhongwuzw](https://github.com/zhongwuzw), [@SD10](https://github.com/sd10) + ## [[Prerelease] 0.13.0](https://github.com/MessageKit/MessageKit/releases/tag/0.13.0) ### Fixed diff --git a/Example/.swiftlint.yml b/Example/.swiftlint.yml new file mode 100644 index 00000000..821c4a93 --- /dev/null +++ b/Example/.swiftlint.yml @@ -0,0 +1,15 @@ + +disabled_rules: + - identifier_name + - trailing_whitespace + - line_length + - type_body_length + - file_length +custom_rules: + override_func: # rule identifier + name: "override in func" # rule name. optional. + regex: "override (open|public|private|internal|fileprivate)" # matching pattern + message: "Use like open override or public override instead" # violation message. optional. + severity: warning # violation severity. optional. +included: + - Sources \ No newline at end of file diff --git a/Example/ChatExample.xcodeproj/project.pbxproj b/Example/ChatExample.xcodeproj/project.pbxproj index 56d35379..1efe577a 100644 --- a/Example/ChatExample.xcodeproj/project.pbxproj +++ b/Example/ChatExample.xcodeproj/project.pbxproj @@ -207,6 +207,7 @@ 882B5EA41CF7D8D100B6E160 /* Embed Frameworks */, 3C8DFCD3063DA0DF099EC7B6 /* [CP] Embed Pods Frameworks */, CD3F8EE3AFF2313DF2D62568 /* [CP] Copy Pods Resources */, + 7E0AFF9D207BB460004DFD4C /* ShellScript */, ); buildRules = ( ); @@ -266,7 +267,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0900; + LastUpgradeCheck = 0930; ORGANIZATIONNAME = MessageKit; TargetAttributes = { 882B5E321CF7D4B900B6E160 = { @@ -464,6 +465,19 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ChatExampleUITests/Pods-ChatExampleUITests-resources.sh\"\n"; showEnvVarsInLog = 0; }; + 7E0AFF9D207BB460004DFD4C /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; + }; CD3F8EE3AFF2313DF2D62568 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -556,12 +570,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -611,12 +627,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; diff --git a/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExample.xcscheme b/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExample.xcscheme index 56543ffd..cebf89c0 100644 --- a/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExample.xcscheme +++ b/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExample.xcscheme @@ -1,6 +1,6 @@ + codeCoverageEnabled = "YES" + shouldUseLaunchSchemeArgsEnv = "YES"> @@ -71,7 +70,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExampleUITests.xcscheme b/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExampleUITests.xcscheme index 6d3eb900..78acc96f 100644 --- a/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExampleUITests.xcscheme +++ b/Example/ChatExample.xcodeproj/xcshareddata/xcschemes/ChatExampleUITests.xcscheme @@ -1,6 +1,6 @@ Bool { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? = nil) -> Bool { UIApplication.shared.statusBarStyle = .lightContent window = UIWindow(frame: UIScreen.main.bounds) diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index 97275ade..a4820889 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -26,7 +26,7 @@ import UIKit import MessageKit import MapKit -class ConversationViewController: MessagesViewController { +internal class ConversationViewController: MessagesViewController { let refreshControl = UIRefreshControl() @@ -91,8 +91,6 @@ class ConversationViewController: MessagesViewController { label.text = "nathan.tannar is typing..." label.font = UIFont.boldSystemFont(ofSize: 16) messageInputBar.topStackView.addArrangedSubview(label) - - messageInputBar.topStackViewPadding.top = 6 messageInputBar.topStackViewPadding.left = 12 @@ -156,11 +154,9 @@ class ConversationViewController: MessagesViewController { }, makeButton(named: "ic_at").onSelected { $0.tintColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) - print("@ Selected") }, makeButton(named: "ic_hashtag").onSelected { $0.tintColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) - print("# Selected") }, .flexibleSpace, makeButton(named: "ic_library").onTextViewDidChange { button, textView in @@ -260,8 +256,8 @@ extension ConversationViewController: MessagesDataSource { func currentSender() -> Sender { return SampleData.shared.currentSender } - - func numberOfMessages(in messagesCollectionView: MessagesCollectionView) -> Int { + + func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int { return messageList.count } @@ -275,15 +271,13 @@ extension ConversationViewController: MessagesDataSource { } func cellBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { - - struct ConversationDateFormatter { - static let formatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateStyle = .medium - return formatter - }() - } - let formatter = ConversationDateFormatter.formatter + + let formatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + return formatter + }() + let dateString = formatter.string(from: message.sentDate) return NSAttributedString(string: dateString, attributes: [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: .caption2)]) } @@ -300,7 +294,7 @@ extension ConversationViewController: MessagesDisplayDelegate { return isFromCurrentSender(message: message) ? .white : .darkText } - func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedStringKey : Any] { + func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedStringKey: Any] { return MessageLabel.defaultAttributes } @@ -357,45 +351,17 @@ extension ConversationViewController: MessagesDisplayDelegate { extension ConversationViewController: MessagesLayoutDelegate { - func avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition { - return AvatarPosition(horizontal: .natural, vertical: .messageBottom) + func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + return 16 } - func messagePadding(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets { - if isFromCurrentSender(message: message) { - return UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) - } else { - return UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) - } + func cellBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + return 16 } - func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment { - if isFromCurrentSender(message: message) { - return .messageTrailing(UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 10)) - } else { - return .messageLeading(UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0)) - } - } - - func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment { - if isFromCurrentSender(message: message) { - return .messageLeading(UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0)) - } else { - return .messageTrailing(UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 10)) - } - } - - func footerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { - + private func footerViewSize(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { return CGSize(width: messagesCollectionView.bounds.width, height: 10) } - - // MARK: - Location Messages - - func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return 200 - } - } // MARK: - MessageCellDelegate @@ -424,7 +390,7 @@ extension ConversationViewController: MessageCellDelegate { extension ConversationViewController: MessageLabelDelegate { - func didSelectAddress(_ addressComponents: [String : String]) { + func didSelectAddress(_ addressComponents: [String: String]) { print("Address Selected: \(addressComponents)") } @@ -440,7 +406,7 @@ extension ConversationViewController: MessageLabelDelegate { print("URL Selected: \(url)") } - func didSelectTransitInformation(_ transitInformation: [String : String]) { + func didSelectTransitInformation(_ transitInformation: [String: String]) { print("TransitInformation Selected: \(transitInformation)") } diff --git a/Example/Sources/IMessageViewController.swift b/Example/Sources/IMessageViewController.swift index 77a8dcaa..4e29233c 100644 --- a/Example/Sources/IMessageViewController.swift +++ b/Example/Sources/IMessageViewController.swift @@ -8,8 +8,7 @@ import MessageKit -class IMessageViewController: MessagesViewController { - +internal class IMessageViewController: MessagesViewController { override func viewDidLoad() { super.viewDidLoad() diff --git a/Example/Sources/InboxViewController.swift b/Example/Sources/InboxViewController.swift index 37f478bc..559437ad 100644 --- a/Example/Sources/InboxViewController.swift +++ b/Example/Sources/InboxViewController.swift @@ -26,7 +26,7 @@ import UIKit import MessageKit import SafariServices -final class InboxViewController: UITableViewController { +final internal class InboxViewController: UITableViewController { let cells = ["Example", "Settings", "Source Code", "Contributors"] @@ -37,7 +37,7 @@ final class InboxViewController: UITableViewController { title = "MessageKit" navigationController?.navigationBar.tintColor = .white navigationController?.navigationBar.barTintColor = UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) - navigationController?.navigationBar.titleTextAttributes = [NSAttributedStringKey.foregroundColor: UIColor.white, NSAttributedStringKey.font : UIFont.systemFont(ofSize: 20, weight: UIFont.Weight.bold) ] + navigationController?.navigationBar.titleTextAttributes = [NSAttributedStringKey.foregroundColor: UIColor.white, NSAttributedStringKey.font: UIFont.systemFont(ofSize: 20, weight: UIFont.Weight.bold) ] tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") tableView.tableFooterView = UIView() } diff --git a/Example/Sources/MockMessage.swift b/Example/Sources/MockMessage.swift index de49c2a8..f7077a82 100644 --- a/Example/Sources/MockMessage.swift +++ b/Example/Sources/MockMessage.swift @@ -1,18 +1,18 @@ /* 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 @@ -23,42 +23,71 @@ */ import Foundation -import MessageKit import CoreLocation +import MessageKit + +private struct MockLocationItem: LocationItem { + + var location: CLLocation + var size: CGSize + + init(location: CLLocation) { + self.location = location + self.size = CGSize(width: 240, height: 240) + } + +} + +private struct MockMediaItem: MediaItem { + + var url: URL? + var image: UIImage? + var placeholderImage: UIImage + var size: CGSize + + init(image: UIImage) { + self.image = image + self.size = CGSize(width: 240, height: 240) + self.placeholderImage = UIImage() + } + +} + +internal struct MockMessage: MessageType { + + var messageId: String + var sender: Sender + var sentDate: Date + var data: MessageData + + private init(data: MessageData, sender: Sender, messageId: String, date: Date) { + self.data = data + self.sender = sender + self.messageId = messageId + self.sentDate = date + } -struct MockMessage: MessageType { - - var messageId: String - var sender: Sender - var sentDate: Date - var data: MessageData - - init(data: MessageData, sender: Sender, messageId: String, date: Date) { - self.data = data - self.sender = sender - self.messageId = messageId - self.sentDate = date - } - init(text: String, sender: Sender, messageId: String, date: Date) { self.init(data: .text(text), sender: sender, messageId: messageId, date: date) - } - + } + init(attributedText: NSAttributedString, sender: Sender, messageId: String, date: Date) { self.init(data: .attributedText(attributedText), sender: sender, messageId: messageId, date: date) - } + } init(image: UIImage, sender: Sender, messageId: String, date: Date) { - self.init(data: .photo(image), sender: sender, messageId: messageId, date: date) + let mediaItem = MockMediaItem(image: image) + self.init(data: .photo(mediaItem), sender: sender, messageId: messageId, date: date) } init(thumbnail: UIImage, sender: Sender, messageId: String, date: Date) { - let url = URL(fileURLWithPath: "") - self.init(data: .video(file: url, thumbnail: thumbnail), sender: sender, messageId: messageId, date: date) + let mediaItem = MockMediaItem(image: thumbnail) + self.init(data: .video(mediaItem), sender: sender, messageId: messageId, date: date) } init(location: CLLocation, sender: Sender, messageId: String, date: Date) { - self.init(data: .location(location), sender: sender, messageId: messageId, date: date) + let locationItem = MockLocationItem(location: location) + self.init(data: .location(locationItem), sender: sender, messageId: messageId, date: date) } init(emoji: String, sender: Sender, messageId: String, date: Date) { diff --git a/Example/Sources/SampleData.swift b/Example/Sources/SampleData.swift index f33d5e8c..10619a8e 100644 --- a/Example/Sources/SampleData.swift +++ b/Example/Sources/SampleData.swift @@ -25,7 +25,7 @@ import MessageKit import CoreLocation -final class SampleData { +final internal class SampleData { static let shared = SampleData() diff --git a/Example/Sources/SettingsViewController.swift b/Example/Sources/SettingsViewController.swift index 61bb813d..dae638b2 100644 --- a/Example/Sources/SettingsViewController.swift +++ b/Example/Sources/SettingsViewController.swift @@ -25,7 +25,7 @@ import UIKit import MessageKit -final class SettingsViewController: UITableViewController { +final internal class SettingsViewController: UITableViewController { // MARK: - Properties @@ -100,16 +100,18 @@ final class SettingsViewController: UITableViewController { // 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 + if 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 + } + return TextFieldTableViewCell() } } diff --git a/Example/Sources/TableViewCells.swift b/Example/Sources/TableViewCells.swift index b3e114e5..6b88a523 100644 --- a/Example/Sources/TableViewCells.swift +++ b/Example/Sources/TableViewCells.swift @@ -24,7 +24,7 @@ import UIKit -class TextFieldTableViewCell: UITableViewCell { +internal class TextFieldTableViewCell: UITableViewCell { static let identifier = "TextFieldTableViewCellIdentifier" diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 6c9f1752..ce879f8f 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 0EE91E661FDEC888005420A2 /* CGRect+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE91E651FDEC887005420A2 /* CGRect+Extensions.swift */; }; + 0EF0888C206F7E83007F2F58 /* CellSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EF0888B206F7E83007F2F58 /* CellSizeCalculator.swift */; }; 1F066E131FD90BB600E11013 /* MessagesViewControllerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7FC8CB1FD2700B006CC979 /* MessagesViewControllerSpec.swift */; }; 1F066E141FD90BB700E11013 /* MessageLabelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F066E101FD90A0600E11013 /* MessageLabelSpec.swift */; }; 1F066E1D1FDA3C1700E11013 /* SenderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F066E1C1FDA3C1700E11013 /* SenderSpec.swift */; }; @@ -15,15 +16,21 @@ 1F066E231FDA3F0200E11013 /* DetectorTypeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F066E221FDA3F0200E11013 /* DetectorTypeSpec.swift */; }; 1F087D451FD274FD00E95A45 /* Nimble.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 1F7FC8C71FD26F49006CC979 /* Nimble.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 1F087D461FD274FD00E95A45 /* Quick.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 1F7FC8C51FD26F33006CC979 /* Quick.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 1F6C040C206A2891007BDE44 /* MessageContentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6C040B206A2891007BDE44 /* MessageContentCell.swift */; }; + 1F6C040E206A2AF4007BDE44 /* MessageReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6C040D206A2AF4007BDE44 /* MessageReusableView.swift */; }; 1F7FC8C61FD26F33006CC979 /* Quick.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F7FC8C51FD26F33006CC979 /* Quick.framework */; }; 1F7FC8C81FD26F49006CC979 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F7FC8C71FD26F49006CC979 /* Nimble.framework */; }; 1F82D1431FB1B75B00B81A88 /* AvatarPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */; }; - 1FAC3488201E44F7000A1504 /* MessagesCollectionViewFlowLayout+Labels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAC3487201E44F7000A1504 /* MessagesCollectionViewFlowLayout+Labels.swift */; }; - 1FAC348A201E4DE7000A1504 /* MessagesCollectionViewFlowLayout+Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAC3489201E4DE7000A1504 /* MessagesCollectionViewFlowLayout+Avatar.swift */; }; - 1FAC348C201E4F88000A1504 /* MessagesCollectionViewFlowLayout+MessageContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAC348B201E4F88000A1504 /* MessagesCollectionViewFlowLayout+MessageContainerView.swift */; }; - 1FAC3490201E50DF000A1504 /* MessageCellLayoutContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAC348F201E50DF000A1504 /* MessageCellLayoutContext.swift */; }; - 1FAC3492201E5346000A1504 /* MessagesCollectionViewFlowLayout+CellSizing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAC3491201E5346000A1504 /* MessagesCollectionViewFlowLayout+CellSizing.swift */; }; 1FCA6D30201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */; }; + 1FD589602064E08A004B5081 /* MediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD5895F2064E08A004B5081 /* MediaItem.swift */; }; + 1FD589612064E1B5004B5081 /* MockMessagesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC7A1F87AB230030B058 /* MockMessagesDataSource.swift */; }; + 1FD589622064E1B9004B5081 /* MockMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC7B1F87AB230030B058 /* MockMessage.swift */; }; + 1FD5896420660C1C004B5081 /* LocationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD5896320660C1C004B5081 /* LocationItem.swift */; }; + 1FE7839E20662835007FA024 /* MessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE7839D20662835007FA024 /* MessageSizeCalculator.swift */; }; + 1FE783A220662905007FA024 /* TextMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE783A120662905007FA024 /* TextMessageSizeCalculator.swift */; }; + 1FE783A4206629A5007FA024 /* MediaMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE783A3206629A5007FA024 /* MediaMessageSizeCalculator.swift */; }; + 1FE783A6206629C2007FA024 /* LocationMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE783A5206629C2007FA024 /* LocationMessageSizeCalculator.swift */; }; + 1FE783A8206633C0007FA024 /* InsetLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FE783A7206633C0007FA024 /* InsetLabel.swift */; }; 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 */; }; @@ -41,8 +48,6 @@ 8962AC941F87AB860030B058 /* MessageKitDateFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC741F87AB230030B058 /* MessageKitDateFormatterTests.swift */; }; 8962AC951F87AB860030B058 /* MessageStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC751F87AB230030B058 /* MessageStyleTests.swift */; }; 8962AC991F87AB860030B058 /* MessagesDisplayDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC811F87AB230030B058 /* MessagesDisplayDelegateTests.swift */; }; - 8962AC9A1F87ABB20030B058 /* MockMessagesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC7A1F87AB230030B058 /* MockMessagesDataSource.swift */; }; - 8962AC9C1F87ABC60030B058 /* MockMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8962AC7B1F87AB230030B058 /* MockMessage.swift */; }; B0C8D99B1F73076B000A86E4 /* MessageKitAssets.bundle in Resources */ = {isa = PBXBuildFile; fileRef = B0C8D99A1F73076B000A86E4 /* MessageKitAssets.bundle */; }; B7A03F181F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F161F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift */; }; B7A03F191F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F171F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift */; }; @@ -56,8 +61,6 @@ B7A03F2D1F866895006AEF79 /* LabelAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F221F866895006AEF79 /* LabelAlignment.swift */; }; B7A03F2E1F866895006AEF79 /* MessageData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F231F866895006AEF79 /* MessageData.swift */; }; B7A03F331F866940006AEF79 /* MessageDateHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F301F866940006AEF79 /* MessageDateHeaderView.swift */; }; - B7A03F341F866940006AEF79 /* MessageFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F311F866940006AEF79 /* MessageFooterView.swift */; }; - B7A03F351F866940006AEF79 /* MessageHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F321F866940006AEF79 /* MessageHeaderView.swift */; }; B7A03F3A1F866946006AEF79 /* TextMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F361F866946006AEF79 /* TextMessageCell.swift */; }; B7A03F3C1F866946006AEF79 /* LocationMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F381F866946006AEF79 /* LocationMessageCell.swift */; }; B7A03F3D1F866946006AEF79 /* MediaMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F391F866946006AEF79 /* MediaMessageCell.swift */; }; @@ -77,12 +80,10 @@ B7A03F601F8669CA006AEF79 /* MessagesDisplayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F561F8669C9006AEF79 /* MessagesDisplayDelegate.swift */; }; B7A03F611F8669CA006AEF79 /* MessagesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F571F8669CA006AEF79 /* MessagesDataSource.swift */; }; B7A03F621F8669CA006AEF79 /* MessageInputBarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F581F8669CA006AEF79 /* MessageInputBarDelegate.swift */; }; - B7A03F6A1F8669EB006AEF79 /* UICollectionView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F641F8669EB006AEF79 /* UICollectionView+Extensions.swift */; }; B7A03F6B1F8669EB006AEF79 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F651F8669EB006AEF79 /* UIColor+Extensions.swift */; }; B7A03F6C1F8669EB006AEF79 /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F661F8669EB006AEF79 /* UIView+Extensions.swift */; }; B7A03F6D1F8669EB006AEF79 /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F671F8669EB006AEF79 /* Bundle+Extensions.swift */; }; B7A03F6E1F8669EB006AEF79 /* NSAttributedString+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F681F8669EB006AEF79 /* NSAttributedString+Extensions.swift */; }; - B7A03F6F1F8669EB006AEF79 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F691F8669EB006AEF79 /* String+Extensions.swift */; }; B7A03F731F866A06006AEF79 /* MessageKit+Availability.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F701F866A06006AEF79 /* MessageKit+Availability.swift */; }; B7A03F751F866A06006AEF79 /* MessageKit.h in Headers */ = {isa = PBXBuildFile; fileRef = B7A03F721F866A06006AEF79 /* MessageKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; B7A03F7B1F866B85006AEF79 /* MessageCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F7A1F866B85006AEF79 /* MessageCollectionViewCell.swift */; }; @@ -114,21 +115,26 @@ /* Begin PBXFileReference section */ 0EE91E651FDEC887005420A2 /* CGRect+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGRect+Extensions.swift"; sourceTree = ""; }; + 0EF0888B206F7E83007F2F58 /* CellSizeCalculator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellSizeCalculator.swift; sourceTree = ""; }; 1F066E101FD90A0600E11013 /* MessageLabelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageLabelSpec.swift; sourceTree = ""; }; 1F066E1C1FDA3C1700E11013 /* SenderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SenderSpec.swift; sourceTree = ""; }; 1F066E201FDA3DEA00E11013 /* AvatarSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarSpec.swift; sourceTree = ""; }; 1F066E221FDA3F0200E11013 /* DetectorTypeSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectorTypeSpec.swift; sourceTree = ""; }; + 1F6C040B206A2891007BDE44 /* MessageContentCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContentCell.swift; sourceTree = ""; }; + 1F6C040D206A2AF4007BDE44 /* MessageReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReusableView.swift; sourceTree = ""; }; 1F7FC8C31FD26F22006CC979 /* Carthage */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Carthage; sourceTree = ""; }; 1F7FC8C51FD26F33006CC979 /* Quick.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quick.framework; path = Carthage/Build/iOS/Quick.framework; sourceTree = ""; }; 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 = ""; }; - 1FAC3487201E44F7000A1504 /* MessagesCollectionViewFlowLayout+Labels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesCollectionViewFlowLayout+Labels.swift"; sourceTree = ""; }; - 1FAC3489201E4DE7000A1504 /* MessagesCollectionViewFlowLayout+Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesCollectionViewFlowLayout+Avatar.swift"; sourceTree = ""; }; - 1FAC348B201E4F88000A1504 /* MessagesCollectionViewFlowLayout+MessageContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesCollectionViewFlowLayout+MessageContainerView.swift"; sourceTree = ""; }; - 1FAC348F201E50DF000A1504 /* MessageCellLayoutContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCellLayoutContext.swift; sourceTree = ""; }; - 1FAC3491201E5346000A1504 /* MessagesCollectionViewFlowLayout+CellSizing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesCollectionViewFlowLayout+CellSizing.swift"; sourceTree = ""; }; 1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Extensions.swift"; sourceTree = ""; }; + 1FD5895F2064E08A004B5081 /* MediaItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaItem.swift; sourceTree = ""; }; + 1FD5896320660C1C004B5081 /* LocationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationItem.swift; sourceTree = ""; }; + 1FE7839D20662835007FA024 /* MessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSizeCalculator.swift; sourceTree = ""; }; + 1FE783A120662905007FA024 /* TextMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageSizeCalculator.swift; sourceTree = ""; }; + 1FE783A3206629A5007FA024 /* MediaMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaMessageSizeCalculator.swift; sourceTree = ""; }; + 1FE783A5206629C2007FA024 /* LocationMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationMessageSizeCalculator.swift; sourceTree = ""; }; + 1FE783A7206633C0007FA024 /* InsetLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsetLabel.swift; sourceTree = ""; }; 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 = ""; }; @@ -163,8 +169,6 @@ B7A03F221F866895006AEF79 /* LabelAlignment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelAlignment.swift; sourceTree = ""; }; B7A03F231F866895006AEF79 /* MessageData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageData.swift; sourceTree = ""; }; B7A03F301F866940006AEF79 /* MessageDateHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageDateHeaderView.swift; sourceTree = ""; }; - B7A03F311F866940006AEF79 /* MessageFooterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageFooterView.swift; sourceTree = ""; }; - B7A03F321F866940006AEF79 /* MessageHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHeaderView.swift; sourceTree = ""; }; B7A03F361F866946006AEF79 /* TextMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextMessageCell.swift; sourceTree = ""; }; B7A03F381F866946006AEF79 /* LocationMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationMessageCell.swift; sourceTree = ""; }; B7A03F391F866946006AEF79 /* MediaMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaMessageCell.swift; sourceTree = ""; }; @@ -184,12 +188,10 @@ B7A03F561F8669C9006AEF79 /* MessagesDisplayDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDisplayDelegate.swift; sourceTree = ""; }; B7A03F571F8669CA006AEF79 /* MessagesDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDataSource.swift; sourceTree = ""; }; B7A03F581F8669CA006AEF79 /* MessageInputBarDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageInputBarDelegate.swift; sourceTree = ""; }; - B7A03F641F8669EB006AEF79 /* UICollectionView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UICollectionView+Extensions.swift"; sourceTree = ""; }; B7A03F651F8669EB006AEF79 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; B7A03F661F8669EB006AEF79 /* UIView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = ""; }; B7A03F671F8669EB006AEF79 /* Bundle+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+Extensions.swift"; sourceTree = ""; }; B7A03F681F8669EB006AEF79 /* NSAttributedString+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Extensions.swift"; sourceTree = ""; }; - B7A03F691F8669EB006AEF79 /* String+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; B7A03F701F866A06006AEF79 /* MessageKit+Availability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MessageKit+Availability.swift"; sourceTree = ""; }; B7A03F711F866A06006AEF79 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B7A03F721F866A06006AEF79 /* MessageKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MessageKit.h; sourceTree = ""; }; @@ -233,6 +235,7 @@ B7A03F381F866946006AEF79 /* LocationMessageCell.swift */, B7A03F391F866946006AEF79 /* MediaMessageCell.swift */, B7A03F7A1F866B85006AEF79 /* MessageCollectionViewCell.swift */, + 1F6C040B206A2891007BDE44 /* MessageContentCell.swift */, B7A03F361F866946006AEF79 /* TextMessageCell.swift */, ); path = Cells; @@ -242,8 +245,7 @@ isa = PBXGroup; children = ( B7A03F301F866940006AEF79 /* MessageDateHeaderView.swift */, - B7A03F311F866940006AEF79 /* MessageFooterView.swift */, - B7A03F321F866940006AEF79 /* MessageHeaderView.swift */, + 1F6C040D206A2AF4007BDE44 /* MessageReusableView.swift */, ); path = "Headers & Footers"; sourceTree = ""; @@ -372,8 +374,6 @@ 0EE91E651FDEC887005420A2 /* CGRect+Extensions.swift */, B7A03F671F8669EB006AEF79 /* Bundle+Extensions.swift */, B7A03F681F8669EB006AEF79 /* NSAttributedString+Extensions.swift */, - B7A03F691F8669EB006AEF79 /* String+Extensions.swift */, - B7A03F641F8669EB006AEF79 /* UICollectionView+Extensions.swift */, B7A03F651F8669EB006AEF79 /* UIColor+Extensions.swift */, B7A03F661F8669EB006AEF79 /* UIView+Extensions.swift */, 1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */, @@ -408,6 +408,7 @@ B7A03F411F86694F006AEF79 /* InputBarItem.swift */, B7A03F401F86694F006AEF79 /* InputTextView.swift */, 38C57C7B1F9AE4870043CC03 /* InputStackView.swift */, + 1FE783A7206633C0007FA024 /* InsetLabel.swift */, B7A03F431F86694F006AEF79 /* MessageContainerView.swift */, B7A03F421F86694F006AEF79 /* MessageInputBar.swift */, B7A03F3F1F86694F006AEF79 /* MessageLabel.swift */, @@ -428,6 +429,8 @@ B7A03F561F8669C9006AEF79 /* MessagesDisplayDelegate.swift */, B7A03F541F8669C9006AEF79 /* MessagesLayoutDelegate.swift */, B7A03F511F8669C9006AEF79 /* MessageType.swift */, + 1FD5895F2064E08A004B5081 /* MediaItem.swift */, + 1FD5896320660C1C004B5081 /* LocationItem.swift */, ); path = Protocols; sourceTree = ""; @@ -446,12 +449,12 @@ isa = PBXGroup; children = ( B7A03F161F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift */, - 1FAC3487201E44F7000A1504 /* MessagesCollectionViewFlowLayout+Labels.swift */, - 1FAC3489201E4DE7000A1504 /* MessagesCollectionViewFlowLayout+Avatar.swift */, - 1FAC348B201E4F88000A1504 /* MessagesCollectionViewFlowLayout+MessageContainerView.swift */, - 1FAC3491201E5346000A1504 /* MessagesCollectionViewFlowLayout+CellSizing.swift */, B7A03F171F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift */, - 1FAC348F201E50DF000A1504 /* MessageCellLayoutContext.swift */, + 1FE7839D20662835007FA024 /* MessageSizeCalculator.swift */, + 1FE783A120662905007FA024 /* TextMessageSizeCalculator.swift */, + 1FE783A3206629A5007FA024 /* MediaMessageSizeCalculator.swift */, + 1FE783A5206629C2007FA024 /* LocationMessageSizeCalculator.swift */, + 0EF0888B206F7E83007F2F58 /* CellSizeCalculator.swift */, ); path = Layout; sourceTree = ""; @@ -598,56 +601,56 @@ 1FF377AA20087D78004FD648 /* MessagesViewController+Menu.swift in Sources */, 38C57C7C1F9AE4890043CC03 /* InputStackView.swift in Sources */, B7A03F5B1F8669CA006AEF79 /* MessageType.swift in Sources */, - B7A03F6F1F8669EB006AEF79 /* String+Extensions.swift in Sources */, - 1FAC348A201E4DE7000A1504 /* MessagesCollectionViewFlowLayout+Avatar.swift in Sources */, B7A03F491F86694F006AEF79 /* InputBarItem.swift in Sources */, B7A03F601F8669CA006AEF79 /* MessagesDisplayDelegate.swift in Sources */, + 1FE783A8206633C0007FA024 /* InsetLabel.swift in Sources */, B7A03F5C1F8669CA006AEF79 /* MessageCellDelegate.swift in Sources */, 1FF377A420087C82004FD648 /* MessageKitError.swift in Sources */, B7A03F4A1F86694F006AEF79 /* MessageInputBar.swift in Sources */, + 1F6C040E206A2AF4007BDE44 /* MessageReusableView.swift in Sources */, B7A03F4B1F86694F006AEF79 /* MessageContainerView.swift in Sources */, B7A03F281F866895006AEF79 /* LocationMessageSnapshotOptions.swift in Sources */, - 1FAC3490201E50DF000A1504 /* MessageCellLayoutContext.swift in Sources */, B7A03F481F86694F006AEF79 /* InputTextView.swift in Sources */, - 1FAC3488201E44F7000A1504 /* MessagesCollectionViewFlowLayout+Labels.swift in Sources */, B7A03F6C1F8669EB006AEF79 /* UIView+Extensions.swift in Sources */, B7A03F3A1F866946006AEF79 /* TextMessageCell.swift in Sources */, B7A03F191F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift in Sources */, B7A03F461F86694F006AEF79 /* AvatarView.swift in Sources */, 1FCA6D30201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift in Sources */, B7A03F3D1F866946006AEF79 /* MediaMessageCell.swift in Sources */, + 1FE783A220662905007FA024 /* TextMessageSizeCalculator.swift in Sources */, B7A03F331F866940006AEF79 /* MessageDateHeaderView.swift in Sources */, B7A03F2E1F866895006AEF79 /* MessageData.swift in Sources */, B7A03F7B1F866B85006AEF79 /* MessageCollectionViewCell.swift in Sources */, B7A03F6B1F8669EB006AEF79 /* UIColor+Extensions.swift in Sources */, B7A03F5E1F8669CA006AEF79 /* MessagesLayoutDelegate.swift in Sources */, B7A03F261F866895006AEF79 /* MessageKitDateFormatter.swift in Sources */, - 1FAC3492201E5346000A1504 /* MessagesCollectionViewFlowLayout+CellSizing.swift in Sources */, - 8962AC9A1F87ABB20030B058 /* MockMessagesDataSource.swift in Sources */, B7A03F6D1F8669EB006AEF79 /* Bundle+Extensions.swift in Sources */, - B7A03F6A1F8669EB006AEF79 /* UICollectionView+Extensions.swift in Sources */, - 8962AC9C1F87ABC60030B058 /* MockMessage.swift in Sources */, + 1FD589602064E08A004B5081 /* MediaItem.swift in Sources */, B7A03F611F8669CA006AEF79 /* MessagesDataSource.swift in Sources */, B7A03F6E1F8669EB006AEF79 /* NSAttributedString+Extensions.swift in Sources */, B7A03F471F86694F006AEF79 /* MessageLabel.swift in Sources */, B7A03F5F1F8669CA006AEF79 /* MessageLabelDelegate.swift in Sources */, B7A03F4C1F86694F006AEF79 /* PlayButtonView.swift in Sources */, + 1FD5896420660C1C004B5081 /* LocationItem.swift in Sources */, B7A03F291F866895006AEF79 /* Sender.swift in Sources */, B7A03F4F1F86697C006AEF79 /* MessagesViewController.swift in Sources */, B7A03F251F866895006AEF79 /* NSConstraintLayoutSet.swift in Sources */, - B7A03F341F866940006AEF79 /* MessageFooterView.swift in Sources */, B7A03F621F8669CA006AEF79 /* MessageInputBarDelegate.swift in Sources */, 0EE91E661FDEC888005420A2 /* CGRect+Extensions.swift in Sources */, B7A03F181F86682C006AEF79 /* MessagesCollectionViewFlowLayout.swift in Sources */, B7A03F2A1F866895006AEF79 /* MessageStyle.swift in Sources */, B7A03F4D1F86694F006AEF79 /* MessagesCollectionView.swift in Sources */, + 0EF0888C206F7E83007F2F58 /* CellSizeCalculator.swift in Sources */, B7A03F351F866940006AEF79 /* MessageHeaderView.swift in Sources */, + 1FE783A4206629A5007FA024 /* MediaMessageSizeCalculator.swift in Sources */, B7A03F731F866A06006AEF79 /* MessageKit+Availability.swift in Sources */, + 1FE7839E20662835007FA024 /* MessageSizeCalculator.swift in Sources */, + 1FE783A6206629C2007FA024 /* LocationMessageSizeCalculator.swift in Sources */, B7A03F2D1F866895006AEF79 /* LabelAlignment.swift in Sources */, 38C57C791F9AE3E50043CC03 /* SeparatorLine.swift in Sources */, + 1F6C040C206A2891007BDE44 /* MessageContentCell.swift in Sources */, B7A03F2C1F866895006AEF79 /* DetectorType.swift in Sources */, B7A03F271F866895006AEF79 /* Avatar.swift in Sources */, - 1FAC348C201E4F88000A1504 /* MessagesCollectionViewFlowLayout+MessageContainerView.swift in Sources */, 1F82D1431FB1B75B00B81A88 /* AvatarPosition.swift in Sources */, 1FF377AC20087DA2004FD648 /* MessagesViewController+Keyboard.swift in Sources */, ); @@ -665,12 +668,14 @@ 1F066E231FDA3F0200E11013 /* DetectorTypeSpec.swift in Sources */, 1F066E211FDA3DEA00E11013 /* AvatarSpec.swift in Sources */, 8962AC901F87AB7D0030B058 /* MessageDateHeaderViewTests.swift in Sources */, + 1FD589612064E1B5004B5081 /* MockMessagesDataSource.swift in Sources */, 8962AC8F1F87AB7D0030B058 /* MessageInputBarTests.swift in Sources */, 1F066E131FD90BB600E11013 /* MessagesViewControllerSpec.swift in Sources */, 1F066E1D1FDA3C1700E11013 /* SenderSpec.swift in Sources */, 8962AC8A1F87AB7D0030B058 /* MessagesCollectionViewTests.swift in Sources */, 8962AC8D1F87AB7D0030B058 /* MessageCollectionViewCellTests.swift in Sources */, 8962AC991F87AB860030B058 /* MessagesDisplayDelegateTests.swift in Sources */, + 1FD589622064E1B9004B5081 /* MockMessage.swift in Sources */, 8962AC8B1F87AB7D0030B058 /* InputBarItemTests.swift in Sources */, 8962AC951F87AB860030B058 /* MessageStyleTests.swift in Sources */, ); diff --git a/Sources/Controllers/MessagesViewController+Keyboard.swift b/Sources/Controllers/MessagesViewController+Keyboard.swift index 2fdbdba6..1e5f9ff4 100644 --- a/Sources/Controllers/MessagesViewController+Keyboard.swift +++ b/Sources/Controllers/MessagesViewController+Keyboard.swift @@ -28,13 +28,13 @@ extension MessagesViewController { // MARK: - Register / Unregister Observers - func addKeyboardObservers() { + internal func addKeyboardObservers() { NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.handleKeyboardDidChangeState(_:)), name: .UIKeyboardWillChangeFrame, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.handleTextViewDidBeginEditing(_:)), name: .UITextViewTextDidBeginEditing, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.adjustScrollViewInset), name: .UIDeviceOrientationDidChange, object: nil) } - func removeKeyboardObservers() { + internal func removeKeyboardObservers() { NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillChangeFrame, object: nil) NotificationCenter.default.removeObserver(self, name: .UITextViewTextDidBeginEditing, object: nil) NotificationCenter.default.removeObserver(self, name: .UIDeviceOrientationDidChange, object: nil) @@ -72,7 +72,7 @@ extension MessagesViewController { } @objc - func adjustScrollViewInset() { + internal func adjustScrollViewInset() { if #available(iOS 11.0, *) { // No need to add to the top contentInset } else { @@ -86,7 +86,7 @@ extension MessagesViewController { // MARK: - Helpers - var keyboardOffsetFrame: CGRect { + internal var keyboardOffsetFrame: CGRect { guard let inputFrame = inputAccessoryView?.frame else { return .zero } return CGRect(origin: inputFrame.origin, size: CGSize(width: inputFrame.width, height: inputFrame.height - iPhoneXBottomInset)) } diff --git a/Sources/Controllers/MessagesViewController+Menu.swift b/Sources/Controllers/MessagesViewController+Menu.swift index a1fa9de6..57317584 100644 --- a/Sources/Controllers/MessagesViewController+Menu.swift +++ b/Sources/Controllers/MessagesViewController+Menu.swift @@ -28,13 +28,11 @@ extension MessagesViewController { // MARK: - Register / Unregister Observers - /// Add observer for `UIMenuControllerWillShowMenu` notification - func addMenuControllerObservers() { + internal func addMenuControllerObservers() { NotificationCenter.default.addObserver(self, selector: #selector(MessagesViewController.menuControllerWillShow(_:)), name: .UIMenuControllerWillShowMenu, object: nil) } - /// Remove observer for `UIMenuControllerWillShowMenu` notification - func removeMenuControllerObservers() { + internal func removeMenuControllerObservers() { NotificationCenter.default.removeObserver(self, name: .UIMenuControllerWillShowMenu, object: nil) } @@ -57,7 +55,7 @@ extension MessagesViewController { currentMenuController.setMenuVisible(false, animated: false) - guard let selectedCell = messagesCollectionView.cellForItem(at: selectedIndexPath) as? MessageCollectionViewCell else { return } + guard let selectedCell = messagesCollectionView.cellForItem(at: selectedIndexPath) as? MessageContentCell else { return } let selectedCellMessageBubbleFrame = selectedCell.convert(selectedCell.messageContainerView.frame, to: view) var messageInputBarFrame: CGRect = .zero diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift index 4f4f9ad5..02b81e86 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.swift @@ -24,10 +24,10 @@ import UIKit +/// A subclass of `UIViewController` with a `MessagesCollectionView` object +/// that is used to display conversation interfaces. open class MessagesViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { - - // MARK: - Properties [Public] /// The `MessagesCollectionView` managed by the messages view controller object. open var messagesCollectionView = MessagesCollectionView() @@ -59,13 +59,11 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { return false } - /// A Boolean value used to determine if `viewDidLayoutSubviews()` has been called. private var isFirstLayout: Bool = true - - /// Indicated selected indexPath when handle menu action - var selectedIndexPathForMenu: IndexPath? - var messageCollectionViewBottomInset: CGFloat = 0 { + internal var selectedIndexPathForMenu: IndexPath? + + internal var messageCollectionViewBottomInset: CGFloat = 0 { didSet { messagesCollectionView.contentInset.bottom = messageCollectionViewBottomInset messagesCollectionView.scrollIndicatorInsets.bottom = messageCollectionViewBottomInset @@ -106,7 +104,6 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { // MARK: - Methods [Private] - /// Sets the default values for the MessagesViewController private func setupDefaults() { extendedLayoutIncludesOpaqueBars = true automaticallyAdjustsScrollViewInsets = false @@ -115,29 +112,24 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { 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) - messagesCollectionView.register(MessageFooterView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionFooter) - messagesCollectionView.register(MessageHeaderView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader) + messagesCollectionView.register(MessageReusableView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader) messagesCollectionView.register(MessageDateHeaderView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader) } - /// Sets the constraints of the `MessagesCollectionView`. private func setupConstraints() { messagesCollectionView.translatesAutoresizingMaskIntoConstraints = false @@ -160,17 +152,14 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { guard let collectionView = collectionView as? MessagesCollectionView else { fatalError(MessageKitError.notMessagesCollectionView) } - // Each message is its own section - return collectionView.messagesDataSource?.numberOfMessages(in: collectionView) ?? 0 + return collectionView.messagesDataSource?.numberOfSections(in: collectionView) ?? 0 } open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 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 + return collectionView.messagesDataSource?.numberOfItems(inSection: section, in: collectionView) ?? 0 } open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { @@ -209,21 +198,15 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { fatalError(MessageKitError.notMessagesCollectionView) } - guard let dataSource = messagesCollectionView.messagesDataSource else { - fatalError(MessageKitError.nilMessagesDataSource) - } - guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { fatalError(MessageKitError.nilMessagesDisplayDelegate) } - let message = dataSource.messageForItem(at: indexPath, in: messagesCollectionView) - switch kind { case UICollectionElementKindSectionHeader: - return displayDelegate.messageHeaderView(for: message, at: indexPath, in: messagesCollectionView) + return displayDelegate.messageHeaderView(for: indexPath, in: messagesCollectionView) case UICollectionElementKindSectionFooter: - return displayDelegate.messageFooterView(for: message, at: indexPath, in: messagesCollectionView) + return displayDelegate.messageFooterView(for: indexPath, in: messagesCollectionView) default: fatalError(MessageKitError.unrecognizedSectionKind) } @@ -241,32 +224,20 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { 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.nilMessagesLayoutDelegate) } - // Could pose a problem if subclass behaviors allows more than one item per section - let indexPath = IndexPath(item: 0, section: section) - let message = dataSource.messageForItem(at: indexPath, in: messagesCollectionView) - return layoutDelegate.headerViewSize(for: message, at: indexPath, in: messagesCollectionView) + return layoutDelegate.headerViewSize(for: section, in: messagesCollectionView) } open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { 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.nilMessagesLayoutDelegate) } - // Could pose a problem if subclass behaviors allows more than one item per section - let indexPath = IndexPath(item: 0, section: section) - let message = dataSource.messageForItem(at: indexPath, in: messagesCollectionView) - return layoutDelegate.footerViewSize(for: message, at: indexPath, in: messagesCollectionView) + return layoutDelegate.footerViewSize(for: section, in: messagesCollectionView) } open func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool { @@ -298,12 +269,14 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { pasteBoard.string = text case .attributedText(let attributedText): pasteBoard.string = attributedText.string - case .photo(let image): - pasteBoard.image = image + case .photo(let mediaItem): + pasteBoard.image = mediaItem.image ?? mediaItem.placeholderImage default: break } } + + // MARK: - Helpers private func addObservers() { NotificationCenter.default.addObserver( diff --git a/Sources/Extensions/Bundle+Extensions.swift b/Sources/Extensions/Bundle+Extensions.swift index 9d6f641e..0cda64b9 100644 --- a/Sources/Extensions/Bundle+Extensions.swift +++ b/Sources/Extensions/Bundle+Extensions.swift @@ -26,7 +26,7 @@ import Foundation extension Bundle { - static func messageKitAssetBundle() -> Bundle { + internal static func messageKitAssetBundle() -> Bundle { let podBundle = Bundle(for: MessagesViewController.self) guard let resourceBundleUrl = podBundle.url(forResource: "MessageKitAssets", withExtension: "bundle") else { diff --git a/Sources/Extensions/CGRect+Extensions.swift b/Sources/Extensions/CGRect+Extensions.swift index 3b68368c..1a1177b5 100644 --- a/Sources/Extensions/CGRect+Extensions.swift +++ b/Sources/Extensions/CGRect+Extensions.swift @@ -26,7 +26,7 @@ import Foundation extension CGRect { - init(_ x: CGFloat, _ y: CGFloat, _ w: CGFloat, _ h: CGFloat) { + internal init(_ x: CGFloat, _ y: CGFloat, _ w: CGFloat, _ h: CGFloat) { self.init(x: x, y: y, width: w, height: h) } diff --git a/Sources/Extensions/NSAttributedString+Extensions.swift b/Sources/Extensions/NSAttributedString+Extensions.swift index 77fb7093..d27f0d3a 100644 --- a/Sources/Extensions/NSAttributedString+Extensions.swift +++ b/Sources/Extensions/NSAttributedString+Extensions.swift @@ -26,7 +26,7 @@ import Foundation extension NSAttributedString { - func height(considering width: CGFloat) -> CGFloat { + internal func height(considering width: CGFloat) -> CGFloat { let constraintBox = CGSize(width: width, height: .greatestFiniteMagnitude) let rect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, context: nil) @@ -34,7 +34,7 @@ extension NSAttributedString { } - func width(considering height: CGFloat) -> CGFloat { + internal func width(considering height: CGFloat) -> CGFloat { let constraintBox = CGSize(width: .greatestFiniteMagnitude, height: height) let rect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, context: nil) diff --git a/Sources/Extensions/UICollectionView+Extensions.swift b/Sources/Extensions/UICollectionView+Extensions.swift deleted file mode 100644 index 9b624064..00000000 --- a/Sources/Extensions/UICollectionView+Extensions.swift +++ /dev/null @@ -1,70 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Foundation - -/// Optional Cell Protocol to Simplify registration/cell type loading in a generic way -public protocol CollectionViewReusable: AnyObject { - static func reuseIdentifier() -> String -} - -public extension MessagesCollectionView { - /// Registers a particular cell using its reuse-identifier - func register(_ cellClass: CellType.Type) { - register(cellClass, forCellWithReuseIdentifier: CellType.reuseIdentifier()) - } - - /// Registers a reusable view for a specific SectionKind - func register(_ headerFooterClass: ViewType.Type, forSupplementaryViewOfKind kind: String) { - register(headerFooterClass, - forSupplementaryViewOfKind: kind, - withReuseIdentifier: ViewType.reuseIdentifier()) - } - - /// Generically dequeues a cell of the correct type allowing you to avoid scattering your code with guard-let-else-fatal - func dequeueReusableCell(_ cellClass: CellType.Type, for indexPath: IndexPath) -> CellType { - guard let cell = dequeueReusableCell(withReuseIdentifier: cellClass.reuseIdentifier(), for: indexPath) as? CellType else { - fatalError("Unable to dequeue \(String(describing: cellClass)) with reuseId of \(cellClass.reuseIdentifier())") - } - return cell - } - - /// Generically dequeues a header of the correct type allowing you to avoid scattering your code with guard-let-else-fatal - func dequeueReusableHeaderView(_ viewClass: ViewType.Type, for indexPath: IndexPath) -> ViewType { - let view = dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: viewClass.reuseIdentifier(), for: indexPath) - guard let viewType = view as? ViewType else { - fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(viewClass.reuseIdentifier())") - } - return viewType - } - - /// Generically dequeues a footer of the correct type allowing you to avoid scattering your code with guard-let-else-fatal - func dequeueReusableFooterView(_ viewClass: ViewType.Type, for indexPath: IndexPath) -> ViewType { - let view = dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionFooter, withReuseIdentifier: viewClass.reuseIdentifier(), for: indexPath) - guard let viewType = view as? ViewType else { - fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(viewClass.reuseIdentifier())") - } - return viewType - } -} diff --git a/Sources/Extensions/UIColor+Extensions.swift b/Sources/Extensions/UIColor+Extensions.swift index 0295a63a..79c4f4fc 100644 --- a/Sources/Extensions/UIColor+Extensions.swift +++ b/Sources/Extensions/UIColor+Extensions.swift @@ -26,14 +26,14 @@ import Foundation extension UIColor { - static let incomingGray = UIColor(red: 230/255, green: 230/255, blue: 235/255, alpha: 1.0) + internal static let incomingGray = UIColor(red: 230/255, green: 230/255, blue: 235/255, alpha: 1.0) - static let outgoingGreen = UIColor(red: 69/255, green: 214/255, blue: 93/255, alpha: 1.0) + internal static let outgoingGreen = UIColor(red: 69/255, green: 214/255, blue: 93/255, alpha: 1.0) - static let inputBarGray = UIColor(red: 247/255, green: 247/255, blue: 247/255, alpha: 1.0) + internal static let inputBarGray = UIColor(red: 247/255, green: 247/255, blue: 247/255, alpha: 1.0) - static let playButtonLightGray = UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1.0) + internal static let playButtonLightGray = UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1.0) - static let sendButtonBlue = UIColor(red: 15/255, green: 135/255, blue: 255/255, alpha: 1.0) + internal static let sendButtonBlue = UIColor(red: 15/255, green: 135/255, blue: 255/255, alpha: 1.0) } diff --git a/Sources/Extensions/UIEdgeInsets+Extensions.swift b/Sources/Extensions/UIEdgeInsets+Extensions.swift index 019b4343..4d99e081 100644 --- a/Sources/Extensions/UIEdgeInsets+Extensions.swift +++ b/Sources/Extensions/UIEdgeInsets+Extensions.swift @@ -26,11 +26,11 @@ import Foundation extension UIEdgeInsets { - var vertical: CGFloat { + internal var vertical: CGFloat { return top + bottom } - var horizontal: CGFloat { + internal var horizontal: CGFloat { return left + right } diff --git a/Sources/Extensions/UIView+Extensions.swift b/Sources/Extensions/UIView+Extensions.swift index f1519695..b38d39a5 100644 --- a/Sources/Extensions/UIView+Extensions.swift +++ b/Sources/Extensions/UIView+Extensions.swift @@ -26,7 +26,7 @@ import UIKit extension UIView { - func fillSuperview() { + internal func fillSuperview() { guard let superview = self.superview else { return } @@ -41,7 +41,7 @@ extension UIView { NSLayoutConstraint.activate(constraints) } - func centerInSuperview() { + internal func centerInSuperview() { guard let superview = self.superview else { return } @@ -53,7 +53,7 @@ extension UIView { NSLayoutConstraint.activate(constraints) } - func constraint(equalTo size: CGSize) { + internal func constraint(equalTo size: CGSize) { guard superview != nil else { return } translatesAutoresizingMaskIntoConstraints = false let constraints: [NSLayoutConstraint] = [ @@ -65,7 +65,7 @@ extension UIView { } @discardableResult - func addConstraints(_ top: NSLayoutYAxisAnchor? = nil, left: NSLayoutXAxisAnchor? = nil, bottom: NSLayoutYAxisAnchor? = nil, right: NSLayoutXAxisAnchor? = nil, topConstant: CGFloat = 0, leftConstant: CGFloat = 0, bottomConstant: CGFloat = 0, rightConstant: CGFloat = 0, widthConstant: CGFloat = 0, heightConstant: CGFloat = 0) -> [NSLayoutConstraint] { + internal func addConstraints(_ top: NSLayoutYAxisAnchor? = nil, left: NSLayoutXAxisAnchor? = nil, bottom: NSLayoutYAxisAnchor? = nil, right: NSLayoutXAxisAnchor? = nil, topConstant: CGFloat = 0, leftConstant: CGFloat = 0, bottomConstant: CGFloat = 0, rightConstant: CGFloat = 0, widthConstant: CGFloat = 0, heightConstant: CGFloat = 0) -> [NSLayoutConstraint] { if self.superview == nil { return [] diff --git a/Sources/Layout/CellSizeCalculator.swift b/Sources/Layout/CellSizeCalculator.swift new file mode 100644 index 00000000..09e356d1 --- /dev/null +++ b/Sources/Layout/CellSizeCalculator.swift @@ -0,0 +1,48 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +/// An object is responsible for +/// sizing and configuring cells for given `IndexPath`s. +open class CellSizeCalculator { + + /// The layout object for which the cell size calculator is used. + public weak var layout: UICollectionViewFlowLayout? + + /// Used to configure the layout attributes for a given cell. + /// + /// - Parameters: + /// - attributes: The attributes of the cell. + /// The default does nothing + open func configure(attributes: UICollectionViewLayoutAttributes) {} + + /// Used to size an item at a given `IndexPath`. + /// + /// - Parameters: + /// - indexPath: The `IndexPath` of the item to be displayed. + /// The default return .zero + open func sizeForItem(at indexPath: IndexPath) -> CGSize { return .zero } + +} diff --git a/Sources/Extensions/String+Extensions.swift b/Sources/Layout/LocationMessageSizeCalculator.swift similarity index 61% rename from Sources/Extensions/String+Extensions.swift rename to Sources/Layout/LocationMessageSizeCalculator.swift index 205db79d..75048212 100644 --- a/Sources/Extensions/String+Extensions.swift +++ b/Sources/Layout/LocationMessageSizeCalculator.swift @@ -1,18 +1,18 @@ /* 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 @@ -24,21 +24,20 @@ import Foundation -extension String { - - func height(considering width: CGFloat, and font: UIFont) -> CGFloat { - - let constraintBox = CGSize(width: width, height: .greatestFiniteMagnitude) - let rect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, attributes: [NSAttributedStringKey.font: font], context: nil) - return rect.height - - } - - func width(considering height: CGFloat, and font: UIFont) -> CGFloat { - - let constraintBox = CGSize(width: .greatestFiniteMagnitude, height: height) - let rect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, attributes: [NSAttributedStringKey.font: font], context: nil) - return rect.width +open class LocationMessageSizeCalculator: MessageSizeCalculator { + open override func messageContainerSize(for message: MessageType) -> CGSize { + switch message.data { + case .location(let item): + let maxWidth = messageContainerMaxWidth(for: message) + if maxWidth < item.size.width { + // Maintain the ratio if width is too great + let height = maxWidth * item.size.height / item.size.width + return CGSize(width: maxWidth, height: height) + } + return item.size + default: + fatalError("messageContainerSize received unhandled MessageDataType: \(message.data)") + } } } diff --git a/Sources/Layout/MediaMessageSizeCalculator.swift b/Sources/Layout/MediaMessageSizeCalculator.swift new file mode 100644 index 00000000..82f03445 --- /dev/null +++ b/Sources/Layout/MediaMessageSizeCalculator.swift @@ -0,0 +1,48 @@ +/* + 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 + +open class MediaMessageSizeCalculator: MessageSizeCalculator { + + open override func messageContainerSize(for message: MessageType) -> CGSize { + let maxWidth = messageContainerMaxWidth(for: message) + let sizeForMediaItem = { (maxWidth: CGFloat, item: MediaItem) -> CGSize in + if maxWidth < item.size.width { + // Maintain the ratio if width is too great + let height = maxWidth * item.size.height / item.size.width + return CGSize(width: maxWidth, height: height) + } + return item.size + } + switch message.data { + case .photo(let item): + return sizeForMediaItem(maxWidth, item) + case .video(let item): + return sizeForMediaItem(maxWidth, item) + default: + fatalError("messageContainerSize received unhandled MessageDataType: \(message.data)") + } + } +} diff --git a/Sources/Layout/MessageSizeCalculator.swift b/Sources/Layout/MessageSizeCalculator.swift new file mode 100644 index 00000000..f107d42b --- /dev/null +++ b/Sources/Layout/MessageSizeCalculator.swift @@ -0,0 +1,203 @@ +/* + 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 + +open class MessageSizeCalculator: CellSizeCalculator { + + public init(layout: MessagesCollectionViewFlowLayout? = nil) { + super.init() + + self.layout = layout + } + + public var incomingAvatarSize = CGSize(width: 30, height: 30) + public var outgoingAvatarSize = CGSize(width: 30, height: 30) + + public var incomingAvatarPosition = AvatarPosition(vertical: .messageBottom) + public var outgoingAvatarPosition = AvatarPosition(vertical: .messageBottom) + + public var incomingMessagePadding = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) + public var outgoingMessagePadding = UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) + + public var incomingCellTopLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42)) + public var outgoingCellTopLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42)) + + public var incomingCellBottomLabelAlignment = LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(left: 42)) + public var outgoingCellBottomLabelAlignment = LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(right: 42)) + + open override func configure(attributes: UICollectionViewLayoutAttributes) { + guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return } + + let dataSource = messagesLayout.messagesDataSource + let indexPath = attributes.indexPath + let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView) + + attributes.avatarSize = avatarSize(for: message) + attributes.avatarPosition = avatarPosition(for: message) + + attributes.messageContainerPadding = messageContainerPadding(for: message) + attributes.messageContainerSize = messageContainerSize(for: message) + attributes.topLabelAlignment = cellTopLabelAlignment(for: message) + attributes.topLabelSize = cellTopLabelSize(for: message, at: indexPath) + + attributes.bottomLabelAlignment = cellBottomLabelAlignment(for: message) + attributes.bottomLabelSize = cellBottomLabelSize(for: message, at: indexPath) + } + + open override func sizeForItem(at indexPath: IndexPath) -> CGSize { + let dataSource = messagesLayout.messagesDataSource + let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView) + let itemHeight = cellContentHeight(for: message, at: indexPath) + return CGSize(width: messagesLayout.itemWidth, height: itemHeight) + } + + open func cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + + let avatarVerticalPosition = avatarPosition(for: message).vertical + let avatarHeight = avatarSize(for: message).height + let messageContainerHeight = messageContainerSize(for: message).height + let bottomLabelHeight = cellBottomLabelSize(for: message, at: indexPath).height + let topLabelHeight = cellTopLabelSize(for: message, at: indexPath).height + let messageVerticalPadding = messageContainerPadding(for: message).vertical + + var cellHeight: CGFloat = 0 + + switch avatarVerticalPosition { + case .cellTop: + cellHeight += max(avatarHeight, topLabelHeight) + cellHeight += bottomLabelHeight + cellHeight += messageContainerHeight + cellHeight += messageVerticalPadding + case .cellBottom: + cellHeight += max(avatarHeight, bottomLabelHeight) + cellHeight += topLabelHeight + cellHeight += messageContainerHeight + cellHeight += messageVerticalPadding + case .messageTop, .messageCenter, .messageBottom: + cellHeight += max(avatarHeight, messageContainerHeight) + cellHeight += messageVerticalPadding + cellHeight += topLabelHeight + cellHeight += bottomLabelHeight + } + + return cellHeight + } + + // MARK: - Avatar + + public func avatarPosition(for message: MessageType) -> AvatarPosition { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + var position = isFromCurrentSender ? outgoingAvatarPosition : incomingAvatarPosition + + switch position.horizontal { + case .cellTrailing, .cellLeading: + break + case .natural: + position.horizontal = isFromCurrentSender ? .cellTrailing : .cellLeading + } + return position + } + + public func avatarSize(for message: MessageType) -> CGSize { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingAvatarSize : incomingAvatarSize + } + + // MARK: - Top Label + + public 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 { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingCellTopLabelAlignment : incomingCellTopLabelAlignment + } + + // MARK: - Bottom Label + + public func cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let layoutDelegate = messagesLayout.messagesLayoutDelegate + let collectionView = messagesLayout.messagesCollectionView + let height = layoutDelegate.cellBottomLabelHeight(for: message, at: indexPath, in: collectionView) + return CGSize(width: messagesLayout.itemWidth, height: height) + } + + public func cellBottomLabelAlignment(for message: MessageType) -> LabelAlignment { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingCellBottomLabelAlignment : incomingCellBottomLabelAlignment + } + + // MARK: - MessageContainer + + public func messageContainerPadding(for message: MessageType) -> UIEdgeInsets { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingMessagePadding : incomingMessagePadding + } + + open func messageContainerSize(for message: MessageType) -> CGSize { + // Returns .zero by default + return .zero + } + + open func messageContainerMaxWidth(for message: MessageType) -> CGFloat { + let avatarWidth = avatarSize(for: message).width + let messagePadding = messageContainerPadding(for: message) + return messagesLayout.itemWidth - avatarWidth - messagePadding.horizontal + } + + // MARK: - Helpers + + public var messagesLayout: MessagesCollectionViewFlowLayout { + guard let layout = layout as? MessagesCollectionViewFlowLayout else { + fatalError("Layout object is missing or is not a MessagesCollectionViewFlowLayout") + } + return layout + } + + internal func labelSize(for attributedText: NSAttributedString, considering maxWidth: CGFloat) -> CGSize { + let estimatedHeight = attributedText.height(considering: maxWidth) + let estimatedWidth = attributedText.width(considering: estimatedHeight) + + let finalHeight = ceil(estimatedHeight) + let finalWidth = estimatedWidth > maxWidth ? maxWidth : ceil(estimatedWidth) + + return CGSize(width: finalWidth, height: finalHeight) + } +} + +fileprivate extension UIEdgeInsets { + init(top: CGFloat = 0, bottom: CGFloat = 0, left: CGFloat = 0, right: CGFloat = 0) { + self.init(top: top, left: left, bottom: bottom, right: right) + } +} diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout+Avatar.swift b/Sources/Layout/MessagesCollectionViewFlowLayout+Avatar.swift deleted file mode 100644 index c9299456..00000000 --- a/Sources/Layout/MessagesCollectionViewFlowLayout+Avatar.swift +++ /dev/null @@ -1,85 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Foundation - -// MARK: - Open - -extension MessagesCollectionViewFlowLayout { - - /// Returns the `AvatarPosition` for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - /// - Note: The default implementation of this method retrieves its value from - /// `avatarPosition(for:at:in)` in `MessagesLayoutDelegate`. - open func avatarPosition(for message: MessageType, at indexPath: IndexPath) -> AvatarPosition { - var position = messagesLayoutDelegate.avatarPosition(for: message, at: indexPath, in: messagesCollectionView) - - switch position.horizontal { - case .cellTrailing, .cellLeading: - break - case .natural: - position.horizontal = messagesDataSource.isFromCurrentSender(message: message) ? .cellTrailing : .cellLeading - } - return position - } - - /// Returns the `AvatarSize` for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - Note: The default implementation of this method retrieves its value from - /// `avatarSize(for:at:in)` in `MessagesLayoutDelegate`. - open func avatarSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - return messagesLayoutDelegate.avatarSize(for: message, at: indexPath, in: messagesCollectionView) - } -} - -// MARK: - Internal - -extension MessagesCollectionViewFlowLayout { - - internal func _avatarPosition(for message: MessageType, at indexPath: IndexPath) -> AvatarPosition { - - guard let cachedPosition = currentLayoutContext.avatarPosition else { - let position = avatarPosition(for: message, at: indexPath) - currentLayoutContext.avatarPosition = position - return position - } - return cachedPosition - } - - internal func _avatarSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - guard let cachedSize = currentLayoutContext.avatarSize else { - let size = avatarSize(for: message, at: indexPath) - currentLayoutContext.avatarSize = size - return size - } - return cachedSize - } -} diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout+CellSizing.swift b/Sources/Layout/MessagesCollectionViewFlowLayout+CellSizing.swift deleted file mode 100644 index 63c0f35a..00000000 --- a/Sources/Layout/MessagesCollectionViewFlowLayout+CellSizing.swift +++ /dev/null @@ -1,100 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Foundation - -// MARK: - Open - -extension MessagesCollectionViewFlowLayout { - - /// Returns the height of a `MessageCollectionViewCell`'s content at a given `IndexPath` - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - open func cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - let avatarPosition = _avatarPosition(for: message, at: indexPath) - let avatarHeight = _avatarSize(for: message, at: indexPath).height - let messageContainerHeight = _messageContainerSize(for: message, at: indexPath).height - let bottomLabelHeight = _cellBottomLabelSize(for: message, at: indexPath).height - let topLabelHeight = _cellTopLabelSize(for: message, at: indexPath).height - let messageVerticalPadding = _messageContainerPadding(for: message, at: indexPath).vertical - - var cellHeight: CGFloat = 0 - - switch avatarPosition.vertical { - case .cellTop: - cellHeight += max(avatarHeight, topLabelHeight) - cellHeight += bottomLabelHeight - cellHeight += messageContainerHeight - cellHeight += messageVerticalPadding - case .cellBottom: - cellHeight += max(avatarHeight, bottomLabelHeight) - cellHeight += topLabelHeight - cellHeight += messageContainerHeight - cellHeight += messageVerticalPadding - case .messageTop, .messageCenter, .messageBottom: - cellHeight += max(avatarHeight, messageContainerHeight) - cellHeight += messageVerticalPadding - cellHeight += topLabelHeight - cellHeight += bottomLabelHeight - } - - return cellHeight - } - - /// Returns the size for the `MessageCollectionViewCell` at a given `IndexPath` - /// considering all of the cell's content. - /// - /// - Parameters: - /// - indexPath: The `IndexPath` of the cell. - open func sizeForItem(at indexPath: IndexPath) -> CGSize { - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - let context = cellLayoutContext(for: message, at: indexPath) - guard let itemHeight = context.itemHeight else { - fatalError("Unexpectedly received a nil itemHeight") - } - return CGSize(width: itemWidth, height: itemHeight) - } -} - -// MARK: - Internal - -extension MessagesCollectionViewFlowLayout { - - internal func _cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - guard let cachedHeight = currentLayoutContext.itemHeight else { - let height = cellContentHeight(for: message, at: indexPath) - currentLayoutContext.itemHeight = height - return height - } - return cachedHeight - } - - internal func _sizeForItem(at indexPath: IndexPath) -> CGSize { - let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - let itemHeight = cellContentHeight(for: message, at: indexPath) - return CGSize(width: itemWidth, height: itemHeight) - } -} diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout+Labels.swift b/Sources/Layout/MessagesCollectionViewFlowLayout+Labels.swift deleted file mode 100644 index 04f437d5..00000000 --- a/Sources/Layout/MessagesCollectionViewFlowLayout+Labels.swift +++ /dev/null @@ -1,279 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Foundation - -// MARK: - Open - -extension MessagesCollectionViewFlowLayout { - - // Top Label - - /// Returns the `LabelAlignment` of the `MessageCollectionViewCell`'s top label - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - Note: The default implementation of this method retrieves its value from - /// `cellTopLabelAlignment(for:at:in)` in `MessagesLayoutDelegate`. - open func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { - return messagesLayoutDelegate.cellTopLabelAlignment(for: message, at: indexPath, in: messagesCollectionView) - } - - /// Returns the size of the `MessageCollectionViewCell`'s top label - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - /// - Note: The default implementation of this method sizes the label to fit. - open func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - let text = messagesDataSource.cellTopLabelAttributedText(for: message, at: indexPath) - guard let topLabelText = text else { return .zero } - let maxWidth = _cellTopLabelMaxWidth(for: message, at: indexPath) - return labelSize(for: topLabelText, considering: maxWidth) - } - - /// Returns the maximum width of the `MessageCollectionViewCell`'s top label - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - open func cellTopLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - let labelAlignment = _cellTopLabelAlignment(for: message, at: indexPath) - let avatarPosition = _avatarPosition(for: message, at: indexPath) - let avatarWidth = _avatarSize(for: message, at: indexPath).width - let messageContainerSize = _messageContainerSize(for: message, at: indexPath) - let messagePadding = _messageContainerPadding(for: message, at: indexPath) - - let avatarHorizontal = avatarPosition.horizontal - let avatarVertical = avatarPosition.vertical - - switch (labelAlignment, avatarHorizontal) { - - case (.cellLeading, _), (.cellTrailing, _): - let width = itemWidth - labelAlignment.insets.horizontal - return avatarVertical != .cellTop ? width : width - avatarWidth - - case (.cellCenter, _): - let width = itemWidth - labelAlignment.insets.horizontal - return avatarVertical != .cellTop ? width : width - (avatarWidth * 2) - - case (.messageTrailing, .cellLeading): - let width = messageContainerSize.width + messagePadding.left - labelAlignment.insets.horizontal - return avatarVertical == .cellTop ? width : width + avatarWidth - - case (.messageLeading, .cellTrailing): - let width = messageContainerSize.width + messagePadding.right - labelAlignment.insets.horizontal - return avatarVertical == .cellTop ? width : width + avatarWidth - - case (.messageLeading, .cellLeading): - return itemWidth - avatarWidth - messagePadding.left - labelAlignment.insets.horizontal - - case (.messageTrailing, .cellTrailing): - return itemWidth - avatarWidth - messagePadding.right - labelAlignment.insets.horizontal - - case (_, .natural): - fatalError(MessageKitError.avatarPositionUnresolved) - } - } - - // Bottom Label - - /// Returns the `LabelAlignment` of the `MessageCollectionViewCell`'s bottom label - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - Note: The default implementation of this method retrieves its value from - /// `cellBottomLabelAlignment(for:at:in)` in `MessagesLayoutDelegate`. - open func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { - return messagesLayoutDelegate.cellBottomLabelAlignment(for: message, at: indexPath, in: messagesCollectionView) - } - - /// Returns the size of the `MessageCollectionViewCell`'s bottom label - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - /// - Note: The default implementation of this method sizes the label to fit. - open func cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - let text = messagesDataSource.cellBottomLabelAttributedText(for: message, at: indexPath) - guard let bottomLabelText = text else { return .zero } - let maxWidth = _cellBottomLabelMaxWidth(for: message, at: indexPath) - return labelSize(for: bottomLabelText, considering: maxWidth) - } - - /// Returns the maximum width of the `MessageCollectionViewCell`'s bottom label - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - open func cellBottomLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - - let labelAlignment = _cellBottomLabelAlignment(for: message, at: indexPath) - let avatarWidth = _avatarSize(for: message, at: indexPath).width - let messageContainerSize = _messageContainerSize(for: message, at: indexPath) - let messagePadding = _messageContainerPadding(for: message, at: indexPath) - let avatarPosition = _avatarPosition(for: message, at: indexPath) - - let avatarHorizontal = avatarPosition.horizontal - let avatarVertical = avatarPosition.vertical - - switch (labelAlignment, avatarHorizontal) { - - case (.cellLeading, _), (.cellTrailing, _): - let width = itemWidth - labelAlignment.insets.horizontal - return avatarVertical != .cellBottom ? width : width - avatarWidth - - case (.cellCenter, _): - let width = itemWidth - labelAlignment.insets.horizontal - return avatarVertical != .cellBottom ? width : width - (avatarWidth * 2) - - case (.messageTrailing, .cellLeading): - let width = messageContainerSize.width + messagePadding.left - labelAlignment.insets.horizontal - return avatarVertical == .cellBottom ? width : width + avatarWidth - - case (.messageLeading, .cellTrailing): - let width = messageContainerSize.width + messagePadding.right - labelAlignment.insets.horizontal - return avatarVertical == .cellBottom ? width : width + avatarWidth - - case (.messageLeading, .cellLeading): - return itemWidth - avatarWidth - messagePadding.left - labelAlignment.insets.horizontal - - case (.messageTrailing, .cellTrailing): - return itemWidth - avatarWidth - messagePadding.right - labelAlignment.insets.horizontal - - case (_, .natural): - fatalError(MessageKitError.avatarPositionUnresolved) - } - } -} - -// MARK: - Internal - -extension MessagesCollectionViewFlowLayout { - - // Top Label - - internal func _cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { - guard let cachedAlignment = currentLayoutContext.topLabelAlignment else { - let alignment = cellTopLabelAlignment(for: message, at: indexPath) - currentLayoutContext.topLabelAlignment = alignment - return alignment - } - return cachedAlignment - } - - internal func _cellTopLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - guard let cachedMaxWidth = currentLayoutContext.topLabelMaxWidth else { - let maxWidth = cellTopLabelMaxWidth(for: message, at: indexPath) - currentLayoutContext.topLabelMaxWidth = maxWidth - return maxWidth - } - return cachedMaxWidth - } - - internal func _cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - guard let cachedSize = currentLayoutContext.topLabelSize else { - let size = cellTopLabelSize(for: message, at: indexPath) - currentLayoutContext.topLabelSize = size - return size - } - return cachedSize - } - - // Bottom Label - - internal func _cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath) -> LabelAlignment { - guard let cachedAlignment = currentLayoutContext.bottomLabelAlignment else { - let alignment = cellBottomLabelAlignment(for: message, at: indexPath) - currentLayoutContext.bottomLabelAlignment = alignment - return alignment - } - return cachedAlignment - } - - internal func _cellBottomLabelMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - guard let cachedMaxWidth = currentLayoutContext.bottomLabelMaxWidth else { - let maxWidth = cellBottomLabelMaxWidth(for: message, at: indexPath) - currentLayoutContext.bottomLabelMaxWidth = maxWidth - return maxWidth - } - return cachedMaxWidth - } - - internal func _cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - guard let cachedSize = currentLayoutContext.bottomLabelSize else { - let size = cellBottomLabelSize(for: message, at: indexPath) - currentLayoutContext.bottomLabelSize = size - return size - } - return cachedSize - } -} - -// MARK: - Helpers - -extension MessagesCollectionViewFlowLayout { - - /// Returns the size required fit a NSAttributedString considering a constrained max width. - /// - /// - Parameters: - /// - attributedText: The `NSAttributedString` used to calculate a size that fits. - /// - maxWidth: The max width available for the label. - internal func labelSize(for attributedText: NSAttributedString, considering maxWidth: CGFloat) -> CGSize { - - let estimatedHeight = attributedText.height(considering: maxWidth) - let estimatedWidth = attributedText.width(considering: estimatedHeight) - - let finalHeight = estimatedHeight.rounded(.up) - let finalWidth = estimatedWidth > maxWidth ? maxWidth : estimatedWidth.rounded(.up) - - return CGSize(width: finalWidth, height: finalHeight) - } - - /// Returns the size required to fit a String considering a constrained max width. - /// - /// - Parameters: - /// - text: The `String` used to calculate a size that fits. - /// - maxWidth: The max width available for the label. - internal func labelSize(for text: String, considering maxWidth: CGFloat, and font: UIFont) -> CGSize { - - let estimatedHeight = text.height(considering: maxWidth, and: font) - let estimatedWidth = text.width(considering: estimatedHeight, and: font) - - let finalHeight = estimatedHeight.rounded(.up) - let finalWidth = estimatedWidth > maxWidth ? maxWidth : estimatedWidth.rounded(.up) - - return CGSize(width: finalWidth, height: finalHeight) - } -} diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout+MessageContainerView.swift b/Sources/Layout/MessagesCollectionViewFlowLayout+MessageContainerView.swift deleted file mode 100644 index 33125f25..00000000 --- a/Sources/Layout/MessagesCollectionViewFlowLayout+MessageContainerView.swift +++ /dev/null @@ -1,158 +0,0 @@ -/* - MIT License - - Copyright (c) 2017-2018 MessageKit - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Foundation - -// MARK: - Open - -extension MessagesCollectionViewFlowLayout { - - /// Returns the insets of the `MessageLabel` in a `TextMessageCell` for - /// the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - /// - Note: The default implementation of this method retrieves its value from - /// `messageLabelInset(for:at:in)` in `MessagesLayoutDelegate`. - open func messageLabelInsets(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { - return messagesLayoutDelegate.messageLabelInset(for: message, at: indexPath, in: messagesCollectionView) - } - - /// Returns the padding around the `MessageContainerView` in a `MessageCollectionViewCell` - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - /// - /// - Note: The default implementation of this method retrieves its value from - /// `messagePadding(for:at:in)` in `MessagesLayoutDelegate`. - open func messageContainerPadding(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { - return messagesLayoutDelegate.messagePadding(for: message, at: indexPath, in: messagesCollectionView) - } - - /// Returns the maximum width of the `MessageContainerView` in a `MessageCollectionViewCell` - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - open func messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - let avatarWidth = _avatarSize(for: message, at: indexPath).width - let messagePadding = _messageContainerPadding(for: message, at: indexPath) - - switch message.data { - case .text, .attributedText: - let messageInsets = _messageLabelInsets(for: message, at: indexPath) - return itemWidth - avatarWidth - messagePadding.horizontal - messageInsets.horizontal - default: - return itemWidth - avatarWidth - messagePadding.horizontal - } - } - - /// Returns the size of the `MessageContainerView` in a `MessageCollectionViewCell` - /// for the `MessageType` at a given `IndexPath`. - /// - /// - Parameters: - /// - message: The `MessageType` for the given `IndexPath`. - /// - indexPath: The `IndexPath` for the given `MessageType`. - open func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - let maxWidth = _messageContainerMaxWidth(for: message, at: indexPath) - - var messageContainerSize: CGSize = .zero - - switch message.data { - case .text(let text): - let messageLabelInsets = _messageLabelInsets(for: message, at: indexPath) - messageContainerSize = labelSize(for: text, considering: maxWidth, and: messageLabelFont) - messageContainerSize.width += messageLabelInsets.horizontal - messageContainerSize.height += messageLabelInsets.vertical - case .attributedText(let text): - let messageLabelInsets = _messageLabelInsets(for: message, at: indexPath) - messageContainerSize = labelSize(for: text, considering: maxWidth) - messageContainerSize.width += messageLabelInsets.horizontal - messageContainerSize.height += messageLabelInsets.vertical - case .emoji(let text): - let messageLabelInsets = _messageLabelInsets(for: message, at: indexPath) - messageContainerSize = labelSize(for: text, considering: maxWidth, and: emojiLabelFont) - messageContainerSize.width += messageLabelInsets.horizontal - messageContainerSize.height += messageLabelInsets.vertical - case .photo, .video: - let width = messagesLayoutDelegate.widthForMedia(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) - let height = messagesLayoutDelegate.heightForMedia(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) - messageContainerSize = CGSize(width: width, height: height) - case .location: - let width = messagesLayoutDelegate.widthForLocation(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) - let height = messagesLayoutDelegate.heightForLocation(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView) - messageContainerSize = CGSize(width: width, height: height) - case .custom: - fatalError(MessageKitError.customDataUnresolvedSize) - } - - return messageContainerSize - } -} - -// MARK: - Internal - -extension MessagesCollectionViewFlowLayout { - - internal func _messageLabelInsets(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { - guard let cachedInsets = currentLayoutContext.messageLabelInsets else { - let insets = messageLabelInsets(for: message, at: indexPath) - currentLayoutContext.messageLabelInsets = insets - return insets - } - return cachedInsets - } - - internal func _messageContainerPadding(for message: MessageType, at indexPath: IndexPath) -> UIEdgeInsets { - guard let cachedPadding = currentLayoutContext.messageContainerPadding else { - let padding = messageContainerPadding(for: message, at: indexPath) - currentLayoutContext.messageContainerPadding = padding - return padding - } - return cachedPadding - } - - internal func _messageContainerMaxWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - guard let cachedMaxWidth = currentLayoutContext.messageContainerMaxWidth else { - let maxWidth = messageContainerMaxWidth(for: message, at: indexPath) - currentLayoutContext.messageContainerMaxWidth = maxWidth - return maxWidth - } - return cachedMaxWidth - } - - internal func _messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - guard let cachedSize = currentLayoutContext.messageContainerSize else { - let size = messageContainerSize(for: message, at: indexPath) - currentLayoutContext.messageContainerSize = size - return size - } - return cachedSize - } -} diff --git a/Sources/Layout/MessagesCollectionViewFlowLayout.swift b/Sources/Layout/MessagesCollectionViewFlowLayout.swift index ba50efed..7612e957 100644 --- a/Sources/Layout/MessagesCollectionViewFlowLayout.swift +++ b/Sources/Layout/MessagesCollectionViewFlowLayout.swift @@ -32,52 +32,39 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { open override class var layoutAttributesClass: AnyClass { return MessagesCollectionViewLayoutAttributes.self } + + /** The `MessagesCollectionView` that owns this layout object. */ + public var messagesCollectionView: MessagesCollectionView { + guard let messagesCollectionView = collectionView as? MessagesCollectionView else { + fatalError(MessageKitError.layoutUsedOnForeignType) + } + return messagesCollectionView + } + + /** The `MessagesDataSource` for the layout's collection view. */ + public var messagesDataSource: MessagesDataSource { + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) + } + return messagesDataSource + } + + /** The `MessagesLayoutDelegate` for the layout's collection view. */ + public var messagesLayoutDelegate: MessagesLayoutDelegate { + guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { + fatalError(MessageKitError.nilMessagesLayoutDelegate) + } + return messagesLayoutDelegate + } - /// The width of an item in the `MessagesCollectionView`. - internal var itemWidth: CGFloat { + public var itemWidth: CGFloat { guard let collectionView = collectionView else { return 0 } return collectionView.frame.width - sectionInset.left - sectionInset.right } - /// The font to be used by `TextMessageCell` for `MessageData.text(String)` case. - /// - /// Note: The default value of this property is `UIFont.preferredFont(forTextStyle: .body)` - open var messageLabelFont: UIFont { - didSet { - emojiLabelFont = messageLabelFont.withSize(2 * messageLabelFont.pointSize) - } - } - - /// The font to be used by `TextMessageCell` for `MessageData.emoji(String)` case. - /// - /// Note: The default value of this property is 2x the `messageLabelFont`. - internal var emojiLabelFont: UIFont - - /// Determines the maximum number of `MessageCollectionViewCell` attributes to cache. - /// - /// Note: The default value of this property is 500. - open var attributesCacheMaxSize: Int = 500 { - didSet { - layoutContextCache.countLimit = attributesCacheMaxSize - } - } - - typealias MessageID = NSString - - /// The cache for `MessageCellLayoutContext`. - /// The key is the `messageId` of the `MessageType`. - fileprivate var layoutContextCache = NSCache() - - /// The `MessageCellLayoutContext` for the current cell. - internal var currentLayoutContext: MessageCellLayoutContext! - // MARK: - Initializers public override init() { - - messageLabelFont = UIFont.preferredFont(forTextStyle: .body) - emojiLabelFont = messageLabelFont.withSize(2 * messageLabelFont.pointSize) - super.init() sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) @@ -92,40 +79,35 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { deinit { NotificationCenter.default.removeObserver(self) } -} -// MARK: - Cache Invalidation + // MARK: - Attributes -extension MessagesCollectionViewFlowLayout { - - /// Removes the cached layout information for a given `MessageType`. - /// - /// - Parameters: - /// - message: The `MessageType` whose cached layout information is to be removed. - public func removeCachedAttributes(for message: MessageType) { - removeCachedAttributes(for: message.messageId) + open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + guard let attributesArray = super.layoutAttributesForElements(in: rect) as? [MessagesCollectionViewLayoutAttributes] else { + return nil + } + for attributes in attributesArray where attributes.representedElementCategory == .cell { + let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath) + cellSizeCalculator.configure(attributes: attributes) + } + return attributesArray } - /// Removes the cached layout information for a `MessageType` with a given `messageId`. - /// - /// - Parameters: - /// - messageId: The id for the `MessageType` whose cached layout information is to be removed. - public func removeCachedAttributes(for messageId: String) { - layoutContextCache.removeObject(forKey: messageId as NSString) + open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { + guard let attributes = super.layoutAttributesForItem(at: indexPath) as? MessagesCollectionViewLayoutAttributes else { + return nil + } + if attributes.representedElementCategory == .cell { + let cellSizeCalculator = cellSizeCalculatorForItem(at: attributes.indexPath) + cellSizeCalculator.configure(attributes: attributes) + } + return attributes } - /// Removes the cached layout information for all `MessageType`s. - public func removeAllCachedAttributes() { - layoutContextCache.removeAllObjects() - } + // MARK: - Layout Invalidation open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { - if collectionView?.bounds.width != newBounds.width { - removeAllCachedAttributes() - return true - } else { - return false - } + return collectionView?.bounds.width != newBounds.width } open override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext { @@ -135,137 +117,42 @@ extension MessagesCollectionViewFlowLayout { return flowLayoutContext } - /// Invalidates the layout and removes all cached attributes on device orientation change @objc private func handleOrientationChange(_ notification: Notification) { - removeAllCachedAttributes() invalidateLayout() } -} -// MARK: - MessagesCollectionViewLayoutAttributes + // MARK: - Cell Sizing -extension MessagesCollectionViewFlowLayout { + lazy open var textMessageSizeCalculator = TextMessageSizeCalculator(layout: self) + lazy open var attributedTextMessageSizeCalculator = TextMessageSizeCalculator(layout: self) + lazy open var emojiMessageSizeCalculator = TextMessageSizeCalculator(layout: self) + lazy open var photoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self) + lazy open var videoMessageSizeCalculator = MediaMessageSizeCalculator(layout: self) + lazy open var locationMessageSizeCalculator = LocationMessageSizeCalculator(layout: self) - open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { - - guard let attributesArray = super.layoutAttributesForElements(in: rect) as? [MessagesCollectionViewLayoutAttributes] else { return nil } - - attributesArray.forEach { attributes in - if attributes.representedElementCategory == UICollectionElementCategory.cell { - configure(attributes: attributes) - } - } - - return attributesArray - } - - open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { - - guard let attributes = super.layoutAttributesForItem(at: indexPath) as? MessagesCollectionViewLayoutAttributes else { return nil } - - if attributes.representedElementCategory == UICollectionElementCategory.cell { - configure(attributes: attributes) - } - - return attributes - - } - - private func configure(attributes: MessagesCollectionViewLayoutAttributes) { - - let indexPath = attributes.indexPath + open func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator { let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) - let context = cellLayoutContext(for: message, at: indexPath) - - attributes.avatarSize = context.avatarSize! - attributes.avatarPosition = context.avatarPosition! - - attributes.messageContainerPadding = context.messageContainerPadding! - attributes.messageContainerSize = context.messageContainerSize! - attributes.messageLabelInsets = context.messageLabelInsets! - - attributes.topLabelAlignment = context.topLabelAlignment! - attributes.topLabelSize = context.topLabelSize! - - attributes.bottomLabelAlignment = context.bottomLabelAlignment! - attributes.bottomLabelSize = context.bottomLabelSize! - switch message.data { - case .emoji: - attributes.messageLabelFont = emojiLabelFont case .text: - attributes.messageLabelFont = messageLabelFont - case .attributedText(let text): - guard !text.string.isEmpty else { return } - guard let font = text.attribute(.font, at: 0, effectiveRange: nil) as? UIFont else { return } - attributes.messageLabelFont = font - default: - break + return textMessageSizeCalculator + case .attributedText: + return attributedTextMessageSizeCalculator + case .emoji: + return emojiMessageSizeCalculator + case .photo: + return photoMessageSizeCalculator + case .video: + return videoMessageSizeCalculator + case .location: + return locationMessageSizeCalculator + case .custom: + fatalError("Must return a CellSizeCalculator for MessageData.custom(Any?)") } } -} -// MARK: - MessageCellLayoutContext - -extension MessagesCollectionViewFlowLayout { - - internal func cellLayoutContext(for message: MessageType, at indexPath: IndexPath) -> MessageCellLayoutContext { - guard let cachedContext = layoutContextCache.object(forKey: message.messageId as NSString) else { - let newContext = newCellLayoutContext(for: message, at: indexPath) - - if messagesLayoutDelegate.shouldCacheLayoutAttributes(for: message) { - layoutContextCache.setObject(newContext, forKey: message.messageId as NSString) - } - return newContext - } - return cachedContext - } - - internal func newCellLayoutContext(for message: MessageType, at indexPath: IndexPath) -> MessageCellLayoutContext { - currentLayoutContext = MessageCellLayoutContext() - currentLayoutContext.avatarPosition = _avatarPosition(for: message, at: indexPath) - currentLayoutContext.avatarSize = _avatarSize(for: message, at: indexPath) - currentLayoutContext.messageContainerPadding = _messageContainerPadding(for: message, at: indexPath) - currentLayoutContext.messageLabelInsets = _messageLabelInsets(for: message, at: indexPath) - currentLayoutContext.messageContainerMaxWidth = _messageContainerMaxWidth(for: message, at: indexPath) - currentLayoutContext.messageContainerSize = _messageContainerSize(for: message, at: indexPath) - currentLayoutContext.topLabelAlignment = _cellTopLabelAlignment(for: message, at: indexPath) - currentLayoutContext.topLabelMaxWidth = _cellTopLabelMaxWidth(for: message, at: indexPath) - currentLayoutContext.topLabelSize = _cellTopLabelSize(for: message, at: indexPath) - currentLayoutContext.bottomLabelAlignment = _cellBottomLabelAlignment(for: message, at: indexPath) - currentLayoutContext.bottomLabelMaxWidth = _cellBottomLabelMaxWidth(for: message, at: indexPath) - currentLayoutContext.bottomLabelSize = _cellBottomLabelSize(for: message, at: indexPath) - currentLayoutContext.itemHeight = _cellContentHeight(for: message, at: indexPath) - return currentLayoutContext - } -} - -// MARK: - Helpers - -extension MessagesCollectionViewFlowLayout { - - /// Convenience property for accessing the layout object's `MessagesCollectionView`. - internal var messagesCollectionView: MessagesCollectionView { - guard let messagesCollectionView = collectionView as? MessagesCollectionView else { - fatalError(MessageKitError.layoutUsedOnForeignType) - } - return messagesCollectionView - } - - /// Convenience property for unwrapping the `MessagesCollectionView`'s `MessagesDataSource`. - internal var messagesDataSource: MessagesDataSource { - guard let messagesDataSource = messagesCollectionView.messagesDataSource else { - fatalError(MessageKitError.nilMessagesDataSource) - } - return messagesDataSource - } - - /// Convenience property for unwrapping the `MessagesCollectionView`'s `MessagesLayoutDelegate`. - internal var messagesLayoutDelegate: MessagesLayoutDelegate { - guard let messagesLayoutDelegate = messagesCollectionView.messagesLayoutDelegate else { - fatalError(MessageKitError.nilMessagesLayoutDelegate) - } - return messagesLayoutDelegate + open func sizeForItem(at indexPath: IndexPath) -> CGSize { + let calculator = cellSizeCalculatorForItem(at: indexPath) + return calculator.sizeForItem(at: indexPath) } } diff --git a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift index 60944c73..16a90399 100644 --- a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift +++ b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift @@ -29,19 +29,19 @@ open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttribu // MARK: - Properties - var avatarSize: CGSize = .zero - var avatarPosition = AvatarPosition(vertical: .cellBottom) + public var avatarSize: CGSize = .zero + public var avatarPosition = AvatarPosition(vertical: .cellBottom) - var messageContainerSize: CGSize = .zero - var messageContainerPadding: UIEdgeInsets = .zero - var messageLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .body) - var messageLabelInsets: UIEdgeInsets = .zero + public var messageContainerSize: CGSize = .zero + public var messageContainerPadding: UIEdgeInsets = .zero + public var messageLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .body) + public var messageLabelInsets: UIEdgeInsets = .zero - var topLabelAlignment = LabelAlignment.cellCenter(.zero) - var topLabelSize: CGSize = .zero + public var topLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) + public var topLabelSize: CGSize = .zero - var bottomLabelAlignment = LabelAlignment.cellCenter(.zero) - var bottomLabelSize: CGSize = .zero + public var bottomLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) + public var bottomLabelSize: CGSize = .zero // MARK: - Methods @@ -63,14 +63,20 @@ open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttribu } open override func isEqual(_ object: Any?) -> Bool { - // MARK: - LEAVE this as is - // swiftlint:disable unused_optional_binding - if let _ = object as? MessagesCollectionViewLayoutAttributes { - return super.isEqual(object) + if let attributes = object as? MessagesCollectionViewLayoutAttributes { + return super.isEqual(object) && attributes.avatarSize == avatarSize + && attributes.avatarPosition == attributes.avatarPosition + && attributes.messageContainerSize == messageContainerSize + && attributes.messageContainerPadding == messageContainerPadding + && attributes.messageLabelFont == messageLabelFont + && attributes.messageLabelInsets == messageLabelInsets + && attributes.topLabelAlignment == topLabelAlignment + && attributes.topLabelSize == topLabelSize + && attributes.bottomLabelAlignment == bottomLabelAlignment + && attributes.bottomLabelSize == bottomLabelSize } else { return false } - // swiftlint:enable unused_optional_binding } } diff --git a/Sources/Layout/TextMessageSizeCalculator.swift b/Sources/Layout/TextMessageSizeCalculator.swift new file mode 100644 index 00000000..0d24c6b4 --- /dev/null +++ b/Sources/Layout/TextMessageSizeCalculator.swift @@ -0,0 +1,90 @@ +/* + 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 + +open class TextMessageSizeCalculator: MessageSizeCalculator { + + public var incomingMessageLabelInsets = UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14) + public var outgoingMessageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18) + + public var messageLabelFont = UIFont.preferredFont(forTextStyle: .body) + + internal func messageLabelInsets(for message: MessageType) -> UIEdgeInsets { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender ? outgoingMessageLabelInsets : incomingMessageLabelInsets + } + + open override func messageContainerMaxWidth(for message: MessageType) -> CGFloat { + let maxWidth = super.messageContainerMaxWidth(for: message) + let textInsets = messageLabelInsets(for: message) + return maxWidth - textInsets.horizontal + } + + open override func messageContainerSize(for message: MessageType) -> CGSize { + let maxWidth = messageContainerMaxWidth(for: message) + + var messageContainerSize: CGSize + let attributedText: NSAttributedString + + switch message.data { + case .attributedText(let text): + attributedText = text + case .text(let text), .emoji(let text): + attributedText = NSAttributedString(string: text, attributes: [.font: messageLabelFont]) + default: + fatalError("messageContainerSize received unhandled MessageDataType: \(message.data)") + } + + messageContainerSize = labelSize(for: attributedText, considering: maxWidth) + + let messageInsets = messageLabelInsets(for: message) + messageContainerSize.width += messageInsets.horizontal + messageContainerSize.height += messageInsets.vertical + + return messageContainerSize + } + + open override func configure(attributes: UICollectionViewLayoutAttributes) { + super.configure(attributes: attributes) + guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return } + + let dataSource = messagesLayout.messagesDataSource + let indexPath = attributes.indexPath + let message = dataSource.messageForItem(at: indexPath, in: messagesLayout.messagesCollectionView) + + attributes.messageLabelInsets = messageLabelInsets(for: message) + attributes.messageLabelFont = messageLabelFont + + switch message.data { + case .attributedText(let text): + guard !text.string.isEmpty else { return } + guard let font = text.attribute(.font, at: 0, effectiveRange: nil) as? UIFont else { return } + attributes.messageLabelFont = font + default: + break + } + } +} diff --git a/Sources/Models/AvatarPosition.swift b/Sources/Models/AvatarPosition.swift index 35ea380c..4ea54f24 100644 --- a/Sources/Models/AvatarPosition.swift +++ b/Sources/Models/AvatarPosition.swift @@ -82,3 +82,13 @@ public struct AvatarPosition { } } + +// MARK: - Equatable Conformance + +extension AvatarPosition: Equatable { + + public static func == (lhs: AvatarPosition, rhs: AvatarPosition) -> Bool { + return lhs.vertical == rhs.vertical && lhs.horizontal == rhs.horizontal + } + +} diff --git a/Sources/Models/DetectorType.swift b/Sources/Models/DetectorType.swift index 20ea43f7..a49b7b6d 100644 --- a/Sources/Models/DetectorType.swift +++ b/Sources/Models/DetectorType.swift @@ -38,7 +38,7 @@ public enum DetectorType { //case hashtag //case custom - var textCheckingType: NSTextCheckingResult.CheckingType { + internal var textCheckingType: NSTextCheckingResult.CheckingType { switch self { case .address: return .address case .date: return .date diff --git a/Sources/Models/LabelAlignment.swift b/Sources/Models/LabelAlignment.swift index b2d66361..b75838de 100644 --- a/Sources/Models/LabelAlignment.swift +++ b/Sources/Models/LabelAlignment.swift @@ -24,38 +24,19 @@ import UIKit -/// An enum represnting the horizontal alignment of a `MessageCollectionViewCell`'s top and bottom labels. -public enum LabelAlignment { +public struct LabelAlignment { - /// Aligns the label's trailing edge to the cell's trailing edge. - /// The `UIEdgeInsets` associated value represents the offset from this position. - case cellTrailing(UIEdgeInsets) - - /// Aligns the label's leading edge to the cell's leading edge. - /// The `UIEdgeInsets` associated value represents the offset from this position. - case cellLeading(UIEdgeInsets) - - /// Aligns the label's center to the cell's center. - /// The `UIEdgeInsets` associated value represents the offset from this position. - case cellCenter(UIEdgeInsets) - - /// Aligns the label's trailing edge to the `MessageContainerView`'s trailing edge. - /// The `UIEdgeInsets` associated value represents the offset from this position. - case messageTrailing(UIEdgeInsets) - - /// Aligns the label's leading edge to the `MessageContainerView`'s leading edge. - /// The `UIEdgeInsets` associated value represents the offset from this position. - case messageLeading(UIEdgeInsets) + public var textAlignment: NSTextAlignment + public var textInsets: UIEdgeInsets - /// Returns the `UIEdgeInsets` associated value for the `LabelAlignment` case. - public var insets: UIEdgeInsets { - switch self { - case .cellTrailing(let insets): return insets - case .cellLeading(let insets): return insets - case .cellCenter(let insets): return insets - case .messageTrailing(let insets): return insets - case .messageLeading(let insets): return insets - } +} + +// MARK: - Equatable Conformance + +extension LabelAlignment: Equatable { + + public static func == (lhs: LabelAlignment, rhs: LabelAlignment) -> Bool { + return lhs.textAlignment == rhs.textAlignment && lhs.textInsets == rhs.textInsets } } diff --git a/Sources/Models/MessageData.swift b/Sources/Models/MessageData.swift index 2bb72bae..25f619ad 100644 --- a/Sources/Models/MessageData.swift +++ b/Sources/Models/MessageData.swift @@ -23,7 +23,6 @@ */ import Foundation -import class CoreLocation.CLLocation /// An enum representing the kind of message and its underlying data. public enum MessageData { @@ -41,13 +40,13 @@ public enum MessageData { case attributedText(NSAttributedString) /// A photo message. - case photo(UIImage) + case photo(MediaItem) /// A video message. - case video(file: URL, thumbnail: UIImage) + case video(MediaItem) /// A location message. - case location(CLLocation) + case location(LocationItem) /// An emoji message. case emoji(String) @@ -55,7 +54,7 @@ public enum MessageData { /// A custom message. /// - Note: Using this case requires that you override the following methods and handle this case: /// - `collectionView(_:cellForItemAt indexPath: IndexPath) -> UICollectionViewCell` - /// - `messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize` + /// - `cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator` case custom(Any?) // MARK: - Not supported yet diff --git a/Sources/Models/MessageKitError.swift b/Sources/Models/MessageKitError.swift index 5d2760ba..d4f3a5a4 100644 --- a/Sources/Models/MessageKitError.swift +++ b/Sources/Models/MessageKitError.swift @@ -22,17 +22,17 @@ 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 nilMessagesLayoutDelegate = "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." - static let customDataUnresolvedCell = "Did not return a cell for MessageData.custom(Any)." - static let customDataUnresolvedSize = "Did not return a size for MessageData.custom(Any)." +internal enum MessageKitError { + internal static let avatarPositionUnresolved = "AvatarPosition Horizontal.natural needs to be resolved." + internal static let nilMessagesDataSource = "MessagesDataSource has not been set." + internal static let nilMessagesDisplayDelegate = "MessagesDisplayDelegate has not been set." + internal static let nilMessagesLayoutDelegate = "MessagesLayoutDelegate has not been set." + internal static let notMessagesCollectionView = "The collectionView is not a MessagesCollectionView." + internal static let layoutUsedOnForeignType = "MessagesCollectionViewFlowLayout is being used on a foreign type." + internal static let unrecognizedSectionKind = "Received unrecognized element kind:" + internal static let unrecognizedCheckingResult = "Received an unrecognized NSTextCheckingResult.CheckingType" + internal static let couldNotLoadAssetsBundle = "MessageKit: Could not load the assets bundle" + internal static let couldNotCreateAssetsPath = "MessageKit: Could not create path to the assets bundle." + internal static let customDataUnresolvedCell = "Did not return a cell for MessageData.custom(Any)." + internal static let customDataUnresolvedSize = "Did not return a size for MessageData.custom(Any)." } diff --git a/Sources/Models/MessageStyle.swift b/Sources/Models/MessageStyle.swift index 6c8e2451..22e9c684 100644 --- a/Sources/Models/MessageStyle.swift +++ b/Sources/Models/MessageStyle.swift @@ -35,7 +35,7 @@ public enum MessageStyle { case topRight case bottomRight - var imageOrientation: UIImageOrientation { + internal var imageOrientation: UIImageOrientation { switch self { case .bottomRight: return .up case .bottomLeft: return .upMirrored @@ -52,7 +52,7 @@ public enum MessageStyle { case curved case pointedEdge - var imageNameSuffix: String { + internal var imageNameSuffix: String { switch self { case .curved: return "_tail_v2" diff --git a/Sources/Models/NSConstraintLayoutSet.swift b/Sources/Models/NSConstraintLayoutSet.swift index ff5b4679..14922ee8 100644 --- a/Sources/Models/NSConstraintLayoutSet.swift +++ b/Sources/Models/NSConstraintLayoutSet.swift @@ -24,18 +24,18 @@ import UIKit -public class NSLayoutConstraintSet { +internal class NSLayoutConstraintSet { - public var top: NSLayoutConstraint? - public var bottom: NSLayoutConstraint? - public var left: NSLayoutConstraint? - public var right: NSLayoutConstraint? - public var centerX: NSLayoutConstraint? - public var centerY: NSLayoutConstraint? - public var width: NSLayoutConstraint? - public var height: NSLayoutConstraint? + internal var top: NSLayoutConstraint? + internal var bottom: NSLayoutConstraint? + internal var left: NSLayoutConstraint? + internal var right: NSLayoutConstraint? + internal var centerX: NSLayoutConstraint? + internal var centerY: NSLayoutConstraint? + internal var width: NSLayoutConstraint? + internal var height: NSLayoutConstraint? - public init(top: NSLayoutConstraint? = nil, bottom: NSLayoutConstraint? = nil, + internal init(top: NSLayoutConstraint? = nil, bottom: NSLayoutConstraint? = nil, left: NSLayoutConstraint? = nil, right: NSLayoutConstraint? = nil, centerX: NSLayoutConstraint? = nil, centerY: NSLayoutConstraint? = nil, width: NSLayoutConstraint? = nil, height: NSLayoutConstraint? = nil) { @@ -59,7 +59,7 @@ public class NSLayoutConstraintSet { /// /// - Returns: Self @discardableResult - func activate() -> Self { + internal func activate() -> Self { NSLayoutConstraint.activate(availableConstraints) return self } @@ -68,7 +68,7 @@ public class NSLayoutConstraintSet { /// /// - Returns: Self @discardableResult - func deactivate() -> Self { + internal func deactivate() -> Self { NSLayoutConstraint.deactivate(availableConstraints) return self } diff --git a/Sources/Protocols/LocationItem.swift b/Sources/Protocols/LocationItem.swift new file mode 100644 index 00000000..5cb12b5d --- /dev/null +++ b/Sources/Protocols/LocationItem.swift @@ -0,0 +1,36 @@ +/* + 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 class CoreLocation.CLLocation + +/// A protocol used to represent the data for a location message. +public protocol LocationItem { + + /// The location. + var location: CLLocation { get set } + + /// The size of the location item. + var size: CGSize { get set } + +} diff --git a/Sources/Layout/MessageCellLayoutContext.swift b/Sources/Protocols/MediaItem.swift similarity index 63% rename from Sources/Layout/MessageCellLayoutContext.swift rename to Sources/Protocols/MediaItem.swift index 693c976f..10acfe12 100644 --- a/Sources/Layout/MessageCellLayoutContext.swift +++ b/Sources/Protocols/MediaItem.swift @@ -24,25 +24,19 @@ import Foundation -/// An internal type used to store the attributes used to calculate -/// the size of a `MessageCollectionViewCell`. -final class MessageCellLayoutContext { +/// A protocol used to represent the data for a media message. +public protocol MediaItem { - var itemHeight: CGFloat? + /// The url where the media is located. + var url: URL? { get set } - var avatarSize: CGSize? - var avatarPosition: AvatarPosition? + /// The image. + var image: UIImage? { get set } - var messageContainerSize: CGSize? - var messageContainerMaxWidth: CGFloat? - var messageContainerPadding: UIEdgeInsets? - var messageLabelInsets: UIEdgeInsets? + /// A placeholder image for when the image is obtained asychronously. + var placeholderImage: UIImage { get set } - var bottomLabelAlignment: LabelAlignment? - var bottomLabelSize: CGSize? - var bottomLabelMaxWidth: CGFloat? + /// The size of the media item. + var size: CGSize { get set } - var topLabelAlignment: LabelAlignment? - var topLabelSize: CGSize? - var topLabelMaxWidth: CGFloat? } diff --git a/Sources/Protocols/MessageCellDelegate.swift b/Sources/Protocols/MessageCellDelegate.swift index 8a4e686f..80b4432b 100644 --- a/Sources/Protocols/MessageCellDelegate.swift +++ b/Sources/Protocols/MessageCellDelegate.swift @@ -24,43 +24,44 @@ import Foundation -/// A protocol used by `MessageCollectionViewCell` subclasses to detect taps in the cell's contents. +/// A protocol used by `MessageContentCell` subclasses to detect taps in the cell's subviews. public protocol MessageCellDelegate: MessageLabelDelegate { - /// Triggered when a touch occurs in the `MessageContainerView`. + /// Triggered when a tap occurs in the `MessageContainerView`. /// /// - Parameters: - /// - cell: The cell where the touch occurred. + /// - cell: The cell where the tap occurred. /// + /// - Note: /// 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 didTapMessage(in cell: MessageCollectionViewCell) - /// Triggered when a touch occurs in the `AvatarView`. + /// Triggered when a tap occurs in the `AvatarView`. /// /// - Parameters: - /// - cell: The cell where the touch occurred. + /// - 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 didTapAvatar(in cell: MessageCollectionViewCell) - /// Triggered when a touch occurs in the cellBottomLabel. + /// Triggered when a tap occurs in the cellBottomLabel. /// /// - Parameters: - /// - cell: The cell where the touch occurred. + /// - 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 didTapBottomLabel(in cell: MessageCollectionViewCell) - /// Triggered when a touch occurs in the cellTopLabel. + /// Triggered when a tap occurs in the cellTopLabel. /// /// - Parameters: - /// - cell: The cell where the touch occurred. + /// - cell: The cell tap the touch 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` diff --git a/Sources/Protocols/MessageInputBarDelegate.swift b/Sources/Protocols/MessageInputBarDelegate.swift index 8c680f27..39c5b180 100644 --- a/Sources/Protocols/MessageInputBarDelegate.swift +++ b/Sources/Protocols/MessageInputBarDelegate.swift @@ -24,26 +24,27 @@ import Foundation -/// MessageInputBarDelegate is a protocol that can recieve notifications from the MessageInputBar +/// A protocol that can receive different event notifications from the MessageInputBar. public protocol MessageInputBarDelegate: AnyObject { - /// Called when the default send button has been selected + /// Called when the default send button has been selected. /// /// - Parameters: - /// - inputBar: The MessageInputBar - /// - text: The current text in the MessageInputBar's InputTextView + /// - inputBar: The `MessageInputBar`. + /// - text: The current text in the `InputTextView` of the `MessageInputBar`. func messageInputBar(_ inputBar: MessageInputBar, didPressSendButtonWith text: String) - /// Called when the instrinsicContentSize of the MessageInputBar has changed. Can be used for adjusting content insets - /// on other views to make sure the MessageInputBar does not cover up any other view + /// Called when the instrinsicContentSize of the MessageInputBar has changed. + /// Can be used for adjusting content insets on other views to make sure + /// the MessageInputBar does not cover up any other view. /// /// - Parameters: - /// - inputBar: The MessageInputBar - /// - size: The new instrinsicContentSize + /// - inputBar: The `MessageInputBar`. + /// - size: The new instrinsic content size. func messageInputBar(_ inputBar: MessageInputBar, didChangeIntrinsicContentTo size: CGSize) - /// Called when the MessageInputBar's InputTextView's text has changed. Useful for adding your own logic without the - /// need of assigning a delegate or notification + /// Called when the `MessageInputBar`'s `InputTextView`'s text has changed. + /// Useful for adding your own logic without the need of assigning a delegate or notification. /// /// - Parameters: /// - inputBar: The MessageInputBar diff --git a/Sources/Protocols/MessageLabelDelegate.swift b/Sources/Protocols/MessageLabelDelegate.swift index e19c9cf1..e84ea622 100644 --- a/Sources/Protocols/MessageLabelDelegate.swift +++ b/Sources/Protocols/MessageLabelDelegate.swift @@ -24,16 +24,37 @@ import Foundation +/// A protocol used to handle tap events on detected text. public protocol MessageLabelDelegate: AnyObject { + /// Triggered when a tap occurs on a detected address. + /// + /// - Parameters: + /// - addressComponents: The components of the selected address. func didSelectAddress(_ addressComponents: [String: String]) + /// Triggered when a tap occurs on a detected date. + /// + /// - Parameters: + /// - date: The selected date. func didSelectDate(_ date: Date) + /// Triggered when a tap occurs on a detected phone number. + /// + /// - Parameters: + /// - phoneNumber: The selected phone number. func didSelectPhoneNumber(_ phoneNumber: String) + /// Triggered when a tap occurs on a detected URL. + /// + /// - Parameters: + /// - url: The selected URL. func didSelectURL(_ url: URL) - + + /// Triggered when a tap occurs on detected transit information. + /// + /// - Parameters: + /// - transitInformation: The selected transit information. func didSelectTransitInformation(_ transitInformation: [String: String]) } diff --git a/Sources/Protocols/MessageType.swift b/Sources/Protocols/MessageType.swift index 5a8b8471..7f687a5b 100644 --- a/Sources/Protocols/MessageType.swift +++ b/Sources/Protocols/MessageType.swift @@ -24,17 +24,14 @@ import Foundation -/// A standard protocol representing a message. Use this protocol to create -/// your own message object to be used by MessageKit. +/// A standard protocol representing a message. +/// Use this protocol to create your own message object to be used by MessageKit. public protocol MessageType { /// The sender of the message. var sender: Sender { get } /// The unique identifier for the message. - /// - /// NOTE: This value must be unique for all messages as it is - /// used to cache layout information. var messageId: String { get } /// The date the message was sent. diff --git a/Sources/Protocols/MessagesDataSource.swift b/Sources/Protocols/MessagesDataSource.swift index 486510eb..0b1f65a9 100644 --- a/Sources/Protocols/MessagesDataSource.swift +++ b/Sources/Protocols/MessagesDataSource.swift @@ -24,18 +24,21 @@ import UIKit +/// An object that adopts the `MessagesDataSource` protocol is responsible for providing +/// the data required by a `MessagesCollectionView`. public protocol MessagesDataSource: AnyObject { /// The `Sender` of new messages in the `MessagesCollectionView`. func currentSender() -> Sender - /// A helper method to determine if a given message is from the current sender. + /// A helper method to determine if a given message is from the current `Sender`. /// /// - Parameters: - /// - message: The message to check if it was sent by the current Sender. + /// - message: The message to check if it was sent by the current `Sender`. /// - /// The default implementation of this method checks for equality between the message's `Sender` - /// and the current Sender. + /// - Note: + /// The default implementation of this method checks for equality between + /// the message's `Sender` and the current `Sender`. func isFromCurrentSender(message: MessageType) -> Bool /// The message to be used for a `MessageCollectionViewCell` at the given `IndexPath`. @@ -45,11 +48,20 @@ public protocol MessagesDataSource: AnyObject { /// - messagesCollectionView: The `MessagesCollectionView` in which the message will be displayed. func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType - /// The number of messages to be displayed in the `MessagesCollectionView`. + /// The number of sections to be displayed in the `MessagesCollectionView`. /// /// - Parameters: /// - messagesCollectionView: The `MessagesCollectionView` in which the messages will be displayed. - func numberOfMessages(in messagesCollectionView: MessagesCollectionView) -> Int + func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int + + /// The number of cells to be displayed in the `MessagesCollectionView`. + /// + /// - Parameters: + /// - section: The number of the section in which the cells will be displayed. + /// - messagesCollectionView: The `MessagesCollectionView` in which the messages will be displayed. + /// - Note: + /// The default implementation of this method returns 1. Putting each message in its own section. + func numberOfItems(inSection section: Int, in messagesCollectionView: MessagesCollectionView) -> Int /// The attributed text to be used for cell's top label. /// @@ -79,6 +91,10 @@ public extension MessagesDataSource { return message.sender == currentSender() } + func numberOfItems(inSection section: Int, in messagesCollectionView: MessagesCollectionView) -> Int { + return 1 + } + func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { return nil } diff --git a/Sources/Protocols/MessagesDisplayDelegate.swift b/Sources/Protocols/MessagesDisplayDelegate.swift index 75d3e72c..288b1cda 100644 --- a/Sources/Protocols/MessagesDisplayDelegate.swift +++ b/Sources/Protocols/MessagesDisplayDelegate.swift @@ -25,7 +25,7 @@ import Foundation import MapKit -/// A protocol used by the `MessagesViewController` to customize the appearance of a `MessageCollectionViewCell`. +/// A protocol used by the `MessagesViewController` to customize the appearance of a `MessageContentCell`. public protocol MessagesDisplayDelegate: AnyObject { // MARK: - All Messages @@ -37,7 +37,8 @@ public protocol MessagesDisplayDelegate: AnyObject { /// - indexPath: The `IndexPath` of the cell. /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. /// - /// The default value returned by this method is `MessageStyle.bubble`. + /// - Note: + /// The default value returned by this method is `MessageStyle.bubble`. func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle /// Specifies the background color of the `MessageContainerView`. @@ -47,44 +48,31 @@ public protocol MessagesDisplayDelegate: AnyObject { /// - indexPath: The `IndexPath` of the cell. /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. /// - /// The default value is `UIColor.clear` for emoji messages. For all other `MessageData` cases, the color depends on the `Sender`: + /// - Note: + /// The default value is `UIColor.clear` for emoji messages. + /// For all other `MessageData` cases, the color depends on the `Sender`. /// - /// Current Sender: Green + /// Current sender: Green /// - /// All other Senders: Gray + /// All other senders: Gray func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor - /// The section header to use for a given `MessageType`. + /// The section header to use for a given `IndexPath`. /// /// - Parameters: /// - message: The `MessageType` that will be displayed for this header. /// - indexPath: The `IndexPath` of the header. /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed. - /// - /// The default value returned by this method is a `MessageDateHeaderView`. - func messageHeaderView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageHeaderView + func messageHeaderView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView - /// Used by the `MessageLayoutDelegate` method `headerViewSize(_:_:_:)` to determine if a header should be displayed. - /// This method checks `MessageCollectionView`'s `showsDateHeaderAfterTimeInterval` property and returns true if - /// the current messages sent date occurs after the specified time interval when compared to the previous message. + /// The section footer to use for a given `IndexPath`. /// /// - Parameters: - /// - message: The `MessageType` that will be displayed for this header. - /// - indexPath: The `IndexPath` of the header. - /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed. - func shouldDisplayHeader(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Bool - - /// The section footer to use for a given `MessageType`. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed for this footer. /// - indexPath: The `IndexPath` of the footer. /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed. - /// - /// The default value returned by this method is a `MessageFooterView`. - func messageFooterView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageFooterView + func messageFooterView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView - /// Configure `AvatarView`‘s image. + /// Used to configure the `AvatarView`‘s image in a `MessageContentCell` class. /// /// - Parameters: /// - avatarView: The `AvatarView` of the cell. @@ -92,7 +80,8 @@ public protocol MessagesDisplayDelegate: AnyObject { /// - indexPath: The `IndexPath` of the cell. /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. /// - /// The default image configured by this method is `?`. + /// - Note: + /// The default image configured by this method is `?`. func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) // MARK: - Text Messages @@ -100,15 +89,16 @@ public protocol MessagesDisplayDelegate: AnyObject { /// Specifies the color of the text for a `TextMessageCell`. /// /// - Parameters: - /// - message: A `MessageType` with a `MessageData` case of `.text` or `.attributedText` to which the color will apply. + /// - message: A `MessageType` with a `MessageData` case of `.text` to which the color will apply. /// - indexPath: The `IndexPath` of the cell. /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. /// - /// The default value returned by this method is determined by the messages `Sender`: + /// - Note: + /// The default value returned by this method is determined by the messages `Sender`. /// - /// Current Sender: UIColor.white + /// Current sender: UIColor.white /// - /// All other Senders: UIColor.darkText + /// All other senders: UIColor.darkText func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor /// Specifies the `DetectorType`s to check for the `MessageType`'s text against. @@ -118,52 +108,59 @@ public protocol MessagesDisplayDelegate: AnyObject { /// - indexPath: The `IndexPath` of the cell. /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. /// - /// The default value returned by this method is all available detector types. + /// - Note: + /// This method returns an empty array by default. func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] /// Specifies the attributes for a given `DetectorType` /// /// - Parameters: /// - detector: The `DetectorType` for the applied attributes. - /// - message: A `MessageType` with a `MessageData` case of `.text` or `.attributedText` - /// to which the detectors will apply. + /// - message: A `MessageType` with a `MessageData` case of `.text` or `.attributedText` to which the detectors will apply. /// - indexPath: The `IndexPath` of the cell. func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedStringKey: Any] // MARK: - Location Messages - /// Ask the delegate for a LocationMessageSnapshotOptions instance to customize the MapView on the given message + /// Used to configure a `LocationMessageSnapshotOptions` instance to customize the map image on the given location message. /// /// - Parameters: - /// - message: The location message to be customized - /// - indexPath: Message's index path - /// - messagesCollectionView: The collection view requesting the information - /// - Returns: Your LocationMessageSnapshotOptions instance with the options to customize map style + /// - message: A `MessageType` with a `MessageData` case of `.location`. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` requesting the information. + /// - Returns: The LocationMessageSnapshotOptions instance with the options to customize map style. func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions - /// Ask the delegate for a custom MKAnnotationView to show on the given message. - /// You can return nil if you don't want to show any annotation. - /// - /// default: MKPinAnnotationView + /// Used to configure the annoation view of the map image on the given location message. /// /// - Parameters: - /// - message: The location message with the annotation to customize - /// - indexPath: Message's index path - /// - messageCollectionView: The collection view requesting the information - /// - Returns: Your customized MKAnnotationView or nil to not show any. + /// - message: A `MessageType` with a `MessageData` case of `.location`. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` requesting the information. + /// - Returns: The `MKAnnotationView` to use as the annotation view. func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? - /// Ask the delegate for a custom animation block to run when whe map screenshot is ready to be displaied in the given location message - /// The animation block is called with the image view to be animated. You can animate it with CoreAnimation, UIView.animate or any library you prefer. - /// - /// default: nil + /// Ask the delegate for a custom animation block to run when whe map screenshot is ready to be displaied in the given location message. + /// The animation block is called with the `UIImageView` to be animated. /// /// - Parameters: - /// - message: The location message with the map to animate - /// - indexPath: Message's index path - /// - messagesCollectionView: The collection view requesting the information - /// - Returns: Your customized animation block. + /// - message: A `MessageType` with a `MessageData` case of `.location`. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` requesting the information. + /// - Returns: The animation block to use to apply the location image. func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? + + // MARK: - Media Messages + + /// Used to configure the `UIImageView` of a `MediaMessageCell. + /// + /// - Parameters: + /// - imageView: The `UIImageView` 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. + func configureMediaMessageImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) + } public extension MessagesDisplayDelegate { @@ -185,24 +182,12 @@ public extension MessagesDisplayDelegate { } } - func messageHeaderView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageHeaderView { - let header = messagesCollectionView.dequeueReusableHeaderView(MessageDateHeaderView.self, for: indexPath) - header.dateLabel.text = MessageKitDateFormatter.shared.string(from: message.sentDate) - return header + func messageHeaderView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView { + return messagesCollectionView.dequeueReusableHeaderView(MessageReusableView.self, for: indexPath) } - func shouldDisplayHeader(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Bool { - guard let dataSource = messagesCollectionView.messagesDataSource else { return false } - if indexPath.section == 0 { return false } - let previousSection = indexPath.section - 1 - let previousIndexPath = IndexPath(item: 0, section: previousSection) - let previousMessage = dataSource.messageForItem(at: previousIndexPath, in: messagesCollectionView) - let timeIntervalSinceLastMessage = message.sentDate.timeIntervalSince(previousMessage.sentDate) - return timeIntervalSinceLastMessage >= messagesCollectionView.showsDateHeaderAfterTimeInterval - } - - func messageFooterView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageFooterView { - return messagesCollectionView.dequeueReusableFooterView(MessageFooterView.self, for: indexPath) + func messageFooterView(for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageReusableView { + return messagesCollectionView.dequeueReusableFooterView(MessageReusableView.self, for: indexPath) } func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { @@ -240,4 +225,8 @@ public extension MessagesDisplayDelegate { return nil } + // MARK: - Media Message Defaults + + func configureMediaMessageImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { + } } diff --git a/Sources/Protocols/MessagesLayoutDelegate.swift b/Sources/Protocols/MessagesLayoutDelegate.swift index f1be3042..2b3022f1 100644 --- a/Sources/Protocols/MessagesLayoutDelegate.swift +++ b/Sources/Protocols/MessagesLayoutDelegate.swift @@ -23,255 +23,70 @@ */ import Foundation -import AVFoundation /// A protocol used by the `MessagesCollectionViewFlowLayout` object to determine /// the size and layout of a `MessageCollectionViewCell` and its contents. public protocol MessagesLayoutDelegate: AnyObject { - // MARK: - All Messages - - /// Specifies the padding around the `MessageContainerView` in a `MessageCollectionViewCell`. + /// Specifies the size to use for a header view. /// /// - 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 determined by the messages `Sender`: - /// - /// Current Sender: `UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4)` - /// - /// All other Senders: `UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30)` - func messagePadding(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets - - /// Specifies the vertical and horizontal alignment for the `AvatarView` in a `MessageCollectionViewCell`. - /// - /// - 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 an `AvatarPosition` with - /// `Horizontal.natural` and `Vertical.messageBottom`. - func avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition - - /// Specifies the horizontal alignment of a `MessageCollectionViewCell`'s top label. - /// - /// - 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 determined by the messages `Sender`: - /// - /// Current Sender: .messageTrailing(.zero) - /// - /// All other senders: .messageLeading(.zero) - func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment - - /// Specifies the horizontal alignment of a `MessageCollectionViewCell`'s bottom label. - /// - /// - 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 determined by the messages `Sender`: - /// - /// Current Sender: .messageLeading(.zero) - /// - /// All other senders: .messageTrailing(.zero) - func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment - - /// Specifies the size of the `AvatarView` in a `MessageCollectionViewCell`. - /// - /// - 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 a size of `30 x 30`. - func avatarSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize - - /// Specifies the size to use for a `MessageHeaderView`. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed for this header. - /// - indexPath: The `IndexPath` of the header. + /// - section: The section number of the header. /// - messagesCollectionView: The `MessagesCollectionView` in which this header will be displayed. /// - /// The default value returned by this method is the width of the `MessagesCollectionView` and a height of 12. - func headerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize + /// - Note: + /// The default value returned by this method is a size of `GGSize.zero`. + func headerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize - /// Specifies the size to use for a `MessageFooterView`. + /// Specifies the size to use for a footer view. /// /// - Parameters: - /// - message: The `MessageType` that will be displayed for this footer. - /// - indexPath: The `IndexPath` of the footer. + /// - section: The section number of the footer. /// - messagesCollectionView: The `MessagesCollectionView` in which this footer will be displayed. /// - /// The default value returned by this method is a size of `GGSize.zero`. - func footerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize + /// - Note: + /// The default value returned by this method is a size of `GGSize.zero`. + func footerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize - // MARK: - Text Messages - - /// Specifies the insets for the text rect of the `MessageLabel` in a `TextMessageCell`. + /// Specifies the height for the `MessageContentCell`'s top label. /// /// - Parameters: - /// - message: A `MessageType` with a `MessageData` case of `.text` or `.attributedText` to which these insets will apply. + /// - message: The `MessageType` that will be displayed for 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 determined by the messages `Sender`: - /// - /// Current Sender: `UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18)` - /// - /// All other Senders: `UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14)` - func messageLabelInset(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets + /// - Note: + /// The default value returned by this method is zero. + func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat - // MARK: - Media Messages - - /// Specifies the width for a `MessageContainerView`. + /// Specifies the height for the `MessageContentCell`'s bottom label. /// /// - Parameters: - /// - message: The `MessageType` that will be displayed by this cell. + /// - message: The `MessageType` that will be displayed for this cell. /// - indexPath: The `IndexPath` of the cell. - /// - maxWidth: The max available width for the `MessageContainerView` respecting the cell's other content. /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. /// - /// The default value returned by this method is the `maxWidth`. - func widthForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat - - /// Specifies the height for a `MessageContainerView`. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - maxWidth: The max available width for the `MessageContainerView` respecting the cell's other content. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// The default value returned by this method uses `AVMakeRect(aspectRatio:insideRect:)` with a bounding - /// rect using the `maxWidth` and `.greatestFiniteMagnitude` for the height. - func heightForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat - - // MARK: - Location Messages - - /// Specifies the width for a `MessageContainerView`. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - maxWidth: The max available width for the `MessageContainerView` respecting the cell's other content. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - /// - /// The default value returned by this method is the `maxWidth`. - func widthForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat - - /// Specifies the height for a `MessageContainerView`. - /// - /// - Parameters: - /// - message: The `MessageType` that will be displayed by this cell. - /// - indexPath: The `IndexPath` of the cell. - /// - maxWidth: The max available width for the `MessageContainerView` respecting the cell's other content. - /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. - func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat - - /// Specifies whether the layout attributes for a given `MessageType` - /// should be cached by the `MessagesCollectionViewFlowLayout`. - /// - Parameters: - /// - message: The `MessageType` whose attributes to cache. - /// - /// The default value returned by this method is `false`. - func shouldCacheLayoutAttributes(for message: MessageType) -> Bool + /// - Note: + /// The default value returned by this method is zero. + func cellBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat } 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 { - fatalError(MessageKitError.nilMessagesDataSource) - } - if dataSource.isFromCurrentSender(message: message) { - return UIEdgeInsets(top: 0, left: 30, bottom: 0, right: 4) - } else { - return UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 30) - } - } - - func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment { - 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 { - fatalError(MessageKitError.nilMessagesDataSource) - } - return dataSource.isFromCurrentSender(message: message) ? .messageLeading(.zero) : .messageTrailing(.zero) - } - - func avatarSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { - return CGSize(width: 30, height: 30) - } - - func avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition { - return AvatarPosition(horizontal: .natural, vertical: .messageBottom) - } - - func headerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { - 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 - } - - func footerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { + func headerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize { return .zero } - func shouldCacheLayoutAttributes(for message: MessageType) -> Bool { - return false + func footerViewSize(for section: Int, in messagesCollectionView: MessagesCollectionView) -> CGSize { + return .zero } - // MARK: - Text Messages Defaults - - func messageLabelInset(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets { - 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 { - return UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14) - } + func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + return 0 } - // MARK: - Media Messages Defaults - - func widthForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return maxWidth - } - - func heightForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - switch message.data { - case .photo(let image), .video(_, let image): - let boundingRect = CGRect(origin: .zero, size: CGSize(width: maxWidth, height: .greatestFiniteMagnitude)) - return AVMakeRect(aspectRatio: image.size, insideRect: boundingRect).height - default: - return 0 - } - } - - // MARK: - Location Messages Defaults - - func widthForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { - return maxWidth + func cellBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + return 0 } } diff --git a/Sources/Supporting/MessageKit+Availability.swift b/Sources/Supporting/MessageKit+Availability.swift index 31fee58f..735409e3 100644 --- a/Sources/Supporting/MessageKit+Availability.swift +++ b/Sources/Supporting/MessageKit+Availability.swift @@ -24,98 +24,49 @@ import Foundation -// MARK: - Deprecated Protocols +public extension MessagesLayoutDelegate { -@available(*, deprecated: 0.11.0, message: "LocationMessageDisplayDelegate has been deprecated in favor of MessagesDisplayDelegate") -typealias LocationMessageDisplayDelegate = MessagesDisplayDelegate - -@available(*, deprecated: 0.11.0, message: "TextMessageDisplayDelegate has been deprecated in favor of MessagesDisplayDelegate") -typealias TextMessageDisplayDelegate = MessagesDisplayDelegate - -@available(*, deprecated: 0.11.0, message: "LocationMessageLayoutDelegate has been deprecated in favor of MessagesDisplayDelegate") -typealias LocationMessageLayoutDelegate = MessagesLayoutDelegate - -@available(*, deprecated: 0.11.0, message: "MediaMessageLayoutDelegate has been deprecated in favor of MessagesDisplayDelegate") -typealias MediaMessageLayoutDelegate = MessagesLayoutDelegate - -// MARK: - AvatarAlignment - -@available(*, deprecated: 0.11.0, message: "Removed in MessageKit 0.11.0. Please use AvatarPosition instead.") -public enum AvatarAlignment {} - -// MARK: - MessagesLayoutDelegate - -extension MessagesLayoutDelegate { - - /// Specifies the vertical alignment for the `AvatarView` in a `MessageCollectionViewCell`. - /// - /// - 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 `AvatarAlignment.cellBottom`. - @available(*, deprecated: 0.11.0, message: "Removed in MessageKit 0.11.0. Please use avatarPosition(for:at:in:) instead.") - public func avatarAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarAlignment { - fatalError("Please use avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition instead.") + func avatarSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize { + fatalError("avatarSize(for:at:in) has been removed in MessageKit 1.0.") } - -} -// MARK: - MessagesCollectionViewFlowLayout - -extension MessagesCollectionViewFlowLayout { - - /// A Boolean value that determines if the `AvatarView` is always on the leading - /// side of a MessageCollectionViewCell. - /// - /// Setting this property to `true` causes `avatarAlwaysTrailing` to be set to `false`. - /// - /// The default value of this property is `false`. - @available(*, deprecated: 0.11.0, message: "Removed in MessageKit 0.11.0. Please use the avatarPosition(for:at:in) delegate method.") - public var avatarAlwaysLeading: Bool { - fatalError("Fatal Error: avatarAlwaysLeading is no longer supported") + func avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition { + fatalError("avatarPosition(for:at:in) has been removed in MessageKit 1.0.") } - - /// A Boolean value that determines if the `AvatarView` is always on the trailing - /// side of a `MessageCollectionViewCell`. - /// - /// Setting this property to `true` causes `avatarAlwaysLeading` to be set to `false`. - /// - /// The default value of this property is `false`. - @available(*, deprecated: 0.11.0, message: "Removed in MessageKit 0.11.0. Please use the avatarPosition(for:at:in) delegate method instead.") - public var avatarAlwaysTrailing: Bool { - fatalError("Fatal Error: avatarAlwaysTrailing is no longer supported") + + func messageLabelInset(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets { + fatalError("messageLabelInset(for:at:in) has been removed in MessageKit 1.0") } - -} -// MARK: - MessagesDataSource + func messagePadding(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets { + fatalError("messagePadding(for:at:in) has been removed in MessageKit 1.0.") + } -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") + func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment { + fatalError("cellTopLabelAlignment(for:at:in) has been removed in MessageKit 1.0.") } -} -// MARK: - MessagesViewController + func cellBottomLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment { + fatalError("cellBottomLabelAlignment(for:at:in) has been removed in MessageKit 1.0.") + } + + func widthForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + fatalError("widthForMedia(message:at:with:in) has been removed in MessageKit 1.0.") + } + + func heightForMedia(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + fatalError("heightForMedia(message:at:with:in) has been removed in MessageKit 1.0.") + } + + func widthForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + fatalError("widthForLocation(message:at:with:in) has been removed in MessageKit 1.0.") + } + + func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + fatalError("heightForLocation(message:at:with:in) has been removed in MessageKit 1.0.") + } -extension MessagesViewController { - /// A Boolean value that determines whether the `MessagesCollectionView` scrolls to the - /// bottom on the view's first layout. - /// - /// The default value of this property is `false`. - @available(*, deprecated: 0.11.1, message: "Removed in MessageKit 0.11.1.") - open var scrollsToBottomOnFirstLayout: Bool { - fatalError("Fatal Error: scrollsToBottomOnFirstLayout is no longer supported") + func shouldCacheLayoutAttributes(for message: MessageType) -> Bool { + fatalError("shouldCacheLayoutAttributes(for:) has been removed in MessageKit 1.0.") } } diff --git a/Sources/Views/AvatarView.swift b/Sources/Views/AvatarView.swift index be28e739..f88af355 100644 --- a/Sources/Views/AvatarView.swift +++ b/Sources/Views/AvatarView.swift @@ -96,8 +96,9 @@ open class AvatarView: UIImageView { //// Text Drawing let textRect = calculateTextRect(outerViewWidth: width, outerViewHeight: height) + let initialsText = NSAttributedString(string: initials, attributes: [.font: font]) if adjustsFontSizeToFitWidth, - initials.width(considering: textRect.height, and: font) > textRect.width { + initialsText.width(considering: textRect.height) > textRect.width { let newFontSize = calculateFontSize(text: initials, font: font, width: textRect.width, height: textRect.height) font = placeholderFont.withSize(newFontSize) } @@ -119,7 +120,8 @@ open class AvatarView: UIImageView { Recursively find the biggest size to fit the text with a given width and height */ private func calculateFontSize(text: String, font: UIFont, width: CGFloat, height: CGFloat) -> CGFloat { - if text.width(considering: height, and: font) > width { + let attributedText = NSAttributedString(string: text, attributes: [.font: font]) + if attributedText.width(considering: height) > width { let newFont = font.withSize(font.pointSize - 1) if newFont.pointSize > minimumFontSize { return font.pointSize diff --git a/Sources/Views/Cells/LocationMessageCell.swift b/Sources/Views/Cells/LocationMessageCell.swift index ad4aad7a..a767f31c 100644 --- a/Sources/Views/Cells/LocationMessageCell.swift +++ b/Sources/Views/Cells/LocationMessageCell.swift @@ -25,14 +25,13 @@ import UIKit import MapKit -open class LocationMessageCell: MessageCollectionViewCell { - - open override class func reuseIdentifier() -> String { return "messagekit.cell.location" } - - // MARK: - Properties +/// A subclass of `MessageContentCell` used to display location messages. +open class LocationMessageCell: MessageContentCell { + /// The activity indicator to be displayed while the map image is loading. open var activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) + /// The image view holding the map image. open var imageView = UIImageView() private weak var snapShotter: MKMapSnapshotter? @@ -45,6 +44,7 @@ open class LocationMessageCell: MessageCollectionViewCell { setupConstraints() } + /// Responsible for setting up the constraints of the cell's subviews. open func setupConstraints() { imageView.fillSuperview() activityIndicator.centerInSuperview() @@ -64,12 +64,12 @@ open class LocationMessageCell: MessageCollectionViewCell { let annotationView = displayDelegate.annotationViewForLocation(message: message, at: indexPath, in: messagesCollectionView) let animationBlock = displayDelegate.animationBlockForLocation(message: message, at: indexPath, in: messagesCollectionView) - guard case let .location(location) = message.data else { fatalError("") } + guard case let .location(locationItem) = message.data else { fatalError("") } activityIndicator.startAnimating() let snapshotOptions = MKMapSnapshotOptions() - snapshotOptions.region = MKCoordinateRegion(center: location.coordinate, span: options.span) + snapshotOptions.region = MKCoordinateRegion(center: locationItem.location.coordinate, span: options.span) snapshotOptions.showsBuildings = options.showsBuildings snapshotOptions.showsPointsOfInterest = options.showsPointsOfInterest @@ -93,7 +93,7 @@ open class LocationMessageCell: MessageCollectionViewCell { snapshot.image.draw(at: .zero) - var point = snapshot.point(for: location.coordinate) + var point = snapshot.point(for: locationItem.location.coordinate) //Move point to reflect annotation anchor point.x -= annotationView.bounds.size.width / 2 point.y -= annotationView.bounds.size.height / 2 diff --git a/Sources/Views/Cells/MediaMessageCell.swift b/Sources/Views/Cells/MediaMessageCell.swift index 3a915b86..5414b406 100644 --- a/Sources/Views/Cells/MediaMessageCell.swift +++ b/Sources/Views/Cells/MediaMessageCell.swift @@ -24,21 +24,25 @@ import UIKit -open class MediaMessageCell: MessageCollectionViewCell { - - open override class func reuseIdentifier() -> String { return "messagekit.cell.mediamessage" } - - // MARK: - Properties +/// A subclass of `MessageContentCell` used to display video and audio messages. +open class MediaMessageCell: MessageContentCell { + /// The play button view to display on video messages. open lazy var playButtonView: PlayButtonView = { let playButtonView = PlayButtonView() return playButtonView }() - open var imageView = UIImageView() + /// The image view display the media content. + open var imageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + return imageView + }() // MARK: - Methods + /// Responsible for setting up the constraints of the cell's subviews. open func setupConstraints() { imageView.fillSuperview() playButtonView.centerInSuperview() @@ -54,15 +58,22 @@ open class MediaMessageCell: 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(MessageKitError.nilMessagesDisplayDelegate) + } + switch message.data { - case .photo(let image): - imageView.image = image + case .photo(let mediaItem): + imageView.image = mediaItem.image ?? mediaItem.placeholderImage playButtonView.isHidden = true - case .video(_, let image): - imageView.image = image + case .video(let mediaItem): + imageView.image = mediaItem.image ?? mediaItem.placeholderImage playButtonView.isHidden = false default: break } + + displayDelegate.configureMediaMessageImageView(imageView, for: message, at: indexPath, in: messagesCollectionView) } } diff --git a/Sources/Views/Cells/MessageCollectionViewCell.swift b/Sources/Views/Cells/MessageCollectionViewCell.swift index 2507402f..8d99ab90 100644 --- a/Sources/Views/Cells/MessageCollectionViewCell.swift +++ b/Sources/Views/Cells/MessageCollectionViewCell.swift @@ -24,236 +24,17 @@ import UIKit -open class MessageCollectionViewCell: UICollectionViewCell, CollectionViewReusable { +/// A subclass of `UICollectionViewCell` to be used inside of a `MessagesCollectionView`. +open class MessageCollectionViewCell: UICollectionViewCell { - open class func reuseIdentifier() -> String { - return "messagekit.cell.base-cell" - } - - open var avatarView = AvatarView() - - open var messageContainerView: MessageContainerView = { - let containerView = MessageContainerView() - containerView.clipsToBounds = true - containerView.layer.masksToBounds = true - return containerView - }() - - open var cellTopLabel: UILabel = { - let label = UILabel() - label.numberOfLines = 0 - return label - }() - - open var cellBottomLabel: UILabel = { - let label = UILabel() - label.numberOfLines = 0 - return label - }() - - open weak var delegate: MessageCellDelegate? + // MARK: - Initializers public override init(frame: CGRect) { super.init(frame: frame) - contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - setupSubviews() } - required public init?(coder aDecoder: NSCoder) { + public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - open func setupSubviews() { - contentView.addSubview(messageContainerView) - contentView.addSubview(avatarView) - contentView.addSubview(cellTopLabel) - contentView.addSubview(cellBottomLabel) - } - - open override func prepareForReuse() { - super.prepareForReuse() - cellTopLabel.text = nil - cellTopLabel.attributedText = nil - cellBottomLabel.text = nil - cellBottomLabel.attributedText = nil - } - - // MARK: - Configuration - - open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { - super.apply(layoutAttributes) - guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return } - // Call this before other laying out other subviews - layoutMessageContainerView(with: attributes) - layoutAvatarView(with: attributes) - layoutBottomLabel(with: attributes) - layoutTopLabel(with: attributes) - } - - open func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) { - guard let dataSource = messagesCollectionView.messagesDataSource else { - fatalError(MessageKitError.nilMessagesDataSource) - } - guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { - fatalError(MessageKitError.nilMessagesDisplayDelegate) - } - - delegate = messagesCollectionView.messageCellDelegate - - 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 topText = dataSource.cellTopLabelAttributedText(for: message, at: indexPath) - let bottomText = dataSource.cellBottomLabelAttributedText(for: message, at: indexPath) - - cellTopLabel.attributedText = topText - cellBottomLabel.attributedText = bottomText - } - - /// Handle tap gesture on contentView and its subviews like messageContainerView, cellTopLabel, cellBottomLabel, avatarView .... - open func handleTapGesture(_ gesture: UIGestureRecognizer) { - let touchLocation = gesture.location(in: self) - - switch true { - case messageContainerView.frame.contains(touchLocation) && !cellContentView(canHandle: convert(touchLocation, to: messageContainerView)): - delegate?.didTapMessage(in: self) - case avatarView.frame.contains(touchLocation): - delegate?.didTapAvatar(in: self) - case cellTopLabel.frame.contains(touchLocation): - delegate?.didTapTopLabel(in: self) - case cellBottomLabel.frame.contains(touchLocation): - delegate?.didTapBottomLabel(in: self) - default: - 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) - 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 - open func cellContentView(canHandle touchPoint: CGPoint) -> Bool { - return false - } - - // MARK: - Origin Calculations - - /// Positions the cell's `AvatarView`. - /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. - open func layoutAvatarView(with attributes: MessagesCollectionViewLayoutAttributes) { - guard attributes.avatarSize != .zero else { return } - - var origin: CGPoint = .zero - - switch attributes.avatarPosition.horizontal { - case .cellLeading: - break - case .cellTrailing: - origin.x = attributes.frame.width - attributes.avatarSize.width - case .natural: - fatalError(MessageKitError.avatarPositionUnresolved) - } - - switch attributes.avatarPosition.vertical { - case .cellTop: - break - case .cellBottom: - origin.y = attributes.frame.height - attributes.avatarSize.height - case .messageTop: // Needs messageContainerView frame to be set - origin.y = messageContainerView.frame.minY - case .messageBottom: // Needs messageContainerView frame to be set - origin.y = messageContainerView.frame.maxY - attributes.avatarSize.height - case .messageCenter: // Needs messageContainerView frame to be set - origin.y = messageContainerView.frame.midY - (attributes.avatarSize.height/2) - } - - avatarView.frame = CGRect(origin: origin, size: attributes.avatarSize) - } - - /// Positions the cell's `MessageContainerView`. - /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. - open func layoutMessageContainerView(with attributes: MessagesCollectionViewLayoutAttributes) { - guard attributes.messageContainerSize != .zero else { return } - - var origin: CGPoint = .zero - origin.y = attributes.topLabelSize.height + attributes.messageContainerPadding.top + attributes.topLabelAlignment.insets.vertical - - switch attributes.avatarPosition.horizontal { - case .cellLeading: - origin.x = attributes.avatarSize.width + attributes.messageContainerPadding.left - case .cellTrailing: - origin.x = attributes.frame.width - attributes.avatarSize.width - attributes.messageContainerSize.width - attributes.messageContainerPadding.right - case .natural: - fatalError(MessageKitError.avatarPositionUnresolved) - } - - messageContainerView.frame = CGRect(origin: origin, size: attributes.messageContainerSize) - } - - /// Positions the cell's top label. - /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. - open func layoutTopLabel(with attributes: MessagesCollectionViewLayoutAttributes) { - guard attributes.topLabelSize != .zero else { return } - - let topLabelAlignment = attributes.topLabelAlignment - let topLabelPadding = topLabelAlignment.insets - let topLabelSize = attributes.topLabelSize - - var origin: CGPoint = .zero - - origin.y = topLabelPadding.top - - switch attributes.topLabelAlignment { - case .cellLeading: - origin.x = topLabelPadding.left - case .cellCenter: - origin.x = (attributes.frame.width/2) + topLabelPadding.left - topLabelPadding.right - case .cellTrailing: - origin.x = attributes.frame.width - topLabelSize.width - topLabelPadding.right - case .messageLeading: // Needs messageContainerView frame to be set - origin.x = messageContainerView.frame.minX + topLabelPadding.left - case .messageTrailing: // Needs messageContainerView frame to be set - origin.x = messageContainerView.frame.maxX - topLabelSize.width - topLabelPadding.right - } - - cellTopLabel.frame = CGRect(origin: origin, size: topLabelSize) - } - - /// Positions the cell's bottom label. - /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. - open func layoutBottomLabel(with attributes: MessagesCollectionViewLayoutAttributes) { - guard attributes.bottomLabelSize != .zero else { return } - - let bottomLabelAlignment = attributes.bottomLabelAlignment - let bottomLabelPadding = bottomLabelAlignment.insets - let bottomLabelSize = attributes.bottomLabelSize - - var origin: CGPoint = .zero - - origin.y = messageContainerView.frame.maxY + attributes.messageContainerPadding.bottom + bottomLabelPadding.top - - switch bottomLabelAlignment { - case .cellLeading: - origin.x = bottomLabelPadding.left - case .cellCenter: - origin.x = (attributes.frame.width/2) + bottomLabelPadding.left - bottomLabelPadding.right - case .cellTrailing: - origin.x = attributes.frame.width - bottomLabelSize.width - bottomLabelPadding.right - case .messageLeading: - origin.x = messageContainerView.frame.minX + bottomLabelPadding.left - case .messageTrailing: - origin.x = messageContainerView.frame.maxX - bottomLabelSize.width - bottomLabelPadding.right - } - - cellBottomLabel.frame = CGRect(origin: origin, size: bottomLabelSize) - } } diff --git a/Sources/Views/Cells/MessageContentCell.swift b/Sources/Views/Cells/MessageContentCell.swift new file mode 100644 index 00000000..89794d10 --- /dev/null +++ b/Sources/Views/Cells/MessageContentCell.swift @@ -0,0 +1,235 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +/// A subclass of `MessageCollectionViewCell` used to display text, media, and location messages. +open class MessageContentCell: MessageCollectionViewCell { + + /// The image view displaying the avatar. + open var avatarView = AvatarView() + + /// The container used for styling and holding the message's content view. + open var messageContainerView: MessageContainerView = { + let containerView = MessageContainerView() + containerView.clipsToBounds = true + containerView.layer.masksToBounds = true + return containerView + }() + + /// The top label of the cell. + open var cellTopLabel: InsetLabel = { + let label = InsetLabel() + label.numberOfLines = 0 + return label + }() + + /// The bottom label of the cell. + open var cellBottomLabel: InsetLabel = { + let label = InsetLabel() + label.numberOfLines = 0 + return label + }() + + /// The `MessageCellDelegate` for the cell. + open weak var delegate: MessageCellDelegate? + + public override init(frame: CGRect) { + super.init(frame: frame) + contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + setupSubviews() + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + open func setupSubviews() { + contentView.addSubview(messageContainerView) + contentView.addSubview(avatarView) + contentView.addSubview(cellTopLabel) + contentView.addSubview(cellBottomLabel) + } + + open override func prepareForReuse() { + super.prepareForReuse() + cellTopLabel.text = nil + cellTopLabel.attributedText = nil + cellBottomLabel.text = nil + cellBottomLabel.attributedText = nil + } + + // MARK: - Configuration + + open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { + super.apply(layoutAttributes) + guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return } + // Call this before other laying out other subviews + layoutMessageContainerView(with: attributes) + layoutAvatarView(with: attributes) + layoutBottomLabel(with: attributes) + layoutTopLabel(with: attributes) + } + + /// Used to configure the cell. + /// + /// - Parameters: + /// - message: The `MessageType` this cell displays. + /// - indexPath: The `IndexPath` for this cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell is contained. + open func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) { + guard let dataSource = messagesCollectionView.messagesDataSource else { + fatalError(MessageKitError.nilMessagesDataSource) + } + guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { + fatalError(MessageKitError.nilMessagesDisplayDelegate) + } + + delegate = messagesCollectionView.messageCellDelegate + + 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 topText = dataSource.cellTopLabelAttributedText(for: message, at: indexPath) + let bottomText = dataSource.cellBottomLabelAttributedText(for: message, at: indexPath) + + cellTopLabel.attributedText = topText + cellBottomLabel.attributedText = bottomText + } + + /// Handle tap gesture on contentView and its subviews like messageContainerView, cellTopLabel, cellBottomLabel, avatarView .... + open func handleTapGesture(_ gesture: UIGestureRecognizer) { + let touchLocation = gesture.location(in: self) + + switch true { + case messageContainerView.frame.contains(touchLocation) && !cellContentView(canHandle: convert(touchLocation, to: messageContainerView)): + delegate?.didTapMessage(in: self) + case avatarView.frame.contains(touchLocation): + delegate?.didTapAvatar(in: self) + case cellTopLabel.frame.contains(touchLocation): + delegate?.didTapTopLabel(in: self) + case cellBottomLabel.frame.contains(touchLocation): + delegate?.didTapBottomLabel(in: self) + default: + 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) + 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 + open func cellContentView(canHandle touchPoint: CGPoint) -> Bool { + return false + } + + // MARK: - Origin Calculations + + /// Positions the cell's `AvatarView`. + /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. + open func layoutAvatarView(with attributes: MessagesCollectionViewLayoutAttributes) { + guard attributes.avatarSize != .zero else { return } + + var origin: CGPoint = .zero + + switch attributes.avatarPosition.horizontal { + case .cellLeading: + break + case .cellTrailing: + origin.x = attributes.frame.width - attributes.avatarSize.width + case .natural: + fatalError(MessageKitError.avatarPositionUnresolved) + } + + switch attributes.avatarPosition.vertical { + case .cellTop: + break + case .cellBottom: + origin.y = attributes.frame.height - attributes.avatarSize.height + case .messageTop: // Needs messageContainerView frame to be set + origin.y = messageContainerView.frame.minY + case .messageBottom: // Needs messageContainerView frame to be set + origin.y = messageContainerView.frame.maxY - attributes.avatarSize.height + case .messageCenter: // Needs messageContainerView frame to be set + origin.y = messageContainerView.frame.midY - (attributes.avatarSize.height/2) + } + + avatarView.frame = CGRect(origin: origin, size: attributes.avatarSize) + } + + /// Positions the cell's `MessageContainerView`. + /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. + open func layoutMessageContainerView(with attributes: MessagesCollectionViewLayoutAttributes) { + guard attributes.messageContainerSize != .zero else { return } + + var origin: CGPoint = .zero + origin.y = attributes.topLabelSize.height + attributes.messageContainerPadding.top + + switch attributes.avatarPosition.horizontal { + case .cellLeading: + origin.x = attributes.avatarSize.width + attributes.messageContainerPadding.left + case .cellTrailing: + origin.x = attributes.frame.width - attributes.avatarSize.width - attributes.messageContainerSize.width - attributes.messageContainerPadding.right + case .natural: + fatalError(MessageKitError.avatarPositionUnresolved) + } + + messageContainerView.frame = CGRect(origin: origin, size: attributes.messageContainerSize) + } + + /// Positions the cell's top label. + /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. + open func layoutTopLabel(with attributes: MessagesCollectionViewLayoutAttributes) { + guard attributes.topLabelSize != .zero else { return } + + cellTopLabel.textAlignment = attributes.topLabelAlignment.textAlignment + cellTopLabel.textInsets = attributes.topLabelAlignment.textInsets + + cellTopLabel.frame = CGRect(origin: .zero, size: attributes.topLabelSize) + } + + /// Positions the cell's bottom label. + /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. + open func layoutBottomLabel(with attributes: MessagesCollectionViewLayoutAttributes) { + guard attributes.bottomLabelSize != .zero else { return } + + cellBottomLabel.textAlignment = attributes.bottomLabelAlignment.textAlignment + cellBottomLabel.textInsets = attributes.bottomLabelAlignment.textInsets + + let y = messageContainerView.frame.maxY + attributes.messageContainerPadding.bottom + let origin = CGPoint(x: 0, y: y) + + cellBottomLabel.frame = CGRect(origin: origin, size: attributes.bottomLabelSize) + } + +} diff --git a/Sources/Views/Cells/TextMessageCell.swift b/Sources/Views/Cells/TextMessageCell.swift index 242d713e..0497e001 100644 --- a/Sources/Views/Cells/TextMessageCell.swift +++ b/Sources/Views/Cells/TextMessageCell.swift @@ -24,18 +24,19 @@ import UIKit -open class TextMessageCell: MessageCollectionViewCell { - - open override class func reuseIdentifier() -> String { return "messagekit.cell.text" } +/// A subclass of `MessageContentCell` used to display text messages. +open class TextMessageCell: MessageContentCell { // MARK: - Properties + /// The `MessageCellDelegate` for the cell. open override weak var delegate: MessageCellDelegate? { didSet { messageLabel.delegate = delegate } } + /// The label used to display the message's text. open var messageLabel = MessageLabel() // MARK: - Methods @@ -44,7 +45,7 @@ open class TextMessageCell: MessageCollectionViewCell { super.apply(layoutAttributes) if let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes { messageLabel.textInsets = attributes.messageLabelInsets - messageLabel.font = attributes.messageLabelFont + messageLabel.messageLabelFont = attributes.messageLabelFont messageLabel.frame = messageContainerView.bounds } } @@ -79,18 +80,22 @@ open class TextMessageCell: MessageCollectionViewCell { switch message.data { case .text(let text), .emoji(let text): messageLabel.text = text + messageLabel.textColor = textColor + if let font = messageLabel.messageLabelFont { + messageLabel.font = font + } case .attributedText(let text): messageLabel.attributedText = text default: break } - // Needs to be set after the attributedText because it takes precedence - messageLabel.textColor = textColor } } - /// Handle `ContentView`'s tap gesture, return false when `ContentView` don't needs to handle gesture + /// Used to handle the cell's contentView's tap gesture. + /// Return false when the contentView does not need to handle the gesture. open override func cellContentView(canHandle touchPoint: CGPoint) -> Bool { return messageLabel.handleGesture(touchPoint) } + } diff --git a/Sources/Views/Headers & Footers/MessageDateHeaderView.swift b/Sources/Views/Headers & Footers/MessageDateHeaderView.swift index 9f54583e..11e4e3d6 100644 --- a/Sources/Views/Headers & Footers/MessageDateHeaderView.swift +++ b/Sources/Views/Headers & Footers/MessageDateHeaderView.swift @@ -24,8 +24,7 @@ import UIKit -open class MessageDateHeaderView: MessageHeaderView { - open override class func reuseIdentifier() -> String { return "messagekit.header.date" } +open class MessageDateHeaderView: MessageReusableView { // MARK: - Properties diff --git a/Sources/Views/Headers & Footers/MessageFooterView.swift b/Sources/Views/Headers & Footers/MessageReusableView.swift similarity index 87% rename from Sources/Views/Headers & Footers/MessageFooterView.swift rename to Sources/Views/Headers & Footers/MessageReusableView.swift index e7df9b56..f8c37c36 100644 --- a/Sources/Views/Headers & Footers/MessageFooterView.swift +++ b/Sources/Views/Headers & Footers/MessageReusableView.swift @@ -1,18 +1,18 @@ /* 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 @@ -22,10 +22,9 @@ SOFTWARE. */ -import UIKit +import Foundation -open class MessageFooterView: UICollectionReusableView, CollectionViewReusable { - open class func reuseIdentifier() -> String { return "messagekit.footer.base" } +open class MessageReusableView: UICollectionReusableView { // MARK: - Initializers diff --git a/Sources/Views/InputStackView.swift b/Sources/Views/InputStackView.swift index f5ea3856..eb750906 100644 --- a/Sources/Views/InputStackView.swift +++ b/Sources/Views/InputStackView.swift @@ -46,7 +46,7 @@ open class InputStackView: UIStackView { // MARK: Initialization - convenience init(axis: UILayoutConstraintAxis, spacing: CGFloat) { + public convenience init(axis: UILayoutConstraintAxis, spacing: CGFloat) { self.init(frame: .zero) self.axis = axis self.spacing = spacing diff --git a/Sources/Views/Headers & Footers/MessageHeaderView.swift b/Sources/Views/InsetLabel.swift similarity index 74% rename from Sources/Views/Headers & Footers/MessageHeaderView.swift rename to Sources/Views/InsetLabel.swift index 789efdb7..b543a8ff 100644 --- a/Sources/Views/Headers & Footers/MessageHeaderView.swift +++ b/Sources/Views/InsetLabel.swift @@ -1,18 +1,18 @@ /* 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 @@ -24,17 +24,15 @@ import UIKit -open class MessageHeaderView: UICollectionReusableView, CollectionViewReusable { - open class func reuseIdentifier() -> String { return "messagekit.header.base" } +open class InsetLabel: UILabel { - // MARK: - Properties - - public override init(frame: CGRect) { - super.init(frame: frame) + open var textInsets: UIEdgeInsets = .zero { + didSet { setNeedsDisplay() } } - public required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + open override func drawText(in rect: CGRect) { + let insetRect = UIEdgeInsetsInsetRect(rect, textInsets) + super.drawText(in: insetRect) } } diff --git a/Sources/Views/MessageLabel.swift b/Sources/Views/MessageLabel.swift index e46c1e59..bd4e3cdb 100644 --- a/Sources/Views/MessageLabel.swift +++ b/Sources/Views/MessageLabel.swift @@ -112,6 +112,8 @@ open class MessageLabel: UILabel { if !isConfiguring { setNeedsDisplay() } } } + + internal var messageLabelFont: UIFont? private var attributesNeedUpdate = false @@ -371,7 +373,7 @@ open class MessageLabel: UILabel { } - func handleGesture(_ touchLocation: CGPoint) -> Bool { + internal func handleGesture(_ touchLocation: CGPoint) -> Bool { guard let index = stringIndex(at: touchLocation) else { return false } diff --git a/Sources/Views/MessagesCollectionView.swift b/Sources/Views/MessagesCollectionView.swift index 44a503d8..323cd76e 100644 --- a/Sources/Views/MessagesCollectionView.swift +++ b/Sources/Views/MessagesCollectionView.swift @@ -36,14 +36,10 @@ open class MessagesCollectionView: UICollectionView { open weak var messageCellDelegate: MessageCellDelegate? - open var showsDateHeaderAfterTimeInterval: TimeInterval = 3600 - private var indexPathForLastItem: IndexPath? { - let lastSection = numberOfSections - 1 guard lastSection >= 0, numberOfItems(inSection: lastSection) > 0 else { return nil } return IndexPath(item: numberOfItems(inSection: lastSection) - 1, section: lastSection) - } // MARK: - Initializers @@ -64,7 +60,7 @@ open class MessagesCollectionView: UICollectionView { // MARK: - Methods - func setupGestureRecognizers() { + private func setupGestureRecognizers() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:))) tapGesture.delaysTouchesBegan = true addGestureRecognizer(tapGesture) @@ -77,7 +73,7 @@ open class MessagesCollectionView: UICollectionView { let touchLocation = gesture.location(in: self) guard let indexPath = indexPathForItem(at: touchLocation) else { return } - let cell = cellForItem(at: indexPath) as? MessageCollectionViewCell + let cell = cellForItem(at: indexPath) as? MessageContentCell cell?.handleTapGesture(gesture) } @@ -106,4 +102,42 @@ open class MessagesCollectionView: UICollectionView { setContentOffset(newOffset, animated: false) } + /// Registers a particular cell using its reuse-identifier + public func register(_ cellClass: T.Type) { + register(cellClass, forCellWithReuseIdentifier: String(describing: T.self)) + } + + /// Registers a reusable view for a specific SectionKind + public func register(_ headerFooterClass: T.Type, forSupplementaryViewOfKind kind: String) { + register(headerFooterClass, + forSupplementaryViewOfKind: kind, + withReuseIdentifier: String(describing: T.self)) + } + + /// Generically dequeues a cell of the correct type allowing you to avoid scattering your code with guard-let-else-fatal + public func dequeueReusableCell(_ cellClass: T.Type, for indexPath: IndexPath) -> T { + guard let cell = dequeueReusableCell(withReuseIdentifier: String(describing: T.self), for: indexPath) as? T else { + fatalError("Unable to dequeue \(String(describing: cellClass)) with reuseId of \(String(describing: T.self))") + } + return cell + } + + /// Generically dequeues a header of the correct type allowing you to avoid scattering your code with guard-let-else-fatal + public func dequeueReusableHeaderView(_ viewClass: T.Type, for indexPath: IndexPath) -> T { + let view = dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: String(describing: T.self), for: indexPath) + guard let viewType = view as? T else { + fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(String(describing: T.self))") + } + return viewType + } + + /// Generically dequeues a footer of the correct type allowing you to avoid scattering your code with guard-let-else-fatal + public func dequeueReusableFooterView(_ viewClass: T.Type, for indexPath: IndexPath) -> T { + let view = dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionFooter, withReuseIdentifier: String(describing: T.self), for: indexPath) + guard let viewType = view as? T else { + fatalError("Unable to dequeue \(String(describing: viewClass)) with reuseId of \(String(describing: T.self))") + } + return viewType + } + } diff --git a/Tests/ControllersTest/MessagesViewControllerTests.swift b/Tests/ControllersTest/MessagesViewControllerTests.swift index 302fd999..520667be 100644 --- a/Tests/ControllersTest/MessagesViewControllerTests.swift +++ b/Tests/ControllersTest/MessagesViewControllerTests.swift @@ -87,7 +87,7 @@ class MessagesViewControllerTests: XCTestCase { sut.messagesCollectionView.reloadData() let count = sut.messagesCollectionView.numberOfSections - let expectedCount = messagesDataSource.numberOfMessages(in: sut.messagesCollectionView) + let expectedCount = messagesDataSource.numberOfSections(in: sut.messagesCollectionView) XCTAssertEqual(count, expectedCount) } diff --git a/Tests/Mocks/MockMessage.swift b/Tests/Mocks/MockMessage.swift index a3e0dc32..cdcb6f08 100644 --- a/Tests/Mocks/MockMessage.swift +++ b/Tests/Mocks/MockMessage.swift @@ -24,6 +24,34 @@ import Foundation import CoreLocation +@testable import MessageKit + +struct MockLocationItem: LocationItem { + + var location: CLLocation + var size: CGSize + + init(location: CLLocation) { + self.location = location + self.size = CGSize(width: 240, height: 240) + } + +} + +struct MockMediaItem: MediaItem { + + var url: URL? + var image: UIImage? + var placeholderImage: UIImage + var size: CGSize + + init(image: UIImage) { + self.image = image + self.size = CGSize(width: 240, height: 240) + self.placeholderImage = UIImage() + } + +} struct MockMessage: MessageType { @@ -48,16 +76,18 @@ struct MockMessage: MessageType { } init(image: UIImage, sender: Sender, messageId: String) { - self.init(data: .photo(image), sender: sender, messageId: messageId) + let mediaItem = MockMediaItem(image: image) + self.init(data: .photo(mediaItem), sender: sender, messageId: messageId) } init(thumbnail: UIImage, sender: Sender, messageId: String) { - let url = URL(fileURLWithPath: "") - self.init(data: .video(file: url, thumbnail: thumbnail), sender: sender, messageId: messageId) + let mediaItem = MockMediaItem(image: thumbnail) + self.init(data: .video(mediaItem), sender: sender, messageId: messageId) } init(location: CLLocation, sender: Sender, messageId: String) { - self.init(data: .location(location), sender: sender, messageId: messageId) + let locationItem = MockLocationItem(location: location) + self.init(data: .location(locationItem), sender: sender, messageId: messageId) } init(emoji: String, sender: Sender, messageId: String) { diff --git a/Tests/Mocks/MockMessagesDataSource.swift b/Tests/Mocks/MockMessagesDataSource.swift index 31e4a0cf..566d8d27 100644 --- a/Tests/Mocks/MockMessagesDataSource.swift +++ b/Tests/Mocks/MockMessagesDataSource.swift @@ -23,6 +23,7 @@ */ import Foundation +@testable import MessageKit class MockMessagesDataSource: MessagesDataSource { @@ -34,7 +35,7 @@ class MockMessagesDataSource: MessagesDataSource { return senders[0] } - func numberOfMessages(in messagesCollectionView: MessagesCollectionView) -> Int { + func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int { return messages.count } diff --git a/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift b/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift index 89a575dd..15d6b774 100644 --- a/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift +++ b/Tests/ProtocolsTests/MessagesDisplayDelegateTests.swift @@ -111,69 +111,12 @@ class MessagesDisplayDelegateTests: XCTestCase { XCTAssertTrue(result) } - func testShouldDisplayHeaderWithoutDataSource_returnsFalseForDefault() { - sut.messagesCollectionView.messagesDataSource = nil - - XCTAssertFalse(sut.shouldDisplayHeader(for: sut.dataProvider.messages[0], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView)) - } - - func testShouldDisplayHeaderForFirstMessage_returnsFalseForDefault() { - XCTAssertFalse(sut.shouldDisplayHeader(for: sut.dataProvider.messages[0], - at: IndexPath(item: 0, section: 0), - in: sut.messagesCollectionView)) - } - - func testShouldDisplayHeaderForMessageWithTimeIntervalSinceLastMessageGreatherThanScheduled_returnsTrue() { - var message = MockMessage(text: "Test", sender: sut.dataProvider.currentSender(), messageId: "003") - let scheduledInterval = sut.messagesCollectionView.showsDateHeaderAfterTimeInterval - message.sentDate = Date(timeIntervalSinceNow: scheduledInterval + 300) - sut.dataProvider.messages.append(message) - - XCTAssertTrue(sut.shouldDisplayHeader(for: sut.dataProvider.messages[2], - at: IndexPath(item: 0, section: 1), - in: sut.messagesCollectionView)) - } - - func testShouldDisplayHeaderForMessageWithTimeIntervalSinceLastMessageEqualThanScheduled_returnsTrue() { - var message = MockMessage(text: "Test", sender: sut.dataProvider.currentSender(), messageId: "003") - let scheduledInterval = sut.messagesCollectionView.showsDateHeaderAfterTimeInterval - message.sentDate = Date(timeIntervalSinceNow: scheduledInterval) - sut.dataProvider.messages.append(message) - - XCTAssertTrue(sut.shouldDisplayHeader(for: sut.dataProvider.messages[2], - at: IndexPath(item: 0, section: 1), - in: sut.messagesCollectionView)) - } - - func testShouldDisplayHeaderForMessageWithTimeIntervalSinceLastMessageLessThanScheduled_returnsFalse() { - var message = MockMessage(text: "Test", sender: sut.dataProvider.currentSender(), messageId: "003") - let scheduledInterval = sut.messagesCollectionView.showsDateHeaderAfterTimeInterval - message.sentDate = Date(timeIntervalSinceNow: scheduledInterval - 300) - sut.dataProvider.messages.append(message) - - XCTAssertFalse(sut.shouldDisplayHeader(for: sut.dataProvider.messages[2], - at: IndexPath(item: 0, section: 1), - in: sut.messagesCollectionView)) - } - func testMessageHeaderView_isNotNil() { - let headerView = sut.messageHeaderView(for: sut.dataProvider.messages[1], - at: IndexPath(item: 0, section: 1), - in: sut.messagesCollectionView) - + let indexPath = IndexPath(item: 0, section: 1) + let headerView = sut.messageHeaderView(for: indexPath, in: sut.messagesCollectionView) XCTAssertNotNil(headerView) } - func testMessageFooterView_isNotNil() { - let footerView = sut.messageFooterView(for: sut.dataProvider.messages[1], - at: IndexPath(item: 0, section: 1), - in: sut.messagesCollectionView) - - XCTAssertNotNil(footerView) - } - } class TextMessageDisplayDelegateTests: XCTestCase { diff --git a/Tests/ViewsTests/MessageCollectionViewCellTests.swift b/Tests/ViewsTests/MessageCollectionViewCellTests.swift index b0583725..da92e331 100644 --- a/Tests/ViewsTests/MessageCollectionViewCellTests.swift +++ b/Tests/ViewsTests/MessageCollectionViewCellTests.swift @@ -25,14 +25,14 @@ import XCTest @testable import MessageKit -class MessageCollectionViewCellTests: XCTestCase { +final class MessageContentCellTests: XCTestCase { - var cell: MessageCollectionViewCell! + var cell: MessageContentCell! let frame = CGRect(x: 0, y: 0, width: 100, height: 100) override func setUp() { super.setUp() - cell = MessageCollectionViewCell(frame: frame) + cell = MessageContentCell(frame: frame) } override func tearDown() { @@ -74,7 +74,7 @@ class MessageCollectionViewCellTests: XCTestCase { } -extension MessageCollectionViewCellTests { +extension MessageContentCellTests { fileprivate class MockMessagesDisplayDelegate: MessagesDisplayDelegate {