Merge pull request #555 from MessageKit/refactor/move-open

MessageKit 1.0 Staging Area
This commit is contained in:
Steven Deutsch
2018-04-11 15:59:14 -05:00
committed by GitHub
71 changed files with 1495 additions and 1972 deletions
+6 -2
View File
@@ -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
- Sources
+102 -24
View File
@@ -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
+15
View File
@@ -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
+19 -1
View File
@@ -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;
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0920"
LastUpgradeVersion = "0930"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -40,9 +40,8 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
codeCoverageEnabled = "YES"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
@@ -71,7 +70,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0920"
LastUpgradeVersion = "0930"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -10,7 +10,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
@@ -31,7 +30,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
+2 -2
View File
@@ -9,8 +9,8 @@ EXTERNAL SOURCES:
:path: ../
SPEC CHECKSUMS:
MessageKit: dcf465101a947bf088cb6a607da04c2d2087d8d7
MessageKit: a70ca5e4c52fae4155f182f13ea8ad12fcd8f27f
PODFILE CHECKSUM: cecdb7bc8129cf99f66de9f68eea3256fec30c3d
COCOAPODS: 1.3.1
COCOAPODS: 1.4.0
+2 -2
View File
@@ -25,11 +25,11 @@
import UIKit
@UIApplicationMain
final class AppDelegate: UIResponder, UIApplicationDelegate {
final internal class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? = nil) -> Bool {
UIApplication.shared.statusBarStyle = .lightContent
window = UIWindow(frame: UIScreen.main.bounds)
@@ -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)")
}
+1 -2
View File
@@ -8,8 +8,7 @@
import MessageKit
class IMessageViewController: MessagesViewController {
internal class IMessageViewController: MessagesViewController {
override func viewDidLoad() {
super.viewDidLoad()
+2 -2
View File
@@ -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()
}
+55 -26
View File
@@ -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) {
+1 -1
View File
@@ -25,7 +25,7 @@
import MessageKit
import CoreLocation
final class SampleData {
final internal class SampleData {
static let shared = SampleData()
+13 -11
View File
@@ -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()
}
}
+1 -1
View File
@@ -24,7 +24,7 @@
import UIKit
class TextFieldTableViewCell: UITableViewCell {
internal class TextFieldTableViewCell: UITableViewCell {
static let identifier = "TextFieldTableViewCellIdentifier"
+44 -39
View File
@@ -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 = "<group>"; };
0EF0888B206F7E83007F2F58 /* CellSizeCalculator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellSizeCalculator.swift; sourceTree = "<group>"; };
1F066E101FD90A0600E11013 /* MessageLabelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageLabelSpec.swift; sourceTree = "<group>"; };
1F066E1C1FDA3C1700E11013 /* SenderSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SenderSpec.swift; sourceTree = "<group>"; };
1F066E201FDA3DEA00E11013 /* AvatarSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarSpec.swift; sourceTree = "<group>"; };
1F066E221FDA3F0200E11013 /* DetectorTypeSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectorTypeSpec.swift; sourceTree = "<group>"; };
1F6C040B206A2891007BDE44 /* MessageContentCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContentCell.swift; sourceTree = "<group>"; };
1F6C040D206A2AF4007BDE44 /* MessageReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReusableView.swift; sourceTree = "<group>"; };
1F7FC8C31FD26F22006CC979 /* Carthage */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Carthage; sourceTree = "<group>"; };
1F7FC8C51FD26F33006CC979 /* Quick.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quick.framework; path = Carthage/Build/iOS/Quick.framework; sourceTree = "<group>"; };
1F7FC8C71FD26F49006CC979 /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Nimble.framework; path = Carthage/Build/iOS/Nimble.framework; sourceTree = "<group>"; };
1F7FC8CB1FD2700B006CC979 /* MessagesViewControllerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewControllerSpec.swift; sourceTree = "<group>"; };
1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarPosition.swift; sourceTree = "<group>"; };
1FAC3487201E44F7000A1504 /* MessagesCollectionViewFlowLayout+Labels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesCollectionViewFlowLayout+Labels.swift"; sourceTree = "<group>"; };
1FAC3489201E4DE7000A1504 /* MessagesCollectionViewFlowLayout+Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesCollectionViewFlowLayout+Avatar.swift"; sourceTree = "<group>"; };
1FAC348B201E4F88000A1504 /* MessagesCollectionViewFlowLayout+MessageContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesCollectionViewFlowLayout+MessageContainerView.swift"; sourceTree = "<group>"; };
1FAC348F201E50DF000A1504 /* MessageCellLayoutContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCellLayoutContext.swift; sourceTree = "<group>"; };
1FAC3491201E5346000A1504 /* MessagesCollectionViewFlowLayout+CellSizing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesCollectionViewFlowLayout+CellSizing.swift"; sourceTree = "<group>"; };
1FCA6D2F201C1CC900BC3480 /* UIEdgeInsets+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Extensions.swift"; sourceTree = "<group>"; };
1FD5895F2064E08A004B5081 /* MediaItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaItem.swift; sourceTree = "<group>"; };
1FD5896320660C1C004B5081 /* LocationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationItem.swift; sourceTree = "<group>"; };
1FE7839D20662835007FA024 /* MessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSizeCalculator.swift; sourceTree = "<group>"; };
1FE783A120662905007FA024 /* TextMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageSizeCalculator.swift; sourceTree = "<group>"; };
1FE783A3206629A5007FA024 /* MediaMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaMessageSizeCalculator.swift; sourceTree = "<group>"; };
1FE783A5206629C2007FA024 /* LocationMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationMessageSizeCalculator.swift; sourceTree = "<group>"; };
1FE783A7206633C0007FA024 /* InsetLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsetLabel.swift; sourceTree = "<group>"; };
1FF377A320087C82004FD648 /* MessageKitError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageKitError.swift; sourceTree = "<group>"; };
1FF377A920087D78004FD648 /* MessagesViewController+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Menu.swift"; sourceTree = "<group>"; };
1FF377AB20087DA2004FD648 /* MessagesViewController+Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessagesViewController+Keyboard.swift"; sourceTree = "<group>"; };
@@ -163,8 +169,6 @@
B7A03F221F866895006AEF79 /* LabelAlignment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelAlignment.swift; sourceTree = "<group>"; };
B7A03F231F866895006AEF79 /* MessageData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageData.swift; sourceTree = "<group>"; };
B7A03F301F866940006AEF79 /* MessageDateHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageDateHeaderView.swift; sourceTree = "<group>"; };
B7A03F311F866940006AEF79 /* MessageFooterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageFooterView.swift; sourceTree = "<group>"; };
B7A03F321F866940006AEF79 /* MessageHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHeaderView.swift; sourceTree = "<group>"; };
B7A03F361F866946006AEF79 /* TextMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextMessageCell.swift; sourceTree = "<group>"; };
B7A03F381F866946006AEF79 /* LocationMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationMessageCell.swift; sourceTree = "<group>"; };
B7A03F391F866946006AEF79 /* MediaMessageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaMessageCell.swift; sourceTree = "<group>"; };
@@ -184,12 +188,10 @@
B7A03F561F8669C9006AEF79 /* MessagesDisplayDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDisplayDelegate.swift; sourceTree = "<group>"; };
B7A03F571F8669CA006AEF79 /* MessagesDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDataSource.swift; sourceTree = "<group>"; };
B7A03F581F8669CA006AEF79 /* MessageInputBarDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageInputBarDelegate.swift; sourceTree = "<group>"; };
B7A03F641F8669EB006AEF79 /* UICollectionView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UICollectionView+Extensions.swift"; sourceTree = "<group>"; };
B7A03F651F8669EB006AEF79 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
B7A03F661F8669EB006AEF79 /* UIView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = "<group>"; };
B7A03F671F8669EB006AEF79 /* Bundle+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+Extensions.swift"; sourceTree = "<group>"; };
B7A03F681F8669EB006AEF79 /* NSAttributedString+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Extensions.swift"; sourceTree = "<group>"; };
B7A03F691F8669EB006AEF79 /* String+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
B7A03F701F866A06006AEF79 /* MessageKit+Availability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MessageKit+Availability.swift"; sourceTree = "<group>"; };
B7A03F711F866A06006AEF79 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B7A03F721F866A06006AEF79 /* MessageKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MessageKit.h; sourceTree = "<group>"; };
@@ -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 = "<group>";
@@ -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 = "<group>";
@@ -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 = "<group>";
@@ -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 */,
);
@@ -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))
}
@@ -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
@@ -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(
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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)
}
@@ -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)
@@ -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<CellType: UICollectionViewCell & CollectionViewReusable>(_ cellClass: CellType.Type) {
register(cellClass, forCellWithReuseIdentifier: CellType.reuseIdentifier())
}
/// Registers a reusable view for a specific SectionKind
func register<ViewType: UICollectionReusableView & CollectionViewReusable>(_ 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<CellType: UICollectionViewCell & CollectionViewReusable>(_ 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<ViewType: UICollectionReusableView & CollectionViewReusable>(_ 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<ViewType: UICollectionReusableView & CollectionViewReusable>(_ 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
}
}
+5 -5
View File
@@ -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)
}
@@ -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
}
+4 -4
View File
@@ -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 []
+48
View File
@@ -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 }
}
@@ -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)")
}
}
}
@@ -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)")
}
}
}
+203
View File
@@ -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)
}
}
@@ -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
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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
}
}
@@ -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<MessageID, MessageCellLayoutContext>()
/// 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)
}
}
@@ -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
}
}
@@ -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
}
}
}
+10
View File
@@ -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
}
}
+1 -1
View File
@@ -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
+11 -30
View File
@@ -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
}
}
+4 -5
View File
@@ -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
+13 -13
View File
@@ -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)."
}
+2 -2
View File
@@ -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"
+12 -12
View File
@@ -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
}
+36
View File
@@ -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 }
}
@@ -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?
}
+10 -9
View File
@@ -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`
+11 -10
View File
@@ -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
+22 -1
View File
@@ -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])
}
+2 -5
View File
@@ -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.
+22 -6
View File
@@ -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
}
+59 -70
View File
@@ -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) {
}
}
+27 -212
View File
@@ -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
}
}
@@ -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.")
}
}
+4 -2
View File
@@ -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
@@ -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
+21 -10
View File
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
+12 -7
View File
@@ -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)
}
}
@@ -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
@@ -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
+1 -1
View File
@@ -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
@@ -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)
}
}
+3 -1
View File
@@ -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 }
+40 -6
View File
@@ -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<T: UICollectionViewCell>(_ cellClass: T.Type) {
register(cellClass, forCellWithReuseIdentifier: String(describing: T.self))
}
/// Registers a reusable view for a specific SectionKind
public func register<T: UICollectionReusableView>(_ 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<T: UICollectionViewCell>(_ 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<T: UICollectionReusableView>(_ 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<T: UICollectionReusableView>(_ 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
}
}
@@ -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)
}
+34 -4
View File
@@ -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) {
+2 -1
View File
@@ -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
}
@@ -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 {
@@ -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 {