mirror of
https://github.com/MessageKit/MessageKit.git
synced 2026-02-06 19:03:19 +00:00
+49
-1
@@ -6,11 +6,59 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa
|
||||
|
||||
## Upcoming release
|
||||
|
||||
## [[Prerelease] 0.11.0](https://github.com/MessageKit/MessageKit/releases/tag/0.11.0)
|
||||
|
||||
### Added
|
||||
|
||||
- **Breaking Change** Added a top `InputStackView` to `MessageInputBar`. This adds the addition of the `.top` case to `InputStackView.Position`.
|
||||
[#320](https://github.com/MessageKit/MessageKit/issues/320) by [@nathantannar4](https://github.com/nathantannar4).
|
||||
|
||||
- **Breaking Change** Added `AvatarPosition` and `avatarPosition(for:at:in)` to configure an
|
||||
`AvatarView`'s vertical and horizontal position in a `MessageCollectionViewCell`.
|
||||
[#322](https://github.com/MessageKit/MessageKit/pull/322) by [@SD10](https://github.com/sd10).
|
||||
|
||||
- Added `shouldCacheLayoutAttributes(for:MessageType)-> Bool` method to `MessagesLayoutDelegate`
|
||||
to manage whether a `MessageType`'s layout information is cached or not.
|
||||
[#364](https://github.com/MessageKit/MessageKit/pull/322) by [@SD10](https://github.com/sd10).
|
||||
|
||||
### Changed
|
||||
|
||||
- **Breaking Change** The `cellTopLabel` and `cellBottomLabel` properties of `MessageCollectionViewCell` are no longer
|
||||
typed as `MessageLabel` and are now regular `UILabel`s.
|
||||
[#355](https://github.com/MessageKit/MessageKit/pull/355) by [@SD10](https://github.com/sd10).
|
||||
|
||||
- All `DetectorType`s for `MessageLabel` are disabled by default.
|
||||
[#356](https://github.com/MessageKit/MessageKit/pull/356) by [@SD10](https://github.com/sd10).
|
||||
|
||||
### Fixed
|
||||
|
||||
Fixes infinite loop when dismissing keyboard on iPhone X.
|
||||
- **Breaking Change** Fixed all instances of misspelled `inital` property. `Avatar.inital` has changed to `Avatar.initial`
|
||||
and the initializer has changed from `public init(image: UIImage? = nil, initals: String = "?")` to `public init(image: UIImage? = nil, initials: String = "?")`.
|
||||
[#298](https://github.com/MessageKit/MessageKit/issues/298) by [@sidmclaughlin](https://github.com/sidmclaughlin).
|
||||
|
||||
- Fixed `MessageInputBar`'s `translucent` functionality.
|
||||
[#348](https://github.com/MessageKit/MessageKit/pull/348) by [@zhongwuzw](https://github.com/zhongwuzw).
|
||||
|
||||
- Fixes infinite loop when dismissing keyboard on iPhone X.
|
||||
[#350](https://github.com/MessageKit/MessageKit/pull/350) by [@nathantannar4](https://github.com/nathantannar4).
|
||||
|
||||
- Fixed incorrect sizing of `cellTopLabel` and `cellBottomLabel`.
|
||||
[#371](https://github.com/MessageKit/MessageKit/pull/371/commits/949796d68d175d2f2c8700ed0cb769935aee2184) by [@SD10](https://github.com/sd10).
|
||||
|
||||
### Removed
|
||||
|
||||
- **Breaking Change** Removed `AvatarAlignment` and `avatarAlignment(for:at:in)` delegate method
|
||||
in favor of new `AvatarPosition` representing both vertical and horizontal alignments.
|
||||
[#322](https://github.com/MessageKit/MessageKit/pull/322) by [@SD10](https://github.com/sd10).
|
||||
|
||||
- **Breaking Change** Removed the `avatarAlwaysLeading` and `avatarAlwaysTrailing` properties of `MessagesCollectionViewFlow$
|
||||
[#322](https://github.com/MessageKit/MessageKit/pull/322) by [@SD10](https://github.com/sd10).
|
||||
|
||||
- **Breaking Change** Removed `LocationMessageDisplayDelegate` & `TextMessageDisplayDelegate` and moved their methods
|
||||
into the `MessagesDisplayDelegate` protocol. Removed `LocationMessageLayoutDelegate` & `MediaMessageLayoutDelegate` and
|
||||
moved their methods into the `MessagesLayoutDelegate` protocol.
|
||||
[#363](https://github.com/MessageKit/MessageKit/pull/363) by [@SD10](https://github.com/sd10).
|
||||
|
||||
## [[Prerelease] 0.10.2](https://github.com/MessageKit/MessageKit/releases/tag/0.10.2)
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
PODS:
|
||||
- MessageKit (0.10.1)
|
||||
- MessageKit (0.10.2)
|
||||
|
||||
DEPENDENCIES:
|
||||
- MessageKit (from `../MessageKit.podspec`)
|
||||
@@ -9,7 +9,7 @@ EXTERNAL SOURCES:
|
||||
:path: ../MessageKit.podspec
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
MessageKit: b0cd905fa1872952f25ff5ea8e67d7135fe7e176
|
||||
MessageKit: a1e8d0f1d7785891e4b37f6252898f23cd8f8b6a
|
||||
|
||||
PODFILE CHECKSUM: 9ac65b8dedf0e1b63fea245b089b6645c4e66309
|
||||
|
||||
|
||||
@@ -220,21 +220,30 @@ extension ConversationViewController: MessagesDataSource {
|
||||
|
||||
func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
|
||||
let name = message.sender.displayName
|
||||
return NSAttributedString(string: name, attributes: [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: .caption1)])
|
||||
return NSAttributedString(string: name, attributes: [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: .caption1)])
|
||||
}
|
||||
|
||||
func cellBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
|
||||
struct ConversationDateFormatter {
|
||||
static let formatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
return formatter
|
||||
}()
|
||||
}
|
||||
let formatter = ConversationDateFormatter.formatter
|
||||
let dateString = formatter.string(from: message.sentDate)
|
||||
return NSAttributedString(string: dateString, attributes: [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: .caption2)])
|
||||
return NSAttributedString(string: dateString, attributes: [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: .caption2)])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MessagesDisplayDelegate
|
||||
|
||||
extension ConversationViewController: MessagesDisplayDelegate, TextMessageDisplayDelegate {
|
||||
extension ConversationViewController: MessagesDisplayDelegate {
|
||||
|
||||
// MARK: - All Messages
|
||||
|
||||
func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
|
||||
return isFromCurrentSender(message: message) ? UIColor(red: 69/255, green: 193/255, blue: 89/255, alpha: 1) : UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1)
|
||||
@@ -251,12 +260,38 @@ extension ConversationViewController: MessagesDisplayDelegate, TextMessageDispla
|
||||
// return .custom(configurationClosure)
|
||||
}
|
||||
|
||||
// MARK: - Location Messages
|
||||
|
||||
func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? {
|
||||
let annotationView = MKAnnotationView(annotation: nil, reuseIdentifier: nil)
|
||||
let pinImage = #imageLiteral(resourceName: "pin")
|
||||
annotationView.image = pinImage
|
||||
annotationView.centerOffset = CGPoint(x: 0, y: -pinImage.size.height / 2)
|
||||
return annotationView
|
||||
}
|
||||
|
||||
func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? {
|
||||
return { view in
|
||||
view.layer.transform = CATransform3DMakeScale(0, 0, 0)
|
||||
view.alpha = 0.0
|
||||
UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0, options: [], animations: {
|
||||
view.layer.transform = CATransform3DIdentity
|
||||
view.alpha = 1.0
|
||||
}, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MessagesLayoutDelegate
|
||||
|
||||
extension ConversationViewController: MessagesLayoutDelegate {
|
||||
|
||||
func avatarPosition(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarPosition {
|
||||
return AvatarPosition(horizontal: .natural, vertical: .messageBottom)
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -281,20 +316,12 @@ extension ConversationViewController: MessagesLayoutDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func avatarAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarAlignment {
|
||||
return .messageBottom
|
||||
}
|
||||
|
||||
func footerViewSize(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGSize {
|
||||
|
||||
return CGSize(width: messagesCollectionView.bounds.width, height: 10)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - LocationMessageLayoutDelegate
|
||||
|
||||
extension ConversationViewController: LocationMessageLayoutDelegate {
|
||||
// MARK: - Location Messages
|
||||
|
||||
func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
|
||||
return 200
|
||||
@@ -302,10 +329,6 @@ extension ConversationViewController: LocationMessageLayoutDelegate {
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MediaMessageLayoutDelegate
|
||||
|
||||
extension ConversationViewController: MediaMessageLayoutDelegate {}
|
||||
|
||||
// MARK: - MessageCellDelegate
|
||||
|
||||
extension ConversationViewController: MessageCellDelegate {
|
||||
@@ -350,31 +373,6 @@ extension ConversationViewController: MessageLabelDelegate {
|
||||
|
||||
}
|
||||
|
||||
// MARK: - LocationMessageDisplayDelegate
|
||||
|
||||
extension ConversationViewController: LocationMessageDisplayDelegate {
|
||||
|
||||
func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? {
|
||||
let annotationView = MKAnnotationView(annotation: nil, reuseIdentifier: nil)
|
||||
let pinImage = #imageLiteral(resourceName: "pin")
|
||||
annotationView.image = pinImage
|
||||
annotationView.centerOffset = CGPoint(x: 0, y: -pinImage.size.height / 2)
|
||||
return annotationView
|
||||
}
|
||||
|
||||
func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? {
|
||||
return { view in
|
||||
view.layer.transform = CATransform3DMakeScale(0, 0, 0)
|
||||
view.alpha = 0.0
|
||||
UIView.animate(withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0, options: [], animations: {
|
||||
view.layer.transform = CATransform3DIdentity
|
||||
view.alpha = 1.0
|
||||
}, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MessageInputBarDelegate
|
||||
|
||||
extension ConversationViewController: MessageInputBarDelegate {
|
||||
@@ -382,7 +380,7 @@ extension ConversationViewController: MessageInputBarDelegate {
|
||||
func messageInputBar(_ inputBar: MessageInputBar, didPressSendButtonWith text: String) {
|
||||
messageList.append(MockMessage(text: text, sender: currentSender(), messageId: UUID().uuidString, date: Date()))
|
||||
inputBar.inputTextView.text = String()
|
||||
messagesCollectionView.reloadData()
|
||||
messagesCollectionView.insertSections([messageList.count - 1])
|
||||
messagesCollectionView.scrollToBottom()
|
||||
}
|
||||
|
||||
|
||||
@@ -185,11 +185,11 @@ final class SampleData {
|
||||
func getAvatarFor(sender: Sender) -> Avatar {
|
||||
switch sender {
|
||||
case dan:
|
||||
return Avatar(image: #imageLiteral(resourceName: "Dan-Leonard"), initals: "DL")
|
||||
return Avatar(image: #imageLiteral(resourceName: "Dan-Leonard"), initials: "DL")
|
||||
case steven:
|
||||
return Avatar(initals: "S")
|
||||
return Avatar(initials: "S")
|
||||
case jobs:
|
||||
return Avatar(image: #imageLiteral(resourceName: "Steve-Jobs"), initals: "SJ")
|
||||
return Avatar(image: #imageLiteral(resourceName: "Steve-Jobs"), initials: "SJ")
|
||||
case cook:
|
||||
return Avatar(image: #imageLiteral(resourceName: "Tim-Cook"))
|
||||
default:
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'MessageKit'
|
||||
s.version = '0.10.2'
|
||||
s.version = '0.11.0'
|
||||
s.license = { :type => "MIT", :file => "LICENSE.md" }
|
||||
|
||||
s.summary = 'An elegant messages UI library for iOS.'
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1F82D1431FB1B75B00B81A88 /* AvatarPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */; };
|
||||
38C57C791F9AE3E50043CC03 /* SeparatorLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C57C781F9AE3E50043CC03 /* SeparatorLine.swift */; };
|
||||
38C57C7C1F9AE4890043CC03 /* InputStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38C57C7B1F9AE4870043CC03 /* InputStackView.swift */; };
|
||||
88916B2D1CF0DF2F00469F91 /* MessageKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 88916B221CF0DF2F00469F91 /* MessageKit.framework */; };
|
||||
@@ -36,11 +37,9 @@
|
||||
B7A03F281F866895006AEF79 /* LocationMessageSnapshotOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F1D1F866895006AEF79 /* LocationMessageSnapshotOptions.swift */; };
|
||||
B7A03F291F866895006AEF79 /* Sender.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F1E1F866895006AEF79 /* Sender.swift */; };
|
||||
B7A03F2A1F866895006AEF79 /* MessageStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F1F1F866895006AEF79 /* MessageStyle.swift */; };
|
||||
B7A03F2B1F866895006AEF79 /* AvatarHorizontalAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F201F866895006AEF79 /* AvatarHorizontalAlignment.swift */; };
|
||||
B7A03F2C1F866895006AEF79 /* DetectorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F211F866895006AEF79 /* DetectorType.swift */; };
|
||||
B7A03F2D1F866895006AEF79 /* LabelAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F221F866895006AEF79 /* LabelAlignment.swift */; };
|
||||
B7A03F2E1F866895006AEF79 /* MessageData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F231F866895006AEF79 /* MessageData.swift */; };
|
||||
B7A03F2F1F866895006AEF79 /* AvatarAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F241F866895006AEF79 /* AvatarAlignment.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 */; };
|
||||
@@ -56,16 +55,13 @@
|
||||
B7A03F4C1F86694F006AEF79 /* PlayButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F441F86694F006AEF79 /* PlayButtonView.swift */; };
|
||||
B7A03F4D1F86694F006AEF79 /* MessagesCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F451F86694F006AEF79 /* MessagesCollectionView.swift */; };
|
||||
B7A03F4F1F86697C006AEF79 /* MessagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F4E1F86697C006AEF79 /* MessagesViewController.swift */; };
|
||||
B7A03F5A1F8669CA006AEF79 /* MediaMessageLayoutDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F501F8669C9006AEF79 /* MediaMessageLayoutDelegate.swift */; };
|
||||
B7A03F5B1F8669CA006AEF79 /* MessageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F511F8669C9006AEF79 /* MessageType.swift */; };
|
||||
B7A03F5C1F8669CA006AEF79 /* MessageCellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F521F8669C9006AEF79 /* MessageCellDelegate.swift */; };
|
||||
B7A03F5D1F8669CA006AEF79 /* LocationMessageDisplayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F531F8669C9006AEF79 /* LocationMessageDisplayDelegate.swift */; };
|
||||
B7A03F5E1F8669CA006AEF79 /* MessagesLayoutDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F541F8669C9006AEF79 /* MessagesLayoutDelegate.swift */; };
|
||||
B7A03F5F1F8669CA006AEF79 /* MessageLabelDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F551F8669C9006AEF79 /* MessageLabelDelegate.swift */; };
|
||||
B7A03F601F8669CA006AEF79 /* MessagesDisplayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F561F8669C9006AEF79 /* MessagesDisplayDelegate.swift */; };
|
||||
B7A03F611F8669CA006AEF79 /* MessagesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F571F8669CA006AEF79 /* MessagesDataSource.swift */; };
|
||||
B7A03F621F8669CA006AEF79 /* MessageInputBarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F581F8669CA006AEF79 /* MessageInputBarDelegate.swift */; };
|
||||
B7A03F631F8669CA006AEF79 /* LocationMessageLayoutDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7A03F591F8669CA006AEF79 /* LocationMessageLayoutDelegate.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 */; };
|
||||
@@ -88,6 +84,7 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarPosition.swift; sourceTree = "<group>"; };
|
||||
38C57C781F9AE3E50043CC03 /* SeparatorLine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeparatorLine.swift; sourceTree = "<group>"; };
|
||||
38C57C7B1F9AE4870043CC03 /* InputStackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputStackView.swift; sourceTree = "<group>"; };
|
||||
88916B221CF0DF2F00469F91 /* MessageKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MessageKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -119,11 +116,9 @@
|
||||
B7A03F1D1F866895006AEF79 /* LocationMessageSnapshotOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationMessageSnapshotOptions.swift; sourceTree = "<group>"; };
|
||||
B7A03F1E1F866895006AEF79 /* Sender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sender.swift; sourceTree = "<group>"; };
|
||||
B7A03F1F1F866895006AEF79 /* MessageStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageStyle.swift; sourceTree = "<group>"; };
|
||||
B7A03F201F866895006AEF79 /* AvatarHorizontalAlignment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarHorizontalAlignment.swift; sourceTree = "<group>"; };
|
||||
B7A03F211F866895006AEF79 /* DetectorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetectorType.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
B7A03F241F866895006AEF79 /* AvatarAlignment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarAlignment.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>"; };
|
||||
@@ -139,16 +134,13 @@
|
||||
B7A03F441F86694F006AEF79 /* PlayButtonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayButtonView.swift; sourceTree = "<group>"; };
|
||||
B7A03F451F86694F006AEF79 /* MessagesCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesCollectionView.swift; sourceTree = "<group>"; };
|
||||
B7A03F4E1F86697C006AEF79 /* MessagesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesViewController.swift; sourceTree = "<group>"; };
|
||||
B7A03F501F8669C9006AEF79 /* MediaMessageLayoutDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaMessageLayoutDelegate.swift; sourceTree = "<group>"; };
|
||||
B7A03F511F8669C9006AEF79 /* MessageType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageType.swift; sourceTree = "<group>"; };
|
||||
B7A03F521F8669C9006AEF79 /* MessageCellDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageCellDelegate.swift; sourceTree = "<group>"; };
|
||||
B7A03F531F8669C9006AEF79 /* LocationMessageDisplayDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationMessageDisplayDelegate.swift; sourceTree = "<group>"; };
|
||||
B7A03F541F8669C9006AEF79 /* MessagesLayoutDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesLayoutDelegate.swift; sourceTree = "<group>"; };
|
||||
B7A03F551F8669C9006AEF79 /* MessageLabelDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageLabelDelegate.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
B7A03F591F8669CA006AEF79 /* LocationMessageLayoutDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationMessageLayoutDelegate.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>"; };
|
||||
@@ -333,8 +325,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B7A03F1C1F866895006AEF79 /* Avatar.swift */,
|
||||
B7A03F241F866895006AEF79 /* AvatarAlignment.swift */,
|
||||
B7A03F201F866895006AEF79 /* AvatarHorizontalAlignment.swift */,
|
||||
1F82D1421FB1B75B00B81A88 /* AvatarPosition.swift */,
|
||||
B7A03F211F866895006AEF79 /* DetectorType.swift */,
|
||||
B7A03F221F866895006AEF79 /* LabelAlignment.swift */,
|
||||
B7A03F1D1F866895006AEF79 /* LocationMessageSnapshotOptions.swift */,
|
||||
@@ -369,9 +360,6 @@
|
||||
B096439B1F295D82004D0129 /* Protocols */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B7A03F531F8669C9006AEF79 /* LocationMessageDisplayDelegate.swift */,
|
||||
B7A03F591F8669CA006AEF79 /* LocationMessageLayoutDelegate.swift */,
|
||||
B7A03F501F8669C9006AEF79 /* MediaMessageLayoutDelegate.swift */,
|
||||
B7A03F521F8669C9006AEF79 /* MessageCellDelegate.swift */,
|
||||
B7A03F581F8669CA006AEF79 /* MessageInputBarDelegate.swift */,
|
||||
B7A03F551F8669C9006AEF79 /* MessageLabelDelegate.swift */,
|
||||
@@ -538,7 +526,6 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B7A03F2B1F866895006AEF79 /* AvatarHorizontalAlignment.swift in Sources */,
|
||||
B7A03F3C1F866946006AEF79 /* LocationMessageCell.swift in Sources */,
|
||||
38C57C7C1F9AE4890043CC03 /* InputStackView.swift in Sources */,
|
||||
B7A03F5B1F8669CA006AEF79 /* MessageType.swift in Sources */,
|
||||
@@ -550,10 +537,8 @@
|
||||
B006FA021F99DE2100509C46 /* MessageIntermediateLayoutAttributes.swift in Sources */,
|
||||
B7A03F4B1F86694F006AEF79 /* MessageContainerView.swift in Sources */,
|
||||
B7A03F281F866895006AEF79 /* LocationMessageSnapshotOptions.swift in Sources */,
|
||||
B7A03F5D1F8669CA006AEF79 /* LocationMessageDisplayDelegate.swift in Sources */,
|
||||
B7A03F481F86694F006AEF79 /* InputTextView.swift in Sources */,
|
||||
B7A03F6C1F8669EB006AEF79 /* UIView+Extensions.swift in Sources */,
|
||||
B7A03F631F8669CA006AEF79 /* LocationMessageLayoutDelegate.swift in Sources */,
|
||||
B7A03F3A1F866946006AEF79 /* TextMessageCell.swift in Sources */,
|
||||
B7A03F191F86682C006AEF79 /* MessagesCollectionViewLayoutAttributes.swift in Sources */,
|
||||
B7A03F461F86694F006AEF79 /* AvatarView.swift in Sources */,
|
||||
@@ -573,7 +558,6 @@
|
||||
B7A03F471F86694F006AEF79 /* MessageLabel.swift in Sources */,
|
||||
B7A03F5F1F8669CA006AEF79 /* MessageLabelDelegate.swift in Sources */,
|
||||
B7A03F4C1F86694F006AEF79 /* PlayButtonView.swift in Sources */,
|
||||
B7A03F5A1F8669CA006AEF79 /* MediaMessageLayoutDelegate.swift in Sources */,
|
||||
B7A03F291F866895006AEF79 /* Sender.swift in Sources */,
|
||||
B7A03F4F1F86697C006AEF79 /* MessagesViewController.swift in Sources */,
|
||||
B7A03F251F866895006AEF79 /* NSConstraintLayoutSet.swift in Sources */,
|
||||
@@ -588,7 +572,7 @@
|
||||
38C57C791F9AE3E50043CC03 /* SeparatorLine.swift in Sources */,
|
||||
B7A03F2C1F866895006AEF79 /* DetectorType.swift in Sources */,
|
||||
B7A03F271F866895006AEF79 /* Avatar.swift in Sources */,
|
||||
B7A03F2F1F866895006AEF79 /* AvatarAlignment.swift in Sources */,
|
||||
1F82D1431FB1B75B00B81A88 /* AvatarPosition.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
import Foundation
|
||||
|
||||
/// Optional Cell Protocol to Simplify registration/cell type loading in a generic way
|
||||
public protocol CollectionViewReusable: class {
|
||||
public protocol CollectionViewReusable: AnyObject {
|
||||
static func reuseIdentifier() -> String
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
/// A intermediate context used to store recently calculated values used by
|
||||
/// the `MessagesCollectionViewFlowLayout` object to reduce redundant calculations.
|
||||
final class MessageIntermediateLayoutAttributes {
|
||||
@@ -31,70 +32,184 @@ final class MessageIntermediateLayoutAttributes {
|
||||
// Message
|
||||
var message: MessageType
|
||||
var indexPath: IndexPath
|
||||
|
||||
// Cell
|
||||
var itemHeight: CGFloat = 0
|
||||
|
||||
init(message: MessageType, indexPath: IndexPath) {
|
||||
self.message = message
|
||||
self.indexPath = indexPath
|
||||
}
|
||||
var cellFrame: CGRect = .zero
|
||||
|
||||
// AvatarView
|
||||
var avatarPosition = AvatarPosition(horizontal: .cellLeading, vertical: .cellBottom)
|
||||
var avatarSize: CGSize = .zero
|
||||
var avatarVertical: AvatarAlignment = .messageBottom
|
||||
var avatarHorizontal: AvatarHorizontalAlignment = .cellLeading
|
||||
|
||||
lazy var avatarFrame: CGRect = {
|
||||
|
||||
guard avatarSize != .zero else { return .zero }
|
||||
|
||||
var origin = CGPoint.zero
|
||||
|
||||
switch avatarPosition.horizontal {
|
||||
case .cellLeading:
|
||||
break
|
||||
case .cellTrailing:
|
||||
origin.x = cellFrame.width - avatarSize.width
|
||||
case .natural:
|
||||
fatalError("AvatarPosition Horizontal.natural needs to be resolved.")
|
||||
}
|
||||
|
||||
switch avatarPosition.vertical {
|
||||
case .cellTop:
|
||||
break
|
||||
case .cellBottom:
|
||||
origin.y = cellFrame.height - avatarSize.height
|
||||
case .messageTop:
|
||||
origin.y = messageContainerFrame.minY
|
||||
case .messageBottom:
|
||||
origin.y = messageContainerFrame.maxY - avatarSize.height
|
||||
case .messageCenter:
|
||||
origin.y = messageContainerFrame.midY - (avatarSize.height/2)
|
||||
}
|
||||
|
||||
return CGRect(origin: origin, size: avatarSize)
|
||||
|
||||
}()
|
||||
|
||||
// MessageContainerView
|
||||
var messageContainerSize: CGSize = .zero
|
||||
var messageContainerMaxWidth: CGFloat = 0
|
||||
var messageContainerPadding: UIEdgeInsets = .zero
|
||||
var messageLabelInsets: UIEdgeInsets = .zero
|
||||
|
||||
lazy var messageContainerFrame: CGRect = {
|
||||
|
||||
guard messageContainerSize != .zero else { return .zero }
|
||||
|
||||
var origin: CGPoint = .zero
|
||||
origin.y = topLabelSize.height + messageContainerPadding.top + topLabelVerticalPadding
|
||||
|
||||
switch avatarPosition.horizontal {
|
||||
case .cellLeading:
|
||||
origin.x = avatarSize.width + messageContainerPadding.left
|
||||
case .cellTrailing:
|
||||
origin.x = cellFrame.width - avatarSize.width - messageContainerSize.width - messageContainerPadding.right
|
||||
case .natural:
|
||||
fatalError("AvatarPosition Horizontal.natural needs to be resolved.")
|
||||
}
|
||||
|
||||
return CGRect(origin: origin, size: messageContainerSize)
|
||||
|
||||
}()
|
||||
|
||||
// Cell Top Label
|
||||
var topLabelAlignment: LabelAlignment = .cellLeading(.zero)
|
||||
var topLabelSize: CGSize = .zero
|
||||
var topLabelMaxWidth: CGFloat = 0
|
||||
|
||||
lazy var topLabelFrame: CGRect = {
|
||||
|
||||
guard topLabelSize != .zero else { return .zero }
|
||||
|
||||
var origin = CGPoint.zero
|
||||
|
||||
origin.y = topLabelPadding.top
|
||||
|
||||
switch topLabelAlignment {
|
||||
case .cellLeading:
|
||||
origin.x = topLabelPadding.left
|
||||
case .cellCenter:
|
||||
origin.x = (cellFrame.width/2) + topLabelPadding.left - topLabelPadding.right
|
||||
case .cellTrailing:
|
||||
origin.x = cellFrame.width - topLabelSize.width - topLabelPadding.right
|
||||
case .messageLeading:
|
||||
origin.x = messageContainerFrame.minX + topLabelPadding.left
|
||||
case .messageTrailing:
|
||||
origin.x = messageContainerFrame.maxX - topLabelSize.width - topLabelPadding.right
|
||||
}
|
||||
|
||||
return CGRect(origin: origin, size: topLabelSize)
|
||||
|
||||
}()
|
||||
|
||||
// Cell Bottom Label
|
||||
var bottomLabelAlignment: LabelAlignment = .cellTrailing(.zero)
|
||||
var bottomLabelSize: CGSize = .zero
|
||||
var bottomLabelMaxWidth: CGFloat = 0
|
||||
|
||||
lazy var bottomLabelFrame: CGRect = {
|
||||
|
||||
guard bottomLabelSize != .zero else { return .zero }
|
||||
|
||||
var origin: CGPoint = .zero
|
||||
|
||||
origin.y = messageContainerFrame.maxY + messageContainerPadding.bottom + bottomLabelPadding.top
|
||||
|
||||
switch bottomLabelAlignment {
|
||||
case .cellLeading:
|
||||
origin.x = bottomLabelPadding.left
|
||||
case .cellCenter:
|
||||
origin.x = (cellFrame.width/2) + bottomLabelPadding.left - bottomLabelPadding.right
|
||||
case .cellTrailing:
|
||||
origin.x = cellFrame.width - bottomLabelSize.width - bottomLabelPadding.right
|
||||
case .messageLeading:
|
||||
origin.x = messageContainerFrame.minX + bottomLabelPadding.left
|
||||
case .messageTrailing:
|
||||
origin.x = messageContainerFrame.maxX - bottomLabelSize.width - bottomLabelPadding.right
|
||||
}
|
||||
|
||||
return CGRect(origin: origin, size: bottomLabelSize)
|
||||
|
||||
}()
|
||||
|
||||
// MARK: - Initializer
|
||||
|
||||
init(message: MessageType, indexPath: IndexPath) {
|
||||
self.message = message
|
||||
self.indexPath = indexPath
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
extension MessageIntermediateLayoutAttributes {
|
||||
|
||||
var bottomLabelPadding: UIEdgeInsets {
|
||||
return bottomLabelAlignment.insets
|
||||
}
|
||||
|
||||
var bottomLabelVerticalPadding: CGFloat {
|
||||
return bottomLabelPadding.top + bottomLabelPadding.bottom
|
||||
}
|
||||
|
||||
var bottomLabelHorizontalPadding: CGFloat {
|
||||
return bottomLabelPadding.left + bottomLabelPadding.right
|
||||
}
|
||||
|
||||
var topLabelPadding: UIEdgeInsets {
|
||||
return topLabelAlignment.insets
|
||||
}
|
||||
|
||||
var topLabelVerticalPadding: CGFloat {
|
||||
return topLabelPadding.top + topLabelPadding.bottom
|
||||
}
|
||||
|
||||
var topLabelHorizontalPadding: CGFloat {
|
||||
return topLabelPadding.left + topLabelPadding.right
|
||||
}
|
||||
|
||||
var messageLabelVerticalInsets: CGFloat {
|
||||
return messageLabelInsets.top + messageLabelInsets.bottom
|
||||
}
|
||||
|
||||
|
||||
var messageLabelHorizontalInsets: CGFloat {
|
||||
return messageLabelInsets.left + messageLabelInsets.right
|
||||
}
|
||||
|
||||
|
||||
var messageVerticalPadding: CGFloat {
|
||||
return messageContainerPadding.top + messageContainerPadding.bottom
|
||||
}
|
||||
|
||||
|
||||
var messageHorizontalPadding: CGFloat {
|
||||
return messageContainerPadding.left + messageContainerPadding.right
|
||||
}
|
||||
|
||||
// Cell Top Label
|
||||
var cellBottomLabelText: NSAttributedString?
|
||||
var cellTopLabelSize: CGSize = .zero
|
||||
var cellTopLabelMaxWidth: CGFloat = 0
|
||||
var cellTopLabelAlignment: LabelAlignment = .cellLeading(.zero)
|
||||
|
||||
var cellTopLabelVerticalInsets: CGFloat {
|
||||
let cellTopLabelInsets = cellTopLabelAlignment.insets
|
||||
return cellTopLabelInsets.top + cellTopLabelInsets.bottom
|
||||
}
|
||||
|
||||
var cellTopLabelHorizontalInsets: CGFloat {
|
||||
let cellTopLabelInsets = cellTopLabelAlignment.insets
|
||||
return cellTopLabelInsets.left + cellTopLabelInsets.right
|
||||
}
|
||||
|
||||
// Cell Bottom Label
|
||||
var cellTopLabelText: NSAttributedString?
|
||||
var cellBottomLabelSize: CGSize = .zero
|
||||
var cellBottomLabelMaxWidth: CGFloat = 0
|
||||
var cellBottomLabelAlignment: LabelAlignment = .cellTrailing(.zero)
|
||||
|
||||
var cellBottomLabelVerticalInsets: CGFloat {
|
||||
let cellBottomLabelInsets = cellBottomLabelAlignment.insets
|
||||
return cellBottomLabelInsets.top + cellBottomLabelInsets.bottom
|
||||
}
|
||||
|
||||
var cellBottomLabelHorizontalInsets: CGFloat {
|
||||
let cellBottomLabelInsets = cellBottomLabelAlignment.insets
|
||||
return cellBottomLabelInsets.left + cellBottomLabelInsets.right
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -40,34 +40,6 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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`.
|
||||
open var avatarAlwaysLeading: Bool {
|
||||
willSet {
|
||||
if newValue {
|
||||
avatarAlwaysTrailing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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`.
|
||||
open var avatarAlwaysTrailing: Bool {
|
||||
willSet {
|
||||
if newValue {
|
||||
avatarAlwaysLeading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines the maximum number of `MessageCollectionViewCell` attributes to cache.
|
||||
///
|
||||
/// The default value of this property is 500.
|
||||
@@ -85,10 +57,11 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout {
|
||||
/// The default value of this property is 2x the `messageLabelFont`.
|
||||
private var emojiLabelFont: UIFont
|
||||
|
||||
typealias MessageID = String
|
||||
|
||||
/// The cache for `MessageIntermediateLayoutAttributes`.
|
||||
/// The key is the `messageId` of the `MessageType`.
|
||||
fileprivate var intermediateAttributesCache: [MessageID: MessageIntermediateLayoutAttributes] = [:]
|
||||
typealias MessageID = String
|
||||
|
||||
/// Convenience property for accessing the layout object's `MessagesCollectionView`.
|
||||
fileprivate var messagesCollectionView: MessagesCollectionView {
|
||||
@@ -127,9 +100,6 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout {
|
||||
messageLabelFont = UIFont.preferredFont(forTextStyle: .body)
|
||||
emojiLabelFont = messageLabelFont.withSize(2 * messageLabelFont.pointSize)
|
||||
|
||||
avatarAlwaysLeading = false
|
||||
avatarAlwaysTrailing = false
|
||||
|
||||
super.init()
|
||||
|
||||
sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8)
|
||||
@@ -224,8 +194,6 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout {
|
||||
let attributes = messageIntermediateLayoutAttributes(for: indexPath)
|
||||
return CGSize(width: itemWidth, height: attributes.itemHeight)
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -245,8 +213,10 @@ fileprivate extension MessagesCollectionViewFlowLayout {
|
||||
return intermediateAttributes
|
||||
} else {
|
||||
let newAttributes = createMessageIntermediateLayoutAttributes(for: message, at: indexPath)
|
||||
|
||||
let shouldCache = messagesLayoutDelegate.shouldCacheLayoutAttributes(for: message) && intermediateAttributesCache.count < attributesCacheMaxSize
|
||||
|
||||
if intermediateAttributesCache.count < attributesCacheMaxSize {
|
||||
if shouldCache {
|
||||
intermediateAttributesCache[message.messageId] = newAttributes
|
||||
}
|
||||
return newAttributes
|
||||
@@ -264,8 +234,7 @@ fileprivate extension MessagesCollectionViewFlowLayout {
|
||||
let attributes = MessageIntermediateLayoutAttributes(message: message, indexPath: indexPath)
|
||||
|
||||
// None of these are dependent on other attributes
|
||||
attributes.avatarVertical = avatarVerticalAlignment(for: attributes)
|
||||
attributes.avatarHorizontal = avatarHorizontalAlignment(for: attributes)
|
||||
attributes.avatarPosition = avatarPosition(for: attributes)
|
||||
attributes.avatarSize = avatarSize(for: attributes)
|
||||
attributes.messageContainerPadding = messageContainerPadding(for: attributes)
|
||||
attributes.messageLabelInsets = messageLabelInsets(for: attributes)
|
||||
@@ -275,16 +244,14 @@ fileprivate extension MessagesCollectionViewFlowLayout {
|
||||
attributes.messageContainerSize = messageContainerSize(for: attributes)
|
||||
|
||||
// Cell Bottom Label
|
||||
attributes.cellBottomLabelText = cellBottomLabelText(for: attributes) // little concerned about storing text here TODO
|
||||
attributes.cellBottomLabelAlignment = cellBottomLabelAlignment(for: attributes)
|
||||
attributes.cellBottomLabelMaxWidth = cellBottomLabelMaxWidth(for: attributes)
|
||||
attributes.cellBottomLabelSize = cellBottomLabelSize(for: attributes)
|
||||
attributes.bottomLabelAlignment = cellBottomLabelAlignment(for: attributes)
|
||||
attributes.bottomLabelMaxWidth = cellBottomLabelMaxWidth(for: attributes)
|
||||
attributes.bottomLabelSize = cellBottomLabelSize(for: attributes)
|
||||
|
||||
// Cell Top Label
|
||||
attributes.cellTopLabelText = cellTopLabelText(for: attributes) // little concerned about storing text here TODO
|
||||
attributes.cellTopLabelAlignment = cellTopLabelAlignment(for: attributes)
|
||||
attributes.cellTopLabelMaxWidth = cellTopLabelMaxWidth(for: attributes)
|
||||
attributes.cellTopLabelSize = cellTopLabelSize(for: attributes)
|
||||
attributes.topLabelAlignment = cellTopLabelAlignment(for: attributes)
|
||||
attributes.topLabelMaxWidth = cellTopLabelMaxWidth(for: attributes)
|
||||
attributes.topLabelSize = cellTopLabelSize(for: attributes)
|
||||
|
||||
// Cell Height
|
||||
attributes.itemHeight = cellHeight(for: attributes)
|
||||
@@ -301,17 +268,13 @@ fileprivate extension MessagesCollectionViewFlowLayout {
|
||||
|
||||
let intermediateAttributes = messageIntermediateLayoutAttributes(for: attributes.indexPath)
|
||||
|
||||
let messageOrigin = messageContainerOrigin(for: intermediateAttributes, and: attributes.frame)
|
||||
attributes.messageContainerFrame = CGRect(origin: messageOrigin, size: intermediateAttributes.messageContainerSize)
|
||||
intermediateAttributes.cellFrame = attributes.frame
|
||||
|
||||
let avaOrigin = avatarOrigin(for: intermediateAttributes, and: attributes.frame)
|
||||
attributes.avatarFrame = CGRect(origin: avaOrigin, size: intermediateAttributes.avatarSize)
|
||||
|
||||
let topLabelOrigin = cellTopLabelOrigin(for: intermediateAttributes, and: attributes.frame)
|
||||
attributes.cellTopLabelFrame = CGRect(origin: topLabelOrigin, size: intermediateAttributes.cellTopLabelSize)
|
||||
|
||||
let bottomLabelOrigin = cellBottomLabelOrigin(for: intermediateAttributes, and: attributes.frame)
|
||||
attributes.cellBottomLabelFrame = CGRect(origin: bottomLabelOrigin, size: intermediateAttributes.cellBottomLabelSize)
|
||||
attributes.messageContainerFrame = intermediateAttributes.messageContainerFrame
|
||||
attributes.topLabelFrame = intermediateAttributes.topLabelFrame
|
||||
attributes.bottomLabelFrame = intermediateAttributes.bottomLabelFrame
|
||||
attributes.avatarFrame = intermediateAttributes.avatarFrame
|
||||
attributes.messageLabelInsets = intermediateAttributes.messageLabelInsets
|
||||
|
||||
switch intermediateAttributes.message.data {
|
||||
case .emoji:
|
||||
@@ -319,15 +282,9 @@ fileprivate extension MessagesCollectionViewFlowLayout {
|
||||
default:
|
||||
attributes.messageLabelFont = messageLabelFont
|
||||
}
|
||||
|
||||
attributes.avatarHorizontalAlignment = intermediateAttributes.avatarHorizontal
|
||||
attributes.messagePadding = intermediateAttributes.messageContainerPadding
|
||||
attributes.messageLabelInsets = intermediateAttributes.messageLabelInsets
|
||||
attributes.cellTopLabelInsets = intermediateAttributes.cellTopLabelAlignment.insets
|
||||
attributes.cellBottomLabelInsets = intermediateAttributes.cellBottomLabelAlignment.insets
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Avatar Calculations [ A - C ]
|
||||
@@ -336,19 +293,24 @@ fileprivate extension MessagesCollectionViewFlowLayout {
|
||||
|
||||
// A
|
||||
|
||||
/// Returns the horizontal alignment of the `AvatarView` for a given `MessageType`.
|
||||
/// Returns the `AvatarPosition` for a given `MessageType`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - attributes: The `MessageIntermediateLayoutAttributes` containing the `MessageType` object.
|
||||
func avatarHorizontalAlignment(for attributes: MessageIntermediateLayoutAttributes) -> AvatarHorizontalAlignment {
|
||||
func avatarPosition(for attributes: MessageIntermediateLayoutAttributes) -> AvatarPosition {
|
||||
var position = messagesLayoutDelegate.avatarPosition(for: attributes.message, at: attributes.indexPath, in: messagesCollectionView)
|
||||
|
||||
if avatarAlwaysTrailing { return .cellTrailing }
|
||||
if avatarAlwaysLeading { return .cellLeading }
|
||||
|
||||
return messagesDataSource.isFromCurrentSender(message: attributes.message) ? .cellTrailing : .cellLeading
|
||||
switch position.horizontal {
|
||||
case .cellTrailing, .cellLeading:
|
||||
break
|
||||
case .natural:
|
||||
position.horizontal = messagesDataSource.isFromCurrentSender(message: attributes.message) ? .cellTrailing : .cellLeading
|
||||
}
|
||||
|
||||
return position
|
||||
}
|
||||
|
||||
|
||||
// B
|
||||
|
||||
/// Returns the size of the `AvatarView` for a given `MessageType`.
|
||||
@@ -359,16 +321,6 @@ fileprivate extension MessagesCollectionViewFlowLayout {
|
||||
return messagesLayoutDelegate.avatarSize(for: attributes.message, at: attributes.indexPath, in: messagesCollectionView)
|
||||
}
|
||||
|
||||
// C
|
||||
|
||||
/// Returns the vertical alignment of the `AvatarView` for a given `MessageType`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - attributes: The `MessageIntermediateLayoutAttributes` containing the `MessageType` object.
|
||||
func avatarVerticalAlignment(for attributes: MessageIntermediateLayoutAttributes) -> AvatarAlignment {
|
||||
return messagesLayoutDelegate.avatarAlignment(for: attributes.message, at: attributes.indexPath, in: messagesCollectionView)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - General Label Size Calculations
|
||||
@@ -479,18 +431,12 @@ private extension MessagesCollectionViewFlowLayout {
|
||||
messageContainerSize.width += attributes.messageLabelHorizontalInsets
|
||||
messageContainerSize.height += attributes.messageLabelVerticalInsets
|
||||
case .photo, .video:
|
||||
guard let layoutDelegate = messagesLayoutDelegate as? MediaMessageLayoutDelegate else {
|
||||
fatalError("Layout object attempting to size media message type and MediaMessageLayoutDelegate is not set.")
|
||||
}
|
||||
let width = layoutDelegate.widthForMedia(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView)
|
||||
let height = layoutDelegate.heightForMedia(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView)
|
||||
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:
|
||||
guard let layoutDelegate = messagesLayoutDelegate as? LocationMessageLayoutDelegate else {
|
||||
fatalError("Layout object attempting to size location message type and LocationMessageLayoutDelegate is not set.")
|
||||
}
|
||||
let width = layoutDelegate.widthForLocation(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView)
|
||||
let height = layoutDelegate.heightForLocation(message: message, at: indexPath, with: maxWidth, in: messagesCollectionView)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -500,20 +446,10 @@ private extension MessagesCollectionViewFlowLayout {
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Cell Bottom Label Calculations [ H - K ]
|
||||
// MARK: - Cell Bottom Label Calculations [ I - K ]
|
||||
|
||||
private extension MessagesCollectionViewFlowLayout {
|
||||
|
||||
// H
|
||||
|
||||
/// Returns the attributed text for the cell's bottom label.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - attributes: The `MessageIntermediateLayoutAttributes` containing the `MessageType` object.
|
||||
func cellBottomLabelText(for attributes: MessageIntermediateLayoutAttributes) -> NSAttributedString? {
|
||||
return messagesDataSource.cellBottomLabelAttributedText(for: attributes.message, at: attributes.indexPath)
|
||||
}
|
||||
|
||||
// I
|
||||
|
||||
/// Returns the alignment of the cell's bottom label.
|
||||
@@ -532,35 +468,38 @@ private extension MessagesCollectionViewFlowLayout {
|
||||
/// - attributes: The `MessageIntermediateLayoutAttributes` to consider when calculating the max width.
|
||||
func cellBottomLabelMaxWidth(for attributes: MessageIntermediateLayoutAttributes) -> CGFloat {
|
||||
|
||||
guard attributes.cellBottomLabelText != nil else { return 0 }
|
||||
|
||||
let labelHorizontal = attributes.cellBottomLabelAlignment
|
||||
let avatarHorizontal = attributes.avatarHorizontal
|
||||
let avatarVertical = attributes.avatarVertical
|
||||
let labelHorizontal = attributes.bottomLabelAlignment
|
||||
let avatarHorizontal = attributes.avatarPosition.horizontal
|
||||
let avatarVertical = attributes.avatarPosition.vertical
|
||||
let avatarWidth = attributes.avatarSize.width
|
||||
|
||||
switch (labelHorizontal, avatarHorizontal, avatarVertical) {
|
||||
|
||||
case (.cellLeading, .cellTrailing, .cellBottom), (.cellTrailing, .cellLeading, .cellBottom):
|
||||
return itemWidth - avatarWidth - attributes.cellBottomLabelHorizontalInsets
|
||||
|
||||
case (.cellLeading, _, _), (.cellTrailing, _, _):
|
||||
return itemWidth - attributes.cellBottomLabelHorizontalInsets
|
||||
|
||||
case (.cellCenter, .cellLeading, .cellBottom), (.cellCenter, .cellTrailing, .cellBottom):
|
||||
return itemWidth - (avatarWidth * 2) - attributes.cellBottomLabelHorizontalInsets
|
||||
|
||||
case (.cellCenter, .cellLeading, _), (.cellCenter, .cellTrailing, _):
|
||||
return itemWidth - attributes.cellBottomLabelHorizontalInsets
|
||||
|
||||
case (.messageTrailing, .cellLeading, .cellBottom), (.messageLeading, .cellTrailing, .cellBottom):
|
||||
return attributes.messageContainerSize.width - attributes.cellBottomLabelHorizontalInsets
|
||||
|
||||
case (.messageLeading, .cellLeading, _), (.messageTrailing, .cellTrailing, _):
|
||||
return itemWidth - avatarWidth - attributes.cellBottomLabelHorizontalInsets
|
||||
|
||||
case (.messageLeading, .cellTrailing, _), (.messageTrailing, .cellLeading, _):
|
||||
return attributes.messageContainerSize.width + avatarWidth - attributes.cellBottomLabelHorizontalInsets
|
||||
|
||||
|
||||
switch (labelHorizontal, avatarHorizontal) {
|
||||
|
||||
case (.cellLeading, _), (.cellTrailing, _):
|
||||
let width = itemWidth - attributes.bottomLabelHorizontalPadding
|
||||
return avatarVertical != .cellBottom ? width : width - avatarWidth
|
||||
|
||||
case (.cellCenter, _):
|
||||
let width = itemWidth - attributes.bottomLabelHorizontalPadding
|
||||
return avatarVertical != .cellBottom ? width : width - (avatarWidth * 2)
|
||||
|
||||
case (.messageTrailing, .cellLeading):
|
||||
let width = attributes.messageContainerSize.width + attributes.messageContainerPadding.left - attributes.bottomLabelHorizontalPadding
|
||||
return avatarVertical == .cellBottom ? width : width + avatarWidth
|
||||
|
||||
case (.messageLeading, .cellTrailing):
|
||||
let width = attributes.messageContainerSize.width + attributes.messageContainerPadding.right - attributes.bottomLabelHorizontalPadding
|
||||
return avatarVertical == .cellBottom ? width : width + avatarWidth
|
||||
|
||||
case (.messageLeading, .cellLeading):
|
||||
return itemWidth - avatarWidth - attributes.messageContainerPadding.left - attributes.bottomLabelHorizontalPadding
|
||||
|
||||
case (.messageTrailing, .cellTrailing):
|
||||
return itemWidth - avatarWidth - attributes.messageContainerPadding.right - attributes.bottomLabelHorizontalPadding
|
||||
|
||||
case (_, .natural):
|
||||
fatalError("AvatarPosition Horizontal.natural needs to be resolved.")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -572,34 +511,21 @@ private extension MessagesCollectionViewFlowLayout {
|
||||
/// - Parameters:
|
||||
/// - attributes: The `MessageIntermediateLayoutAttributes` to consider when calculating label's size.
|
||||
func cellBottomLabelSize(for attributes: MessageIntermediateLayoutAttributes) -> CGSize {
|
||||
guard let bottomLabelText = attributes.cellBottomLabelText else { return .zero }
|
||||
|
||||
var bottomLabelSize = labelSize(for: bottomLabelText, considering: attributes.cellBottomLabelMaxWidth)
|
||||
bottomLabelSize.width += attributes.cellBottomLabelHorizontalInsets
|
||||
bottomLabelSize.height += attributes.cellBottomLabelVerticalInsets
|
||||
|
||||
return bottomLabelSize
|
||||
let text = messagesDataSource.cellBottomLabelAttributedText(for: attributes.message, at: attributes.indexPath)
|
||||
|
||||
guard let bottomLabelText = text else { return .zero }
|
||||
return labelSize(for: bottomLabelText, considering: attributes.bottomLabelMaxWidth)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Cell Top Label Size Calculations [ L - O ]
|
||||
// MARK: - Cell Top Label Size Calculations [ L - N ]
|
||||
|
||||
private extension MessagesCollectionViewFlowLayout {
|
||||
|
||||
// L
|
||||
|
||||
/// Returns the attributed text for the cell's top label.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - attributes: The `MessageIntermediateLayoutAttributes` containing the `MessageType` object.
|
||||
func cellTopLabelText(for attributes: MessageIntermediateLayoutAttributes) -> NSAttributedString? {
|
||||
return messagesDataSource.cellTopLabelAttributedText(for: attributes.message, at: attributes.indexPath)
|
||||
}
|
||||
|
||||
// M
|
||||
|
||||
/// Returns the alignment of the cell's top label.
|
||||
///
|
||||
/// - Parameters:
|
||||
@@ -608,7 +534,7 @@ private extension MessagesCollectionViewFlowLayout {
|
||||
return messagesLayoutDelegate.cellTopLabelAlignment(for: attributes.message, at: attributes.indexPath, in: messagesCollectionView)
|
||||
}
|
||||
|
||||
// N
|
||||
// M
|
||||
|
||||
/// Returns the max available width for the cell's top label considering the specified layout information.
|
||||
///
|
||||
@@ -616,53 +542,55 @@ private extension MessagesCollectionViewFlowLayout {
|
||||
/// - attributes: The `MessageIntermediateLayoutAttributes` to consider when calculating the max width.
|
||||
func cellTopLabelMaxWidth(for attributes: MessageIntermediateLayoutAttributes) -> CGFloat {
|
||||
|
||||
guard attributes.cellTopLabelText != nil else { return 0 }
|
||||
|
||||
let labelHorizontal = attributes.cellTopLabelAlignment
|
||||
let avatarHorizontal = attributes.avatarHorizontal
|
||||
let avatarVertical = attributes.avatarVertical
|
||||
let labelHorizontal = attributes.topLabelAlignment
|
||||
let avatarHorizontal = attributes.avatarPosition.horizontal
|
||||
let avatarVertical = attributes.avatarPosition.vertical
|
||||
let avatarWidth = attributes.avatarSize.width
|
||||
|
||||
switch (labelHorizontal, avatarHorizontal, avatarVertical) {
|
||||
|
||||
case (.cellLeading, .cellTrailing, .cellTop), (.cellTrailing, .cellLeading, .cellTop):
|
||||
return itemWidth - avatarWidth - attributes.cellTopLabelHorizontalInsets
|
||||
|
||||
case (.cellLeading, _, _), (.cellTrailing, _, _):
|
||||
return itemWidth - attributes.cellTopLabelHorizontalInsets
|
||||
|
||||
case (.cellCenter, .cellLeading, .cellTop), (.cellCenter, .cellTrailing, .cellTop):
|
||||
return itemWidth - (avatarWidth * 2) - attributes.cellTopLabelHorizontalInsets
|
||||
|
||||
case (.cellCenter, .cellLeading, _), (.cellCenter, .cellTrailing, _):
|
||||
return itemWidth - attributes.cellTopLabelHorizontalInsets
|
||||
|
||||
case (.messageTrailing, .cellLeading, .cellTop), (.messageLeading, .cellTrailing, .cellTop):
|
||||
return attributes.messageContainerSize.width - attributes.cellTopLabelHorizontalInsets
|
||||
|
||||
case (.messageLeading, .cellLeading, _), (.messageTrailing, .cellTrailing, _):
|
||||
return itemWidth - avatarWidth - attributes.cellTopLabelHorizontalInsets
|
||||
|
||||
case (.messageLeading, .cellTrailing, _), (.messageTrailing, .cellLeading, _):
|
||||
return attributes.messageContainerSize.width + avatarWidth - attributes.cellTopLabelHorizontalInsets
|
||||
switch (labelHorizontal, avatarHorizontal) {
|
||||
|
||||
case (.cellLeading, _), (.cellTrailing, _):
|
||||
let width = itemWidth - attributes.topLabelHorizontalPadding
|
||||
return avatarVertical != .cellTop ? width : width - avatarWidth
|
||||
|
||||
case (.cellCenter, _):
|
||||
let width = itemWidth - attributes.topLabelHorizontalPadding
|
||||
return avatarVertical != .cellTop ? width : width - (avatarWidth * 2)
|
||||
|
||||
case (.messageTrailing, .cellLeading):
|
||||
let width = attributes.messageContainerSize.width + attributes.messageContainerPadding.left - attributes.topLabelHorizontalPadding
|
||||
return avatarVertical == .cellTop ? width : width + avatarWidth
|
||||
|
||||
case (.messageLeading, .cellTrailing):
|
||||
let width = attributes.messageContainerSize.width + attributes.messageContainerPadding.right - attributes.topLabelHorizontalPadding
|
||||
return avatarVertical == .cellTop ? width : width + avatarWidth
|
||||
|
||||
case (.messageLeading, .cellLeading):
|
||||
return itemWidth - avatarWidth - attributes.messageContainerPadding.left - attributes.topLabelHorizontalPadding
|
||||
|
||||
case (.messageTrailing, .cellTrailing):
|
||||
return itemWidth - avatarWidth - attributes.messageContainerPadding.right - attributes.topLabelHorizontalPadding
|
||||
|
||||
case (_, .natural):
|
||||
fatalError("AvatarPosition Horizontal.natural needs to be resolved.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// O
|
||||
// N
|
||||
|
||||
/// Returns the size of the cell's top label considering the specified layout information.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - attributes: The `MessageIntermediateLayoutAttributes` to consider when calculating label's size.
|
||||
func cellTopLabelSize(for attributes: MessageIntermediateLayoutAttributes) -> CGSize {
|
||||
guard let topLabelText = attributes.cellTopLabelText else { return .zero }
|
||||
|
||||
var topLabelSize = labelSize(for: topLabelText, considering: attributes.cellTopLabelMaxWidth)
|
||||
topLabelSize.width += attributes.cellTopLabelHorizontalInsets
|
||||
topLabelSize.height += attributes.cellTopLabelVerticalInsets
|
||||
let text = messagesDataSource.cellTopLabelAttributedText(for: attributes.message, at: attributes.indexPath)
|
||||
|
||||
return topLabelSize
|
||||
guard let topLabelText = text else { return .zero }
|
||||
|
||||
return labelSize(for: topLabelText, considering: attributes.topLabelMaxWidth)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -681,22 +609,22 @@ private extension MessagesCollectionViewFlowLayout {
|
||||
|
||||
var cellHeight: CGFloat = 0
|
||||
|
||||
switch attributes.avatarVertical {
|
||||
switch attributes.avatarPosition.vertical {
|
||||
case .cellTop:
|
||||
cellHeight += max(attributes.avatarSize.height, attributes.cellTopLabelSize.height)
|
||||
cellHeight += attributes.cellBottomLabelSize.height
|
||||
cellHeight += max(attributes.avatarSize.height, attributes.topLabelSize.height)
|
||||
cellHeight += attributes.bottomLabelSize.height
|
||||
cellHeight += attributes.messageContainerSize.height
|
||||
cellHeight += attributes.messageVerticalPadding
|
||||
case .cellBottom:
|
||||
cellHeight += max(attributes.avatarSize.height, attributes.cellBottomLabelSize.height)
|
||||
cellHeight += attributes.cellTopLabelSize.height
|
||||
cellHeight += max(attributes.avatarSize.height, attributes.bottomLabelSize.height)
|
||||
cellHeight += attributes.topLabelSize.height
|
||||
cellHeight += attributes.messageContainerSize.height
|
||||
cellHeight += attributes.messageVerticalPadding
|
||||
case .messageTop, .messageCenter, .messageBottom:
|
||||
cellHeight += max(attributes.avatarSize.height, attributes.messageContainerSize.height)
|
||||
cellHeight += attributes.messageVerticalPadding
|
||||
cellHeight += attributes.cellTopLabelSize.height
|
||||
cellHeight += attributes.cellBottomLabelSize.height
|
||||
cellHeight += attributes.topLabelSize.height
|
||||
cellHeight += attributes.bottomLabelSize.height
|
||||
}
|
||||
|
||||
return cellHeight
|
||||
@@ -704,137 +632,3 @@ private extension MessagesCollectionViewFlowLayout {
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Cell Content Origin Calculations [ Q - T ]
|
||||
|
||||
fileprivate extension MessagesCollectionViewFlowLayout {
|
||||
|
||||
// Q
|
||||
|
||||
/// Returns the origin point for the `AvatarView`'s frame.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - attributes: The `MessageIntermediateLayoutAttributes` to consider when calculating origin.
|
||||
private func avatarOrigin(for attributes: MessageIntermediateLayoutAttributes, and contentFrame: CGRect) -> CGPoint {
|
||||
|
||||
guard attributes.avatarSize != .zero else { return .zero }
|
||||
|
||||
var origin = CGPoint.zero
|
||||
|
||||
switch attributes.avatarHorizontal {
|
||||
case .cellLeading:
|
||||
origin.x = 0
|
||||
case .cellTrailing:
|
||||
origin.x = contentFrame.width - attributes.avatarSize.width
|
||||
}
|
||||
|
||||
switch attributes.avatarVertical {
|
||||
case .cellTop:
|
||||
origin.y = 0
|
||||
case .cellBottom:
|
||||
origin.y = contentFrame.height - attributes.avatarSize.height
|
||||
case .messageTop:
|
||||
origin.y = attributes.cellTopLabelSize.height + attributes.messageContainerPadding.top
|
||||
case .messageBottom:
|
||||
origin.y = contentFrame.height - attributes.avatarSize.height - attributes.cellBottomLabelSize.height - attributes.messageContainerPadding.bottom
|
||||
case .messageCenter:
|
||||
let messageMidY = attributes.messageContainerSize.height / 2
|
||||
let avatarMidY = attributes.avatarSize.height / 2
|
||||
origin.y = attributes.cellTopLabelSize.height + attributes.messageContainerPadding.top + messageMidY - avatarMidY
|
||||
}
|
||||
|
||||
return origin
|
||||
}
|
||||
|
||||
// R
|
||||
|
||||
/// Returns the origin point for the `MessageContainerView`'s frame.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - attributes: The `MessageIntermediateLayoutAttributes` to consider when calculating origin.
|
||||
private func messageContainerOrigin(for attributes: MessageIntermediateLayoutAttributes, and contentFrame: CGRect) -> CGPoint {
|
||||
|
||||
guard attributes.messageContainerSize != .zero else { return .zero }
|
||||
|
||||
var origin = CGPoint.zero
|
||||
|
||||
switch attributes.avatarHorizontal {
|
||||
case .cellLeading:
|
||||
origin.x = attributes.avatarSize.width + attributes.messageContainerPadding.left
|
||||
origin.y = attributes.cellTopLabelSize.height + attributes.messageContainerPadding.top
|
||||
case .cellTrailing:
|
||||
origin.x = contentFrame.width - attributes.avatarSize.width - attributes.messageContainerSize.width - attributes.messageContainerPadding.right
|
||||
origin.y = attributes.cellTopLabelSize.height + attributes.messageContainerPadding.top
|
||||
}
|
||||
|
||||
return origin
|
||||
|
||||
}
|
||||
|
||||
// S
|
||||
|
||||
/// Returns the origin point for the cell's bottom label's frame.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - attributes: The `MessageIntermediateLayoutAttributes` to consider when calculating origin.
|
||||
private func cellBottomLabelOrigin(for attributes: MessageIntermediateLayoutAttributes, and contentFrame: CGRect) -> CGPoint {
|
||||
|
||||
guard attributes.cellBottomLabelSize != .zero else { return .zero }
|
||||
|
||||
var origin = CGPoint(x: 0, y: attributes.cellTopLabelSize.height + attributes.messageContainerSize.height + attributes.messageVerticalPadding)
|
||||
|
||||
switch (attributes.cellBottomLabelAlignment, attributes.avatarHorizontal) {
|
||||
case (.cellLeading, _):
|
||||
origin.x = 0
|
||||
case (.cellCenter, _):
|
||||
origin.x = contentFrame.width / 2 - (attributes.cellBottomLabelSize.width / 2)
|
||||
case (.cellTrailing, _):
|
||||
origin.x = contentFrame.width - attributes.cellBottomLabelSize.width
|
||||
case (.messageLeading, .cellLeading):
|
||||
origin.x = attributes.avatarSize.width + attributes.messageContainerPadding.left
|
||||
case (.messageLeading, .cellTrailing):
|
||||
origin.x = contentFrame.width - attributes.avatarSize.width - attributes.messageContainerPadding.right - attributes.messageContainerSize.width
|
||||
case (.messageTrailing, .cellTrailing):
|
||||
origin.x = contentFrame.width - attributes.avatarSize.width - attributes.messageContainerPadding.right - attributes.cellBottomLabelSize.width
|
||||
case (.messageTrailing, .cellLeading):
|
||||
origin.x = attributes.avatarSize.width + attributes.messageContainerPadding.left + attributes.messageContainerSize.width - attributes.cellBottomLabelSize.width
|
||||
}
|
||||
|
||||
return origin
|
||||
|
||||
}
|
||||
|
||||
// T
|
||||
|
||||
/// Returns the origin point for the cell's top label's frame.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - attributes: The `MessageIntermediateLayoutAttributes` to consider when calculating origin.
|
||||
fileprivate func cellTopLabelOrigin(for attributes: MessageIntermediateLayoutAttributes, and contentFrame: CGRect) -> CGPoint {
|
||||
|
||||
guard attributes.cellTopLabelSize != .zero else { return .zero }
|
||||
|
||||
var origin = CGPoint.zero
|
||||
|
||||
switch (attributes.cellTopLabelAlignment, attributes.avatarHorizontal) {
|
||||
case (.cellLeading, _):
|
||||
origin.x = 0
|
||||
case (.cellCenter, _):
|
||||
origin.x = contentFrame.width / 2 - (attributes.cellTopLabelSize.width / 2)
|
||||
case (.cellTrailing, _):
|
||||
origin.x = contentFrame.width - attributes.cellTopLabelSize.width
|
||||
case (.messageLeading, .cellLeading):
|
||||
origin.x = attributes.avatarSize.width + attributes.messageContainerPadding.left
|
||||
case (.messageLeading, .cellTrailing):
|
||||
origin.x = contentFrame.width - attributes.avatarSize.width - attributes.messageContainerPadding.right - attributes.messageContainerSize.width
|
||||
case (.messageTrailing, .cellTrailing):
|
||||
origin.x = contentFrame.width - attributes.avatarSize.width - attributes.messageContainerPadding.right - attributes.cellTopLabelSize.width
|
||||
case (.messageTrailing, .cellLeading):
|
||||
origin.x = contentFrame.width - attributes.messageContainerPadding.right - attributes.cellTopLabelSize.width
|
||||
}
|
||||
|
||||
return origin
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -29,34 +29,26 @@ final class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttrib
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
var avatarHorizontalAlignment = AvatarHorizontalAlignment.cellLeading
|
||||
var avatarFrame: CGRect = .zero
|
||||
|
||||
var messageContainerFrame: CGRect = .zero
|
||||
var messagePadding: UIEdgeInsets = .zero
|
||||
var messageLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .body)
|
||||
var messageContainerFrame: CGRect = .zero
|
||||
var messageLabelInsets: UIEdgeInsets = .zero
|
||||
|
||||
var cellTopLabelFrame: CGRect = .zero
|
||||
var cellTopLabelInsets: UIEdgeInsets = .zero
|
||||
|
||||
var cellBottomLabelFrame: CGRect = .zero
|
||||
var cellBottomLabelInsets: UIEdgeInsets = .zero
|
||||
var topLabelFrame: CGRect = .zero
|
||||
var bottomLabelFrame: CGRect = .zero
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
override func copy(with zone: NSZone? = nil) -> Any {
|
||||
// swiftlint:disable force_cast
|
||||
let copy = super.copy(with: zone) as! MessagesCollectionViewLayoutAttributes
|
||||
copy.avatarHorizontalAlignment = avatarHorizontalAlignment
|
||||
copy.avatarFrame = avatarFrame
|
||||
copy.messageContainerFrame = messageContainerFrame
|
||||
copy.messageLabelFont = messageLabelFont
|
||||
copy.messageLabelInsets = messageLabelInsets
|
||||
copy.cellTopLabelFrame = cellTopLabelFrame
|
||||
copy.cellTopLabelInsets = cellTopLabelInsets
|
||||
copy.cellBottomLabelFrame = cellBottomLabelFrame
|
||||
copy.cellBottomLabelInsets = cellBottomLabelInsets
|
||||
copy.topLabelFrame = topLabelFrame
|
||||
copy.bottomLabelFrame = bottomLabelFrame
|
||||
return copy
|
||||
// swiftlint:enable force_cast
|
||||
}
|
||||
|
||||
@@ -35,13 +35,13 @@ public struct Avatar {
|
||||
/// The placeholder initials to be used in the case where no image is provided.
|
||||
///
|
||||
/// The default value of this property is "?".
|
||||
public var initals: String = "?"
|
||||
public var initials: String = "?"
|
||||
|
||||
// MARK: - Initializer
|
||||
|
||||
public init(image: UIImage? = nil, initals: String = "?") {
|
||||
public init(image: UIImage? = nil, initials: String = "?") {
|
||||
self.image = image
|
||||
self.initals = initals
|
||||
self.initials = initials
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 MessageKit
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
/// An enum representing the verical alignment for an `AvatarView`.
|
||||
public enum AvatarAlignment {
|
||||
|
||||
/// Aligns the `AvatarView`'s top edge to the cell's top edge.
|
||||
case cellTop
|
||||
|
||||
/// Aligns the `AvatarView`'s bottom edge to the cell's bottom edge.
|
||||
case cellBottom
|
||||
|
||||
/// Aligns the `AvatarView`'s top edge to the `MessageContainerView`'s top edge.
|
||||
case messageTop
|
||||
|
||||
/// Aligns the `AvatarView`'s bottom edge to the `MessageContainerView`s bottom edge.
|
||||
case messageBottom
|
||||
|
||||
/// Aligns the `AvatarView` center to the `MessageContainerView` center.
|
||||
case messageCenter
|
||||
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 MessageKit
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - AvatarVerticalAlignment
|
||||
|
||||
/// An enum representing the horizontal alignment of an `AvatarView`.
|
||||
internal enum AvatarHorizontalAlignment {
|
||||
|
||||
/// Positions the `AvatarView` on the side closest to the cell's leading edge.
|
||||
case cellLeading
|
||||
|
||||
/// Positions the `AvatarView` on the side closest to the cell's trailing edge.
|
||||
case cellTrailing
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 MessageKit
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Used to determine the `Horizontal` and `Vertical` position of
|
||||
// an `AvatarView` in a `MessagesCollectionViewCell`.
|
||||
public struct AvatarPosition {
|
||||
|
||||
/// An enum representing the horizontal alignment of an `AvatarView`.
|
||||
public enum Horizontal {
|
||||
|
||||
/// Positions the `AvatarView` on the side closest to the cell's leading edge.
|
||||
case cellLeading
|
||||
|
||||
/// Positions the `AvatarView` on the side closest to the cell's trailing edge.
|
||||
case cellTrailing
|
||||
|
||||
/// Positions the `AvatarView` based on whether the message is from the current Sender.
|
||||
/// The cell is positioned `.cellTrailling` if `isFromCurrentSender` is true
|
||||
/// and `.cellLeading` if false.
|
||||
case natural
|
||||
}
|
||||
|
||||
/// An enum representing the verical alignment for an `AvatarView`.
|
||||
public enum Vertical {
|
||||
|
||||
/// Aligns the `AvatarView`'s top edge to the cell's top edge.
|
||||
case cellTop
|
||||
|
||||
/// Aligns the `AvatarView`'s bottom edge to the cell's bottom edge.
|
||||
case cellBottom
|
||||
|
||||
/// Aligns the `AvatarView`'s top edge to the `MessageContainerView`'s top edge.
|
||||
case messageTop
|
||||
|
||||
/// Aligns the `AvatarView`'s bottom edge to the `MessageContainerView`s bottom edge.
|
||||
case messageBottom
|
||||
|
||||
/// Aligns the `AvatarView` center to the `MessageContainerView` center.
|
||||
case messageCenter
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// The vertical position
|
||||
public var vertical: Vertical
|
||||
|
||||
// The horizontal position
|
||||
public var horizontal: Horizontal
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
public init(horizontal: Horizontal, vertical: Vertical) {
|
||||
self.horizontal = horizontal
|
||||
self.vertical = vertical
|
||||
}
|
||||
|
||||
public init(vertical: Vertical) {
|
||||
self.init(horizontal: .natural, vertical: vertical)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 MessageKit
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import MapKit
|
||||
|
||||
/// Conform to this protocol to customize location messages's style
|
||||
public protocol LocationMessageDisplayDelegate: MessagesDisplayDelegate {
|
||||
|
||||
/// Ask the delegate for a LocationMessageSnapshotOptions instance to customize the MapView on the given 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
|
||||
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
|
||||
///
|
||||
/// - 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.
|
||||
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
|
||||
///
|
||||
/// - 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.
|
||||
func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)?
|
||||
|
||||
}
|
||||
|
||||
public extension LocationMessageDisplayDelegate {
|
||||
|
||||
func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions {
|
||||
return LocationMessageSnapshotOptions()
|
||||
}
|
||||
|
||||
func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? {
|
||||
return MKPinAnnotationView(annotation: nil, reuseIdentifier: nil)
|
||||
}
|
||||
|
||||
func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? {
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 MessageKit
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A protocol used by the `MessagesCollectionViewFlowLayout` object to determine
|
||||
/// the size and layout of a `LocationMessageCell` and its contents.
|
||||
public protocol LocationMessageLayoutDelegate: MessagesLayoutDelegate {
|
||||
|
||||
/// 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
|
||||
|
||||
}
|
||||
|
||||
public extension LocationMessageLayoutDelegate {
|
||||
|
||||
func widthForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
|
||||
return maxWidth
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 MessageKit
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
import AVFoundation
|
||||
|
||||
/// A protocol used by the `MessagesCollectionViewFlowLayout` object to determine
|
||||
/// the size and layout of a `MediaMessageCell`s and its contents.
|
||||
public protocol MediaMessageLayoutDelegate: MessagesLayoutDelegate {
|
||||
|
||||
/// 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 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
|
||||
|
||||
}
|
||||
|
||||
public extension MediaMessageLayoutDelegate {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,7 +25,7 @@
|
||||
import Foundation
|
||||
|
||||
/// A protocol used by `MessageCollectionViewCell` subclasses to detect taps in the cell's contents.
|
||||
public protocol MessageCellDelegate: class, MessageLabelDelegate {
|
||||
public protocol MessageCellDelegate: MessageLabelDelegate {
|
||||
|
||||
/// Triggered when a touch occurs in the `MessageContainerView`.
|
||||
///
|
||||
|
||||
@@ -23,10 +23,9 @@
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
/// MessageInputBarDelegate is a protocol that can recieve notifications from the MessageInputBar
|
||||
public protocol MessageInputBarDelegate: class {
|
||||
public protocol MessageInputBarDelegate: AnyObject {
|
||||
|
||||
/// Called when the default send button has been selected
|
||||
///
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol MessageLabelDelegate: class {
|
||||
public protocol MessageLabelDelegate: AnyObject {
|
||||
|
||||
func didSelectAddress(_ addressComponents: [String: String])
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
public protocol MessagesDataSource: class {
|
||||
public protocol MessagesDataSource: AnyObject {
|
||||
|
||||
/// The `Sender` of new messages in the `MessagesCollectionView`.
|
||||
func currentSender() -> Sender
|
||||
|
||||
@@ -23,51 +23,12 @@
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A protocol used by the `MessagesViewController` to customize the appearance of a `TextMessageCell`.
|
||||
public protocol TextMessageDisplayDelegate: class {
|
||||
|
||||
/// 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.
|
||||
/// - 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: UIColor.white
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - message: A `MessageType` with a `MessageData` case of `.text` or `.attributedText` to which the detectors 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 all available detector types.
|
||||
func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType]
|
||||
|
||||
}
|
||||
|
||||
public extension TextMessageDisplayDelegate {
|
||||
|
||||
func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else { return .darkText }
|
||||
return dataSource.isFromCurrentSender(message: message) ? .white : .darkText
|
||||
}
|
||||
|
||||
func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] {
|
||||
return [.url, .address, .phoneNumber, .date]
|
||||
}
|
||||
|
||||
}
|
||||
import MapKit
|
||||
|
||||
/// A protocol used by the `MessagesViewController` to customize the appearance of a `MessagesCollectionViewCell`.
|
||||
public protocol MessagesDisplayDelegate: class {
|
||||
public protocol MessagesDisplayDelegate: AnyObject {
|
||||
|
||||
// MARK: - All Messages
|
||||
|
||||
/// Specifies the `MessageStyle` to be used for a `MessageContainerView`.
|
||||
///
|
||||
@@ -123,10 +84,74 @@ public protocol MessagesDisplayDelegate: class {
|
||||
/// The default value returned by this method is a `MessageFooterView`.
|
||||
func messageFooterView(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageFooterView
|
||||
|
||||
// MARK: - Text Messages
|
||||
|
||||
/// 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.
|
||||
/// - 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: UIColor.white
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - message: A `MessageType` with a `MessageData` case of `.text` or `.attributedText` to which the detectors 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 all available detector types.
|
||||
func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType]
|
||||
|
||||
// MARK: - Location Messages
|
||||
|
||||
/// Ask the delegate for a LocationMessageSnapshotOptions instance to customize the MapView on the given 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
|
||||
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
|
||||
///
|
||||
/// - 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.
|
||||
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
|
||||
///
|
||||
/// - 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.
|
||||
func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)?
|
||||
|
||||
|
||||
}
|
||||
|
||||
public extension MessagesDisplayDelegate {
|
||||
|
||||
// MARK: - All Messages Defaults
|
||||
|
||||
func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle {
|
||||
return .bubble
|
||||
}
|
||||
@@ -162,4 +187,29 @@ public extension MessagesDisplayDelegate {
|
||||
return messagesCollectionView.dequeueReusableFooterView(MessageFooterView.self, for: indexPath)
|
||||
}
|
||||
|
||||
// MARK: - Text Messages Defaults
|
||||
|
||||
func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor {
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else { return .darkText }
|
||||
return dataSource.isFromCurrentSender(message: message) ? .white : .darkText
|
||||
}
|
||||
|
||||
func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] {
|
||||
return []
|
||||
}
|
||||
|
||||
// MARK: - Location Messages Defaults
|
||||
|
||||
func snapshotOptionsForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LocationMessageSnapshotOptions {
|
||||
return LocationMessageSnapshotOptions()
|
||||
}
|
||||
|
||||
func annotationViewForLocation(message: MessageType, at indexPath: IndexPath, in messageCollectionView: MessagesCollectionView) -> MKAnnotationView? {
|
||||
return MKPinAnnotationView(annotation: nil, reuseIdentifier: nil)
|
||||
}
|
||||
|
||||
func animationBlockForLocation(message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> ((UIImageView) -> Void)? {
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,24 +23,13 @@
|
||||
*/
|
||||
|
||||
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: class {
|
||||
public protocol MessagesLayoutDelegate: AnyObject {
|
||||
|
||||
/// Specifies the insets for the text rect of the `MessageLabel` in a `TextMessageCell`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - message: A `MessageType` with a `MessageData` case of `.text` or `.attributedText` to which these insets 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`:
|
||||
///
|
||||
/// 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
|
||||
// MARK: - All Messages
|
||||
|
||||
/// Specifies the padding around the `MessageContainerView` in a `MessageCollectionViewCell`.
|
||||
///
|
||||
@@ -55,16 +44,17 @@ public protocol MessagesLayoutDelegate: class {
|
||||
///
|
||||
/// 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 alignment for the `AvatarView` in a `MessageCollectionViewCell`.
|
||||
|
||||
/// 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 `AvatarAlignment.cellBottom`.
|
||||
func avatarAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarAlignment
|
||||
/// 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.
|
||||
///
|
||||
@@ -124,18 +114,83 @@ public protocol MessagesLayoutDelegate: class {
|
||||
/// 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
|
||||
|
||||
// MARK: - Text Messages
|
||||
|
||||
/// Specifies the insets for the text rect of the `MessageLabel` in a `TextMessageCell`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - message: A `MessageType` with a `MessageData` case of `.text` or `.attributedText` to which these insets 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`:
|
||||
///
|
||||
/// 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
|
||||
|
||||
// MARK: - Media 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 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
|
||||
|
||||
}
|
||||
|
||||
public extension MessagesLayoutDelegate {
|
||||
|
||||
func messageLabelInset(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets {
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else { return .zero }
|
||||
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)
|
||||
}
|
||||
}
|
||||
// MARK: - All Messages Defaults
|
||||
|
||||
func messagePadding(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets {
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else { return .zero }
|
||||
@@ -146,10 +201,6 @@ public extension MessagesLayoutDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func avatarAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarAlignment {
|
||||
return .cellBottom
|
||||
}
|
||||
|
||||
func cellTopLabelAlignment(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> LabelAlignment {
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else { return .cellCenter(.zero) }
|
||||
return dataSource.isFromCurrentSender(message: message) ? .messageTrailing(.zero) : .messageLeading(.zero)
|
||||
@@ -163,6 +214,10 @@ public extension MessagesLayoutDelegate {
|
||||
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 { return .zero }
|
||||
@@ -174,4 +229,43 @@ public extension MessagesLayoutDelegate {
|
||||
return .zero
|
||||
}
|
||||
|
||||
func shouldCacheLayoutAttributes(for message: MessageType) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: - Text Messages Defaults
|
||||
|
||||
func messageLabelInset(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets {
|
||||
guard let dataSource = messagesCollectionView.messagesDataSource else { return .zero }
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
/*
|
||||
MIT License
|
||||
|
||||
|
||||
Copyright (c) 2017 MessageKit
|
||||
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
@@ -24,59 +24,66 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - MessagesViewController
|
||||
// MARK: - Deprecated Protocols
|
||||
|
||||
public extension MessagesViewController {
|
||||
@available(*, deprecated: 0.11.0, message: "LocationMessageDisplayDelegate has been deprecated in favor of MessagesDisplayDelegate")
|
||||
typealias LocationMessageDisplayDelegate = MessagesDisplayDelegate
|
||||
|
||||
@available(*, deprecated: 0.10.0, message: "Removed in MessageKit 0.10.0. Please use the messagesCollectionView.contentInsets.top property.")
|
||||
public var additionalTopContentInset: CGFloat {
|
||||
get {
|
||||
return messagesCollectionView.contentInset.top
|
||||
}
|
||||
set {
|
||||
messagesCollectionView.contentInset.top = newValue
|
||||
}
|
||||
@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.")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MessagesCollectionView
|
||||
|
||||
public extension MessagesCollectionView {
|
||||
|
||||
@available(*, deprecated: 0.9.0, message: "Removed in MessageKit 0.9.0. Please use dequeueReusableHeaderView")
|
||||
public func dequeueMessageHeaderView(withReuseIdentifier identifier: String = "MessageHeaderView", for indexPath: IndexPath) -> MessageHeaderView {
|
||||
return dequeueReusableHeaderView(MessageHeaderView.self, for: indexPath)
|
||||
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")
|
||||
}
|
||||
|
||||
@available(*, deprecated: 0.9.0, message: "Removed in MessageKit 0.9.0. Please use dequeueReusableFooterView")
|
||||
public func dequeueMessageFooterView(withReuseIdentifier identifier: String = "MessageFooterView", for indexPath: IndexPath) -> MessageFooterView {
|
||||
return dequeueReusableFooterView(MessageFooterView.self, for: indexPath)
|
||||
|
||||
/// 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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - MessagesCollectionViewFlowLayout
|
||||
|
||||
public extension MessagesCollectionViewFlowLayout {
|
||||
|
||||
@available(*, deprecated: 0.9.0, message: "Removed in MessageKit 0.9.0. Please use messageLabelInsets(for:indexPath:messagesCollectionView)")
|
||||
public var messageLabelInsets: UIEdgeInsets {
|
||||
return UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 14)
|
||||
}
|
||||
|
||||
@available(*, deprecated: 0.9.0, message: "Removed in MessageKit 0.9.0. Please use associated value of LabelAlignment")
|
||||
public var cellTopLabelInsets: UIEdgeInsets {
|
||||
return .zero
|
||||
}
|
||||
@available(*, deprecated: 0.9.0, message: "Removed in MessageKit 0.9.0. Please use associated value of LabelAlignment")
|
||||
public var cellBottomLabelInsets: UIEdgeInsets {
|
||||
return .zero
|
||||
}
|
||||
|
||||
@available(*, deprecated: 0.9.0, message: "Removed in MessageKit 0.9.0. Please use messagePadding(for:at:in) method of MessagesLayoutDelegate")
|
||||
public var messageToViewEdgePadding: CGFloat {
|
||||
return 30
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ open class AvatarView: UIView {
|
||||
self.init(frame: .zero)
|
||||
}
|
||||
|
||||
private func getImageFrom(initals: String) -> UIImage {
|
||||
private func getImageFrom(initials: String) -> UIImage {
|
||||
let width = frame.width
|
||||
let height = frame.height
|
||||
if width == 0 || height == 0 {return UIImage()}
|
||||
@@ -96,8 +96,8 @@ open class AvatarView: UIView {
|
||||
//// Text Drawing
|
||||
let textRect = calculateTextRect(outerViewWidth: width, outerViewHeight: height)
|
||||
if adjustsFontSizeToFitWidth,
|
||||
initals.width(considering: textRect.height, and: font) > textRect.width {
|
||||
let newFontSize = calculateFontSize(text: initals, font: font, width: textRect.width, height: textRect.height)
|
||||
initials.width(considering: textRect.height, and: font) > textRect.width {
|
||||
let newFontSize = calculateFontSize(text: initials, font: font, width: textRect.width, height: textRect.height)
|
||||
font = placeholderFont.withSize(newFontSize)
|
||||
}
|
||||
|
||||
@@ -105,10 +105,10 @@ open class AvatarView: UIView {
|
||||
textStyle.alignment = .center
|
||||
let textFontAttributes: [NSAttributedStringKey: Any] = [NSAttributedStringKey.font: font, NSAttributedStringKey.foregroundColor: placeholderTextColor, NSAttributedStringKey.paragraphStyle: textStyle]
|
||||
|
||||
let textTextHeight: CGFloat = initals.boundingRect(with: CGSize(width: textRect.width, height: CGFloat.infinity), options: .usesLineFragmentOrigin, attributes: textFontAttributes, context: nil).height
|
||||
let textTextHeight: CGFloat = initials.boundingRect(with: CGSize(width: textRect.width, height: CGFloat.infinity), options: .usesLineFragmentOrigin, attributes: textFontAttributes, context: nil).height
|
||||
context.saveGState()
|
||||
context.clip(to: textRect)
|
||||
initals.draw(in: CGRect(x: textRect.minX, y: textRect.minY + (textRect.height - textTextHeight) / 2, width: textRect.width, height: textTextHeight), withAttributes: textFontAttributes)
|
||||
initials.draw(in: CGRect(x: textRect.minX, y: textRect.minY + (textRect.height - textTextHeight) / 2, width: textRect.width, height: textTextHeight), withAttributes: textFontAttributes)
|
||||
context.restoreGState()
|
||||
guard let renderedImage = UIGraphicsGetImageFromCurrentImageContext() else { assertionFailure("Could not create image from context"); return UIImage()}
|
||||
return renderedImage
|
||||
@@ -163,14 +163,14 @@ open class AvatarView: UIView {
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.frame = frame
|
||||
addSubview(imageView)
|
||||
imageView.image = avatar.image ?? getImageFrom(initals: avatar.initals)
|
||||
imageView.image = avatar.image ?? getImageFrom(initials: avatar.initials)
|
||||
setCorner(radius: nil)
|
||||
}
|
||||
|
||||
// MARK: - Open setters
|
||||
|
||||
open func set(avatar: Avatar) {
|
||||
imageView.image = avatar.image ?? getImageFrom(initals: avatar.initals)
|
||||
imageView.image = avatar.image ?? getImageFrom(initials: avatar.initials)
|
||||
}
|
||||
|
||||
open func setCorner(radius: CGFloat?) {
|
||||
|
||||
@@ -37,7 +37,7 @@ open class LocationMessageCell: MessageCollectionViewCell<UIImageView> {
|
||||
|
||||
switch message.data {
|
||||
case .location(let location):
|
||||
guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate as? LocationMessageDisplayDelegate else { return }
|
||||
guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { return }
|
||||
let options = displayDelegate.snapshotOptionsForLocation(message: message, at: indexPath, in: messagesCollectionView)
|
||||
let annotationView = displayDelegate.annotationViewForLocation(message: message, at: indexPath, in: messagesCollectionView)
|
||||
let animationBlock = displayDelegate.animationBlockForLocation(message: message, at: indexPath, in: messagesCollectionView)
|
||||
|
||||
@@ -38,10 +38,10 @@ open class MessageCollectionViewCell<ContentView: UIView>: UICollectionViewCell,
|
||||
|
||||
open var avatarView: AvatarView = AvatarView()
|
||||
|
||||
open var cellTopLabel: MessageLabel = {
|
||||
let topLabel = MessageLabel()
|
||||
topLabel.enabledDetectors = []
|
||||
return topLabel
|
||||
open var cellTopLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
open var messageContentView: ContentView = {
|
||||
@@ -51,10 +51,10 @@ open class MessageCollectionViewCell<ContentView: UIView>: UICollectionViewCell,
|
||||
return contentView
|
||||
}()
|
||||
|
||||
open var cellBottomLabel: MessageLabel = {
|
||||
let bottomLabel = MessageLabel()
|
||||
bottomLabel.enabledDetectors = []
|
||||
return bottomLabel
|
||||
open var cellBottomLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
open weak var delegate: MessageCellDelegate?
|
||||
@@ -77,13 +77,11 @@ open class MessageCollectionViewCell<ContentView: UIView>: UICollectionViewCell,
|
||||
// MARK: - Methods
|
||||
|
||||
internal func setupSubviews() {
|
||||
|
||||
contentView.addSubview(cellTopLabel)
|
||||
contentView.addSubview(messageContainerView)
|
||||
messageContainerView.addSubview(messageContentView)
|
||||
contentView.addSubview(avatarView)
|
||||
contentView.addSubview(cellBottomLabel)
|
||||
|
||||
}
|
||||
|
||||
open override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
|
||||
@@ -95,12 +93,8 @@ open class MessageCollectionViewCell<ContentView: UIView>: UICollectionViewCell,
|
||||
|
||||
messageContainerView.frame = attributes.messageContainerFrame
|
||||
messageContentView.frame = messageContainerView.bounds
|
||||
|
||||
cellTopLabel.frame = attributes.cellTopLabelFrame
|
||||
cellTopLabel.textInsets = attributes.cellTopLabelInsets
|
||||
|
||||
cellBottomLabel.frame = attributes.cellBottomLabelFrame
|
||||
cellBottomLabel.textInsets = attributes.cellBottomLabelInsets
|
||||
cellTopLabel.frame = attributes.topLabelFrame
|
||||
cellBottomLabel.frame = attributes.bottomLabelFrame
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -47,8 +47,12 @@ open class TextMessageCell: MessageCollectionViewCell<MessageLabel> {
|
||||
super.apply(layoutAttributes)
|
||||
|
||||
guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return }
|
||||
messageContentView.textInsets = attributes.messageLabelInsets
|
||||
messageContentView.font = attributes.messageLabelFont
|
||||
|
||||
messageContentView.configure {
|
||||
messageContentView.textInsets = attributes.messageLabelInsets
|
||||
messageContentView.font = attributes.messageLabelFont
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
open override func prepareForReuse() {
|
||||
@@ -60,11 +64,14 @@ open class TextMessageCell: MessageCollectionViewCell<MessageLabel> {
|
||||
open override func configure(with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView) {
|
||||
super.configure(with: message, at: indexPath, and: messagesCollectionView)
|
||||
|
||||
if let displayDelegate = messagesCollectionView.messagesDisplayDelegate as? TextMessageDisplayDelegate {
|
||||
if let displayDelegate = messagesCollectionView.messagesDisplayDelegate {
|
||||
let textColor = displayDelegate.textColor(for: message, at: indexPath, in: messagesCollectionView)
|
||||
let detectors = displayDelegate.enabledDetectors(for: message, at: indexPath, in: messagesCollectionView)
|
||||
messageContentView.textColor = textColor
|
||||
messageContentView.enabledDetectors = detectors
|
||||
|
||||
messageContentView.configure {
|
||||
messageContentView.textColor = textColor
|
||||
messageContentView.enabledDetectors = detectors
|
||||
}
|
||||
}
|
||||
|
||||
switch message.data {
|
||||
|
||||
@@ -39,8 +39,9 @@ open class InputStackView: UIStackView {
|
||||
/// - left: Left Stack View
|
||||
/// - right: Bottom Stack View
|
||||
/// - bottom: Left Stack View
|
||||
/// - top: Top Stack View
|
||||
public enum Position {
|
||||
case left, right, bottom
|
||||
case left, right, bottom, top
|
||||
}
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
@@ -159,10 +159,13 @@ open class InputTextView: UITextView {
|
||||
top: placeholderLabel.topAnchor.constraint(equalTo: topAnchor, constant: placeholderLabelInsets.top),
|
||||
bottom: placeholderLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -placeholderLabelInsets.bottom),
|
||||
left: placeholderLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: placeholderLabelInsets.left),
|
||||
right: placeholderLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: (placeholderLabelInsets.left + placeholderLabelInsets.right)),
|
||||
right: placeholderLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -placeholderLabelInsets.right),
|
||||
centerX: placeholderLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
centerY: placeholderLabel.centerYAnchor.constraint(equalTo: centerYAnchor)
|
||||
).activate()
|
||||
)
|
||||
placeholderLabelConstraintSet?.centerX?.priority = .defaultLow
|
||||
placeholderLabelConstraintSet?.centerY?.priority = .defaultLow
|
||||
placeholderLabelConstraintSet?.activate()
|
||||
}
|
||||
|
||||
/// Updates the placeholderLabels constraint constants to match the placeholderLabelInsets
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
/// A powerful InputAccessoryView ideal for messaging applications
|
||||
open class MessageInputBar: UIView {
|
||||
|
||||
public enum UIStackViewPosition {
|
||||
@@ -32,28 +33,60 @@ open class MessageInputBar: UIView {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
/// A delegate to broadcast notifications from the MessageInputBar
|
||||
open weak var delegate: MessageInputBarDelegate?
|
||||
|
||||
/// A background view that adds a blur effect. Shown when 'isTransparent' is set to TRUE. Hidden by default.
|
||||
open let blurView: UIView = {
|
||||
let blurEffect = UIBlurEffect(style: .extraLight)
|
||||
let view = UIVisualEffectView(effect: blurEffect)
|
||||
/// The background UIView anchored to the bottom, left, and right of the MessageInputBar
|
||||
/// with a top anchor equal to the bottom of the top InputStackView
|
||||
open var backgroundView: UIView = {
|
||||
let view = UIView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.isHidden = true
|
||||
view.backgroundColor = .inputBarGray
|
||||
return view
|
||||
}()
|
||||
|
||||
/// When set to true, the blurView in the background is shown and the backgroundColor is set to .clear. Default is FALSE
|
||||
/**
|
||||
A UIVisualEffectView that adds a blur effect to make the view appear transparent.
|
||||
|
||||
## Important Notes ##
|
||||
1. The blurView is initially not added to the backgroundView to improve performance when not needed. When `isTranslucent` is set to TRUE for the first time the blurView is added and anchored to the `backgroundView`s edge anchors
|
||||
*/
|
||||
open var blurView: UIVisualEffectView = {
|
||||
let blurEffect = UIBlurEffect(style: .light)
|
||||
let view = UIVisualEffectView(effect: blurEffect)
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
/// Determines if the MessageInputBar should have a translucent effect
|
||||
open var isTranslucent: Bool = false {
|
||||
didSet {
|
||||
if isTranslucent && blurView.superview == nil {
|
||||
backgroundView.addSubview(blurView)
|
||||
blurView.fillSuperview()
|
||||
}
|
||||
blurView.isHidden = !isTranslucent
|
||||
backgroundColor = isTranslucent ? .clear : .white
|
||||
let color: UIColor = backgroundView.backgroundColor ?? .inputBarGray
|
||||
backgroundView.backgroundColor = isTranslucent ? color.withAlphaComponent(0.75) : color.withAlphaComponent(1.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A boarder line anchored to the top of the view
|
||||
/// A SeparatorLine that is initially placed in the topStackView
|
||||
open let separatorLine = SeparatorLine()
|
||||
|
||||
/**
|
||||
The InputStackView at the InputStackView.top position
|
||||
|
||||
## Important Notes ##
|
||||
1. It's axis is initially set to .vertical
|
||||
2. It's alignment is initially set to .fill
|
||||
*/
|
||||
open let topStackView: InputStackView = {
|
||||
let stackView = InputStackView(axis: .vertical, spacing: 0)
|
||||
stackView.alignment = .fill
|
||||
return stackView
|
||||
}()
|
||||
|
||||
/**
|
||||
The InputStackView at the InputStackView.left position
|
||||
|
||||
@@ -78,24 +111,16 @@ open class MessageInputBar: UIView {
|
||||
2. It's spacing is initially set to 15
|
||||
*/
|
||||
open let bottomStackView = InputStackView(axis: .horizontal, spacing: 15)
|
||||
|
||||
open lazy var inputTextView: InputTextView = { [weak self] in
|
||||
|
||||
/// The InputTextView a user can input a message in
|
||||
open lazy var inputTextView: InputTextView = {
|
||||
let textView = InputTextView()
|
||||
textView.translatesAutoresizingMaskIntoConstraints = false
|
||||
textView.messageInputBar = self
|
||||
return textView
|
||||
}()
|
||||
|
||||
/// The padding around the textView that separates it from the stackViews
|
||||
open var textViewPadding: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8) {
|
||||
didSet {
|
||||
textViewLayoutSet?.bottom?.constant = -textViewPadding.bottom
|
||||
textViewLayoutSet?.left?.constant = textViewPadding.left
|
||||
textViewLayoutSet?.right?.constant = -textViewPadding.right
|
||||
bottomStackViewLayoutSet?.top?.constant = textViewPadding.bottom
|
||||
}
|
||||
}
|
||||
|
||||
/// A InputBarButtonItem used as the send button and initially placed in the rightStackView
|
||||
open var sendButton: InputBarButtonItem = {
|
||||
return InputBarButtonItem()
|
||||
.configure {
|
||||
@@ -108,46 +133,82 @@ open class MessageInputBar: UIView {
|
||||
}
|
||||
}()
|
||||
|
||||
/// The anchor contants used by the UIStackViews and InputTextView to create padding within the InputBarAccessoryView
|
||||
/**
|
||||
The anchor contants used by the InputStackView's and InputTextView to create padding
|
||||
within the MessageInputBar
|
||||
|
||||
## Important Notes ##
|
||||
|
||||
````
|
||||
V:|...-[InputStackView.top]-(padding.top)-[InputTextView]-(textViewPadding.bottom)-[InputStackView.bottom]-(padding.bottom)-|
|
||||
|
||||
H:|-(padding.left)-[InputStackView.left(leftStackViewWidthConstant)]-(textViewPadding.left)-[InputTextView]-(textViewPadding.right)-[InputStackView.right(rightStackViewWidthConstant)]-(padding.right)-|
|
||||
````
|
||||
|
||||
*/
|
||||
open var padding: UIEdgeInsets = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12) {
|
||||
didSet {
|
||||
updateViewContraints()
|
||||
updatePadding()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
The anchor constants used by the top InputStackView
|
||||
|
||||
## Important Notes ##
|
||||
1. The topStackViewPadding.bottom property is not used. Use padding.top
|
||||
|
||||
````
|
||||
V:|-(topStackViewPadding.top)-[InputStackView.top]-(padding.top)-[InputTextView]-...|
|
||||
|
||||
H:|-(topStackViewPadding.left)-[InputStackView.top]-(topStackViewPadding.right)-|
|
||||
````
|
||||
|
||||
*/
|
||||
open var topStackViewPadding: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) {
|
||||
didSet {
|
||||
updateTopStackViewPadding()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
The anchor constants used by the InputStackView
|
||||
|
||||
## Important Notes ##
|
||||
1. The inputTextViewPadding.top property is not used. Use padding.top
|
||||
|
||||
````
|
||||
V:|...-(padding.top)-[InputTextView]-(inputTextViewPadding.bottom)-[InputStackView.bottom]-...|
|
||||
|
||||
H:|...-[InputStackView.left]-(inputTextViewPadding.left)-[InputTextView]-(inputTextViewPadding.left)-[InputStackView.left.right]-...|
|
||||
````
|
||||
|
||||
*/
|
||||
open var textViewPadding: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8) {
|
||||
didSet {
|
||||
updateTextViewPadding()
|
||||
}
|
||||
}
|
||||
|
||||
open override var intrinsicContentSize: CGSize {
|
||||
let maxSize = CGSize(width: inputTextView.bounds.width, height: .greatestFiniteMagnitude)
|
||||
let sizeToFit = inputTextView.sizeThatFits(maxSize)
|
||||
var heightToFit = sizeToFit.height.rounded() + padding.top + padding.bottom
|
||||
|
||||
if heightToFit >= maxHeight {
|
||||
if !isOverMaxHeight {
|
||||
textViewHeightAnchor?.isActive = true
|
||||
inputTextView.isScrollEnabled = true
|
||||
isOverMaxHeight = true
|
||||
}
|
||||
heightToFit = maxHeight
|
||||
} else {
|
||||
if isOverMaxHeight {
|
||||
textViewHeightAnchor?.isActive = false
|
||||
inputTextView.isScrollEnabled = false
|
||||
isOverMaxHeight = false
|
||||
}
|
||||
}
|
||||
|
||||
let size = CGSize(width: bounds.width, height: heightToFit)
|
||||
|
||||
let size = calculateIntrinsicContentSize()
|
||||
if previousIntrinsicContentSize != size {
|
||||
delegate?.messageInputBar(self, didChangeIntrinsicContentTo: size)
|
||||
previousIntrinsicContentSize = size
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
private(set) var isOverMaxHeight = false
|
||||
/// The intrinsicContentSize can change a lot so the delegate method
|
||||
/// `inputBar(self, didChangeIntrinsicContentTo: size)` only needs to be called
|
||||
/// when it's different
|
||||
public private(set) var previousIntrinsicContentSize: CGSize?
|
||||
|
||||
/// The maximum intrinsicContentSize height. When reached the delegate 'didChangeIntrinsicContentTo' will be called.
|
||||
/// A boolean that indicates if the maxTextViewHeight has been met. Keeping track of this
|
||||
/// improves the performance
|
||||
public private(set) var isOverMaxTextViewHeight = false
|
||||
|
||||
/// The maximum height that the InputTextView can reach
|
||||
open var maxHeight: CGFloat = UIScreen.main.bounds.height / 3 {
|
||||
didSet {
|
||||
textViewHeightAnchor?.constant = maxHeight
|
||||
@@ -155,28 +216,35 @@ open class MessageInputBar: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
private var inputBarMinusTextViewHeight: CGFloat {
|
||||
return topStackViewPadding.top + padding.top + textViewPadding.bottom + padding.bottom + topStackView.bounds.height + bottomStackView.bounds.height
|
||||
}
|
||||
|
||||
/// The fixed widthAnchor constant of the leftStackView
|
||||
private(set) var leftStackViewWidthContant: CGFloat = 0 {
|
||||
public private(set) var leftStackViewWidthConstant: CGFloat = 0 {
|
||||
didSet {
|
||||
leftStackViewLayoutSet?.width?.constant = leftStackViewWidthContant
|
||||
leftStackViewLayoutSet?.width?.constant = leftStackViewWidthConstant
|
||||
}
|
||||
}
|
||||
|
||||
/// The fixed widthAnchor constant of the rightStackView
|
||||
private(set) var rightStackViewWidthContant: CGFloat = 52 {
|
||||
public private(set) var rightStackViewWidthConstant: CGFloat = 52 {
|
||||
didSet {
|
||||
rightStackViewLayoutSet?.width?.constant = rightStackViewWidthContant
|
||||
rightStackViewLayoutSet?.width?.constant = rightStackViewWidthConstant
|
||||
}
|
||||
}
|
||||
|
||||
/// The InputBarItems held in the leftStackView
|
||||
private(set) var leftStackViewItems: [InputBarButtonItem] = []
|
||||
public private(set) var leftStackViewItems: [InputBarButtonItem] = []
|
||||
|
||||
/// The InputBarItems held in the rightStackView
|
||||
private(set) var rightStackViewItems: [InputBarButtonItem] = []
|
||||
public private(set) var rightStackViewItems: [InputBarButtonItem] = []
|
||||
|
||||
/// The InputBarItems held in the bottomStackView
|
||||
private(set) var bottomStackViewItems: [InputBarButtonItem] = []
|
||||
public private(set) var bottomStackViewItems: [InputBarButtonItem] = []
|
||||
|
||||
/// The InputBarItems held in the topStackView
|
||||
public private(set) var topStackViewItems: [InputBarButtonItem] = []
|
||||
|
||||
/// The InputBarItems held to make use of their hooks but they are not automatically added to a UIStackView
|
||||
open var nonStackViewItems: [InputBarButtonItem] = []
|
||||
@@ -190,10 +258,10 @@ open class MessageInputBar: UIView {
|
||||
|
||||
private var textViewLayoutSet: NSLayoutConstraintSet?
|
||||
private var textViewHeightAnchor: NSLayoutConstraint?
|
||||
private var topStackViewLayoutSet: NSLayoutConstraintSet?
|
||||
private var leftStackViewLayoutSet: NSLayoutConstraintSet?
|
||||
private var rightStackViewLayoutSet: NSLayoutConstraintSet?
|
||||
private var bottomStackViewLayoutSet: NSLayoutConstraintSet?
|
||||
private var previousIntrinsicContentSize: CGSize?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
@@ -217,18 +285,20 @@ open class MessageInputBar: UIView {
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
/// Sets up the default properties
|
||||
open func setup() {
|
||||
|
||||
backgroundColor = .inputBarGray
|
||||
autoresizingMask = [.flexibleHeight]
|
||||
setupSubviews()
|
||||
setupConstraints()
|
||||
setupObservers()
|
||||
}
|
||||
|
||||
/// Adds all of the subviews
|
||||
private func setupSubviews() {
|
||||
|
||||
addSubview(blurView)
|
||||
addSubview(backgroundView)
|
||||
addSubview(topStackView)
|
||||
addSubview(inputTextView)
|
||||
addSubview(leftStackView)
|
||||
addSubview(rightStackView)
|
||||
@@ -237,13 +307,20 @@ open class MessageInputBar: UIView {
|
||||
setStackViewItems([sendButton], forStack: .right, animated: false)
|
||||
}
|
||||
|
||||
/// Sets up the initial constraints of each subview
|
||||
private func setupConstraints() {
|
||||
|
||||
separatorLine.addConstraints(topAnchor, left: leftAnchor, right: rightAnchor, heightConstant: 0.5)
|
||||
blurView.fillSuperview()
|
||||
separatorLine.addConstraints(topAnchor, left: leftAnchor, right: rightAnchor, heightConstant: 1)
|
||||
topStackViewLayoutSet = NSLayoutConstraintSet(
|
||||
top: topStackView.topAnchor.constraint(equalTo: topAnchor, constant: topStackViewPadding.top),
|
||||
bottom: topStackView.bottomAnchor.constraint(equalTo: inputTextView.topAnchor, constant: -padding.top),
|
||||
left: topStackView.leftAnchor.constraint(equalTo: leftAnchor, constant: topStackViewPadding.left),
|
||||
right: topStackView.rightAnchor.constraint(equalTo: rightAnchor, constant: -topStackViewPadding.right)
|
||||
)
|
||||
backgroundView.addConstraints(topStackView.bottomAnchor, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor)
|
||||
|
||||
textViewLayoutSet = NSLayoutConstraintSet(
|
||||
top: inputTextView.topAnchor.constraint(equalTo: topAnchor, constant: padding.top),
|
||||
top: inputTextView.topAnchor.constraint(equalTo: topStackView.bottomAnchor, constant: padding.top),
|
||||
bottom: inputTextView.bottomAnchor.constraint(equalTo: bottomStackView.topAnchor, constant: -textViewPadding.bottom),
|
||||
left: inputTextView.leftAnchor.constraint(equalTo: leftStackView.rightAnchor, constant: textViewPadding.left),
|
||||
right: inputTextView.rightAnchor.constraint(equalTo: rightStackView.leftAnchor, constant: -textViewPadding.right)
|
||||
@@ -254,14 +331,14 @@ open class MessageInputBar: UIView {
|
||||
top: leftStackView.topAnchor.constraint(equalTo: topAnchor, constant: padding.top),
|
||||
bottom: leftStackView.bottomAnchor.constraint(equalTo: inputTextView.bottomAnchor, constant: 0),
|
||||
left: leftStackView.leftAnchor.constraint(equalTo: leftAnchor, constant: padding.left),
|
||||
width: leftStackView.widthAnchor.constraint(equalToConstant: leftStackViewWidthContant)
|
||||
width: leftStackView.widthAnchor.constraint(equalToConstant: leftStackViewWidthConstant)
|
||||
)
|
||||
|
||||
rightStackViewLayoutSet = NSLayoutConstraintSet(
|
||||
top: rightStackView.topAnchor.constraint(equalTo: topAnchor, constant: padding.top),
|
||||
bottom: rightStackView.bottomAnchor.constraint(equalTo: inputTextView.bottomAnchor, constant: 0),
|
||||
right: rightStackView.rightAnchor.constraint(equalTo: rightAnchor, constant: -padding.right),
|
||||
width: rightStackView.widthAnchor.constraint(equalToConstant: rightStackViewWidthContant)
|
||||
width: rightStackView.widthAnchor.constraint(equalToConstant: rightStackViewWidthConstant)
|
||||
)
|
||||
|
||||
bottomStackViewLayoutSet = NSLayoutConstraintSet(
|
||||
@@ -273,16 +350,19 @@ open class MessageInputBar: UIView {
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
// Switch to safeAreaLayoutGuide
|
||||
topStackViewLayoutSet?.left = topStackView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor, constant: topStackViewPadding.left)
|
||||
topStackViewLayoutSet?.right = topStackView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: topStackViewPadding.right)
|
||||
leftStackViewLayoutSet?.left = leftStackView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor, constant: padding.left)
|
||||
rightStackViewLayoutSet?.right = rightStackView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: -padding.right)
|
||||
bottomStackViewLayoutSet?.left = bottomStackView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor, constant: padding.left)
|
||||
bottomStackViewLayoutSet?.right = bottomStackView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor, constant: -padding.right)
|
||||
}
|
||||
topStackViewLayoutSet?.activate()
|
||||
leftStackViewLayoutSet?.activate()
|
||||
rightStackViewLayoutSet?.activate()
|
||||
bottomStackViewLayoutSet?.activate()
|
||||
}
|
||||
|
||||
|
||||
open override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
if #available(iOS 11.0, *) {
|
||||
@@ -295,8 +375,8 @@ open class MessageInputBar: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
private func updateViewContraints() {
|
||||
|
||||
private func updatePadding() {
|
||||
|
||||
textViewLayoutSet?.top?.constant = padding.top
|
||||
leftStackViewLayoutSet?.top?.constant = padding.top
|
||||
leftStackViewLayoutSet?.left?.constant = padding.left
|
||||
@@ -307,6 +387,22 @@ open class MessageInputBar: UIView {
|
||||
bottomStackViewLayoutSet?.bottom?.constant = -padding.bottom
|
||||
}
|
||||
|
||||
/// Updates the constraint constants that correspond to the inputTextViewPadding UIEdgeInsets
|
||||
private func updateTextViewPadding() {
|
||||
textViewLayoutSet?.left?.constant = textViewPadding.left
|
||||
textViewLayoutSet?.right?.constant = -textViewPadding.right
|
||||
textViewLayoutSet?.bottom?.constant = -textViewPadding.bottom
|
||||
bottomStackViewLayoutSet?.top?.constant = textViewPadding.bottom
|
||||
}
|
||||
|
||||
/// Updates the constraint constants that correspond to the topStackViewPadding UIEdgeInsets
|
||||
private func updateTopStackViewPadding() {
|
||||
topStackViewLayoutSet?.top?.constant = topStackViewPadding.top
|
||||
topStackViewLayoutSet?.left?.constant = topStackViewPadding.left
|
||||
topStackViewLayoutSet?.right?.constant = -topStackViewPadding.right
|
||||
}
|
||||
|
||||
/// Adds the required notification observers
|
||||
private func setupObservers() {
|
||||
|
||||
NotificationCenter.default.addObserver(self,
|
||||
@@ -323,12 +419,42 @@ open class MessageInputBar: UIView {
|
||||
name: .UITextViewTextDidEndEditing, object: nil)
|
||||
}
|
||||
|
||||
/// Calculates the correct intrinsicContentSize of the MessageInputBar
|
||||
///
|
||||
/// - Returns: The required intrinsicContentSize
|
||||
open func calculateIntrinsicContentSize() -> CGSize {
|
||||
|
||||
let maxTextViewSize = CGSize(width: inputTextView.bounds.width, height: .greatestFiniteMagnitude)
|
||||
let inputTextViewHeight = inputTextView.sizeThatFits(maxTextViewSize).height.rounded()
|
||||
var heightToFit: CGFloat = inputBarMinusTextViewHeight
|
||||
|
||||
if inputTextViewHeight >= maxHeight {
|
||||
if !isOverMaxTextViewHeight {
|
||||
textViewHeightAnchor?.isActive = true
|
||||
inputTextView.isScrollEnabled = true
|
||||
isOverMaxTextViewHeight = true
|
||||
}
|
||||
heightToFit += maxHeight
|
||||
} else {
|
||||
if isOverMaxTextViewHeight {
|
||||
textViewHeightAnchor?.isActive = false
|
||||
inputTextView.isScrollEnabled = false
|
||||
isOverMaxTextViewHeight = false
|
||||
inputTextView.invalidateIntrinsicContentSize()
|
||||
}
|
||||
heightToFit += inputTextViewHeight
|
||||
}
|
||||
return CGSize(width: bounds.width, height: heightToFit)
|
||||
}
|
||||
|
||||
// MARK: - Layout Helper Methods
|
||||
|
||||
/// Layout the given UIStackView's
|
||||
///
|
||||
/// - Parameter positions: The UIStackView's to layout
|
||||
public func layoutStackViews(_ positions: [InputStackView.Position] = [.left, .right, .bottom]) {
|
||||
public func layoutStackViews(_ positions: [InputStackView.Position] = [.left, .right, .bottom, .top]) {
|
||||
|
||||
guard superview != nil else { return }
|
||||
|
||||
for position in positions {
|
||||
switch position {
|
||||
@@ -341,6 +467,9 @@ open class MessageInputBar: UIView {
|
||||
case .bottom:
|
||||
bottomStackView.setNeedsLayout()
|
||||
bottomStackView.layoutIfNeeded()
|
||||
case .top:
|
||||
bottomStackView.setNeedsLayout()
|
||||
bottomStackView.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -356,6 +485,7 @@ open class MessageInputBar: UIView {
|
||||
leftStackViewLayoutSet?.deactivate()
|
||||
rightStackViewLayoutSet?.deactivate()
|
||||
bottomStackViewLayoutSet?.deactivate()
|
||||
topStackViewLayoutSet?.deactivate()
|
||||
if animated {
|
||||
DispatchQueue.main.async {
|
||||
UIView.animate(withDuration: 0.3, animations: animations)
|
||||
@@ -367,11 +497,12 @@ open class MessageInputBar: UIView {
|
||||
leftStackViewLayoutSet?.activate()
|
||||
rightStackViewLayoutSet?.activate()
|
||||
bottomStackViewLayoutSet?.activate()
|
||||
topStackViewLayoutSet?.activate()
|
||||
}
|
||||
|
||||
// MARK: - UIStackView InputBarItem Methods
|
||||
|
||||
/// Removes all of the arranged subviews from the UIStackView and adds the given items. Sets the inputBarAccessoryView property of the InputBarButtonItem
|
||||
/// Removes all of the arranged subviews from the UIStackView and adds the given items. Sets the messageInputBar property of the InputBarButtonItem
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - items: New UIStackView arranged views
|
||||
@@ -389,6 +520,7 @@ open class MessageInputBar: UIView {
|
||||
$0.parentStackViewPosition = position
|
||||
leftStackView.addArrangedSubview($0)
|
||||
}
|
||||
guard superview != nil else { return }
|
||||
leftStackView.layoutIfNeeded()
|
||||
case .right:
|
||||
rightStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
@@ -398,6 +530,7 @@ open class MessageInputBar: UIView {
|
||||
$0.parentStackViewPosition = position
|
||||
rightStackView.addArrangedSubview($0)
|
||||
}
|
||||
guard superview != nil else { return }
|
||||
rightStackView.layoutIfNeeded()
|
||||
case .bottom:
|
||||
bottomStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
@@ -407,8 +540,20 @@ open class MessageInputBar: UIView {
|
||||
$0.parentStackViewPosition = position
|
||||
bottomStackView.addArrangedSubview($0)
|
||||
}
|
||||
guard superview != nil else { return }
|
||||
bottomStackView.layoutIfNeeded()
|
||||
case .top:
|
||||
topStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
topStackViewItems = items
|
||||
topStackViewItems.forEach {
|
||||
$0.messageInputBar = self
|
||||
$0.parentStackViewPosition = position
|
||||
topStackView.addArrangedSubview($0)
|
||||
}
|
||||
guard superview != nil else { return }
|
||||
topStackView.layoutIfNeeded()
|
||||
}
|
||||
invalidateIntrinsicContentSize()
|
||||
}
|
||||
|
||||
performLayout(animated) {
|
||||
@@ -423,8 +568,9 @@ open class MessageInputBar: UIView {
|
||||
/// - animated: If the layout should be animated
|
||||
open func setLeftStackViewWidthConstant(to newValue: CGFloat, animated: Bool) {
|
||||
performLayout(animated) {
|
||||
self.leftStackViewWidthContant = newValue
|
||||
self.leftStackViewWidthConstant = newValue
|
||||
self.layoutStackViews([.left])
|
||||
guard self.superview != nil else { return }
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
@@ -436,19 +582,25 @@ open class MessageInputBar: UIView {
|
||||
/// - animated: If the layout should be animated
|
||||
open func setRightStackViewWidthConstant(to newValue: CGFloat, animated: Bool) {
|
||||
performLayout(animated) {
|
||||
self.rightStackViewWidthContant = newValue
|
||||
self.rightStackViewWidthConstant = newValue
|
||||
self.layoutStackViews([.right])
|
||||
guard self.superview != nil else { return }
|
||||
self.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Notifications/Hooks
|
||||
|
||||
/// Invalidates the intrinsicContentSize
|
||||
@objc
|
||||
open func orientationDidChange() {
|
||||
invalidateIntrinsicContentSize()
|
||||
}
|
||||
|
||||
/// Enables/Disables the sendButton based on the InputTextView's text being empty
|
||||
/// Calls each items `textViewDidChangeAction` method
|
||||
/// Calls the delegates `textViewTextDidChangeTo` method
|
||||
/// Invalidates the intrinsicContentSize
|
||||
@objc
|
||||
open func textViewDidChange() {
|
||||
let trimmedText = inputTextView.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
@@ -462,11 +614,13 @@ open class MessageInputBar: UIView {
|
||||
invalidateIntrinsicContentSize()
|
||||
}
|
||||
|
||||
/// Calls each items `keyboardEditingBeginsAction` method
|
||||
@objc
|
||||
open func textViewDidBeginEditing() {
|
||||
self.items.forEach { $0.keyboardEditingBeginsAction() }
|
||||
}
|
||||
|
||||
/// Calls each items `keyboardEditingEndsAction` method
|
||||
@objc
|
||||
open func textViewDidEndEditing() {
|
||||
self.items.forEach { $0.keyboardEditingEndsAction() }
|
||||
@@ -474,6 +628,9 @@ open class MessageInputBar: UIView {
|
||||
|
||||
// MARK: - User Actions
|
||||
|
||||
/// Calls the delegates `didPressSendButtonWith` method
|
||||
/// Assumes that the InputTextView's text has been set to empty and calls `inputTextViewDidChange()`
|
||||
/// Invalidates each of the inputManagers
|
||||
open func didSelectSendButton() {
|
||||
delegate?.messageInputBar(self, didPressSendButtonWith: inputTextView.text)
|
||||
textViewDidChange()
|
||||
|
||||
@@ -50,100 +50,94 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate {
|
||||
}()
|
||||
|
||||
private lazy var rangesForDetectors: [DetectorType: [(NSRange, MessageTextCheckingType)]] = [:]
|
||||
|
||||
private var isConfiguring: Bool = false
|
||||
|
||||
// MARK: - Public Properties
|
||||
|
||||
open weak var delegate: MessageLabelDelegate?
|
||||
|
||||
open var enabledDetectors: [DetectorType] = [.phoneNumber, .address, .date, .url]
|
||||
open var enabledDetectors: [DetectorType] = [] {
|
||||
didSet {
|
||||
setTextStorage(shouldParse: true)
|
||||
}
|
||||
}
|
||||
|
||||
open override var attributedText: NSAttributedString? {
|
||||
didSet {
|
||||
guard attributedText != oldValue else { return }
|
||||
setTextStorage()
|
||||
setTextStorage(shouldParse: true)
|
||||
}
|
||||
}
|
||||
|
||||
open override var text: String? {
|
||||
didSet {
|
||||
guard text != oldValue else { return }
|
||||
setTextStorage()
|
||||
setTextStorage(shouldParse: true)
|
||||
}
|
||||
}
|
||||
|
||||
open override var font: UIFont! {
|
||||
didSet {
|
||||
guard font != oldValue else { return }
|
||||
guard let attributedText = attributedText else { return }
|
||||
textStorage.setAttributedString(attributedText)
|
||||
setNeedsDisplay()
|
||||
setTextStorage(shouldParse: false)
|
||||
}
|
||||
}
|
||||
|
||||
open override var textColor: UIColor! {
|
||||
didSet {
|
||||
guard textColor != oldValue else { return }
|
||||
guard let attributedText = attributedText else { return }
|
||||
textStorage.setAttributedString(attributedText)
|
||||
setNeedsDisplay()
|
||||
setTextStorage(shouldParse: false)
|
||||
}
|
||||
}
|
||||
|
||||
open override var lineBreakMode: NSLineBreakMode {
|
||||
didSet {
|
||||
guard lineBreakMode != oldValue else { return }
|
||||
textContainer.lineBreakMode = lineBreakMode
|
||||
setNeedsDisplay()
|
||||
if !isConfiguring { setNeedsDisplay() }
|
||||
}
|
||||
}
|
||||
|
||||
open override var numberOfLines: Int {
|
||||
didSet {
|
||||
guard numberOfLines != oldValue else { return }
|
||||
textContainer.maximumNumberOfLines = numberOfLines
|
||||
setNeedsDisplay()
|
||||
if !isConfiguring { setNeedsDisplay() }
|
||||
}
|
||||
}
|
||||
|
||||
open override var textAlignment: NSTextAlignment {
|
||||
didSet {
|
||||
guard textAlignment != oldValue else { return }
|
||||
setTextStorage()
|
||||
setTextStorage(shouldParse: false)
|
||||
}
|
||||
}
|
||||
|
||||
open var textInsets: UIEdgeInsets = .zero {
|
||||
didSet {
|
||||
guard textInsets != oldValue else { return }
|
||||
setNeedsDisplay()
|
||||
if !isConfiguring { setNeedsDisplay() }
|
||||
}
|
||||
}
|
||||
|
||||
open var addressAttributes: [NSAttributedStringKey: Any] = [:] {
|
||||
didSet {
|
||||
updateAttributes(for: .address)
|
||||
setNeedsDisplay()
|
||||
if !isConfiguring { setNeedsDisplay() }
|
||||
}
|
||||
}
|
||||
|
||||
open var dateAttributes: [NSAttributedStringKey: Any] = [:] {
|
||||
didSet {
|
||||
updateAttributes(for: .date)
|
||||
setNeedsDisplay()
|
||||
if !isConfiguring { setNeedsDisplay() }
|
||||
}
|
||||
}
|
||||
|
||||
open var phoneNumberAttributes: [NSAttributedStringKey: Any] = [:] {
|
||||
didSet {
|
||||
updateAttributes(for: .phoneNumber)
|
||||
setNeedsDisplay()
|
||||
if !isConfiguring { setNeedsDisplay() }
|
||||
}
|
||||
}
|
||||
|
||||
open var urlAttributes: [NSAttributedStringKey: Any] = [:] {
|
||||
didSet {
|
||||
updateAttributes(for: .url)
|
||||
setNeedsDisplay()
|
||||
if !isConfiguring { setNeedsDisplay() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,6 +184,13 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
public func configure(block: () -> Void) {
|
||||
isConfiguring = true
|
||||
block()
|
||||
isConfiguring = false
|
||||
setNeedsDisplay()
|
||||
}
|
||||
|
||||
// MARK: UIGestureRecognizer Delegate
|
||||
|
||||
@@ -232,60 +233,53 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate {
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
private func setTextStorage() {
|
||||
|
||||
// Anytime we update the text storage we need to clear the previous ranges
|
||||
rangesForDetectors.removeAll()
|
||||
private func setTextStorage(shouldParse: Bool) {
|
||||
|
||||
guard let attributedText = attributedText, attributedText.length > 0 else {
|
||||
textStorage.setAttributedString(NSAttributedString())
|
||||
setNeedsDisplay()
|
||||
return
|
||||
}
|
||||
|
||||
guard let checkingResults = parse(text: attributedText, for: enabledDetectors), checkingResults.isEmpty == false else {
|
||||
let textWithParagraphAttributes = addParagraphStyleAttribute(to: attributedText)
|
||||
textStorage.setAttributedString(textWithParagraphAttributes)
|
||||
setNeedsDisplay()
|
||||
return
|
||||
|
||||
let style = paragraphStyle(for: attributedText)
|
||||
let range = NSRange(location: 0, length: attributedText.length)
|
||||
|
||||
let mutableText = NSMutableAttributedString(attributedString: attributedText)
|
||||
mutableText.addAttribute(.paragraphStyle, value: style, range: range)
|
||||
|
||||
if shouldParse {
|
||||
rangesForDetectors.removeAll()
|
||||
let results = parse(text: mutableText)
|
||||
setRangesForDetectors(in: results)
|
||||
}
|
||||
|
||||
for (detector, rangeTuples) in rangesForDetectors {
|
||||
if enabledDetectors.contains(detector) {
|
||||
let attributes = detectorAttributes(for: detector)
|
||||
rangeTuples.forEach { (range, _) in
|
||||
mutableText.addAttributes(attributes, range: range)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setRangesForDetectors(in: checkingResults)
|
||||
let modifiedText = NSAttributedString(attributedString: mutableText)
|
||||
textStorage.setAttributedString(modifiedText)
|
||||
|
||||
let textWithDetectorAttributes = addDetectorAttributes(to: attributedText, for: checkingResults)
|
||||
let textWithParagraphAttributes = addParagraphStyleAttribute(to: textWithDetectorAttributes)
|
||||
|
||||
textStorage.setAttributedString(textWithParagraphAttributes)
|
||||
|
||||
setNeedsDisplay()
|
||||
if !isConfiguring { setNeedsDisplay() }
|
||||
|
||||
}
|
||||
|
||||
private func addParagraphStyleAttribute(to text: NSAttributedString) -> NSAttributedString {
|
||||
|
||||
let mutableAttributedString = NSMutableAttributedString(attributedString: text)
|
||||
var textRange = NSRange(location: 0, length: 0)
|
||||
|
||||
let paragraphStyle = text.attribute(NSAttributedStringKey.paragraphStyle, at: 0, effectiveRange: &textRange) as? NSMutableParagraphStyle ?? NSMutableParagraphStyle()
|
||||
paragraphStyle.lineBreakMode = lineBreakMode
|
||||
paragraphStyle.alignment = textAlignment
|
||||
|
||||
mutableAttributedString.addAttribute(NSAttributedStringKey.paragraphStyle, value: paragraphStyle, range: textRange)
|
||||
|
||||
return mutableAttributedString
|
||||
|
||||
}
|
||||
|
||||
private func addDetectorAttributes(to text: NSAttributedString, for checkingResults: [NSTextCheckingResult]) -> NSAttributedString {
|
||||
|
||||
let mutableAttributedString = NSMutableAttributedString(attributedString: text)
|
||||
|
||||
checkingResults.forEach { result in
|
||||
let attributes = detectorAttributes(for: result.resultType)
|
||||
mutableAttributedString.addAttributes(attributes, range: result.range)
|
||||
}
|
||||
|
||||
return mutableAttributedString
|
||||
|
||||
private func paragraphStyle(for text: NSAttributedString) -> NSParagraphStyle {
|
||||
guard text.length > 0 else { return NSParagraphStyle() }
|
||||
|
||||
var range = NSRange(location: 0, length: text.length)
|
||||
let existingStyle = text.attribute(.paragraphStyle, at: 0, effectiveRange: &range) as? NSMutableParagraphStyle
|
||||
let style = existingStyle ?? NSMutableParagraphStyle()
|
||||
|
||||
style.lineBreakMode = lineBreakMode
|
||||
style.alignment = textAlignment
|
||||
|
||||
return style
|
||||
}
|
||||
|
||||
private func updateAttributes(for detectorType: DetectorType) {
|
||||
@@ -336,16 +330,18 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate {
|
||||
|
||||
// MARK: - Parsing Text
|
||||
|
||||
private func parse(text: NSAttributedString, for detectorTypes: [DetectorType]) -> [NSTextCheckingResult]? {
|
||||
guard detectorTypes.isEmpty == false else { return nil }
|
||||
let checkingTypes = detectorTypes.reduce(0) { $0 | $1.textCheckingType.rawValue }
|
||||
private func parse(text: NSAttributedString) -> [NSTextCheckingResult] {
|
||||
guard enabledDetectors.isEmpty == false else { return [] }
|
||||
let checkingTypes = enabledDetectors.reduce(0) { $0 | $1.textCheckingType.rawValue }
|
||||
let detector = try? NSDataDetector(types: checkingTypes)
|
||||
|
||||
return detector?.matches(in: text.string, options: [], range: NSRange(location: 0, length: text.length))
|
||||
let range = NSRange(location: 0, length: text.length)
|
||||
return detector?.matches(in: text.string, options: [], range: range) ?? []
|
||||
}
|
||||
|
||||
private func setRangesForDetectors(in checkingResults: [NSTextCheckingResult]) {
|
||||
|
||||
guard checkingResults.isEmpty == false else { return }
|
||||
|
||||
for result in checkingResults {
|
||||
|
||||
switch result.resultType {
|
||||
@@ -388,15 +384,17 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate {
|
||||
location.x -= textOffset.x
|
||||
location.y -= textOffset.y
|
||||
|
||||
let glyphIndex = layoutManager.glyphIndex(for: location, in: textContainer)
|
||||
|
||||
let lineRect = layoutManager.lineFragmentUsedRect(forGlyphAt: glyphIndex, effectiveRange: nil)
|
||||
let index = layoutManager.glyphIndex(for: location, in: textContainer)
|
||||
|
||||
let lineRect = layoutManager.lineFragmentUsedRect(forGlyphAt: index, effectiveRange: nil)
|
||||
|
||||
var characterIndex: Int?
|
||||
|
||||
if lineRect.contains(location) {
|
||||
return layoutManager.characterIndexForGlyph(at: glyphIndex)
|
||||
} else {
|
||||
return nil
|
||||
characterIndex = layoutManager.characterIndexForGlyph(at: index)
|
||||
}
|
||||
|
||||
return characterIndex
|
||||
|
||||
}
|
||||
|
||||
@@ -408,7 +406,7 @@ open class MessageLabel: UILabel, UIGestureRecognizerDelegate {
|
||||
|
||||
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
|
||||
addGestureRecognizer(longPressGesture)
|
||||
tapGesture.delegate = self
|
||||
longPressGesture.delegate = self
|
||||
|
||||
isUserInteractionEnabled = true
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ class MessagesViewControllerTests: XCTestCase {
|
||||
|
||||
}
|
||||
|
||||
private class MockLayoutDelegate: MessagesLayoutDelegate, LocationMessageLayoutDelegate, MediaMessageLayoutDelegate {
|
||||
private class MockLayoutDelegate: MessagesLayoutDelegate {
|
||||
|
||||
// MARK: - LocationMessageLayoutDelegate
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class AvatarTests: XCTestCase {
|
||||
func testDefaultInit() {
|
||||
let avatar = Avatar()
|
||||
XCTAssertNil(avatar.image)
|
||||
XCTAssertEqual(avatar.initals, "?")
|
||||
XCTAssertEqual(avatar.initials, "?")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ class MessagesDisplayDelegateTests: XCTestCase {
|
||||
func testAvatarDefaultState() {
|
||||
XCTAssertNotNil(sut.dataProvider.avatar(for: sut.dataProvider.messages[0],
|
||||
at: IndexPath(item: 0, section: 0),
|
||||
in: sut.messagesCollectionView).initals)
|
||||
in: sut.messagesCollectionView).initials)
|
||||
}
|
||||
|
||||
func testCellTopLabelDefaultState() {
|
||||
@@ -226,18 +226,23 @@ class TextMessageDisplayDelegateTests: XCTestCase {
|
||||
XCTAssertEqual(textColor, .darkText)
|
||||
}
|
||||
|
||||
func testEnableDetectors_returnsUrlAddressPhoneNumberAndDateForDefault() {
|
||||
func testEnableDetectors_returnsEmptyForDefault() {
|
||||
let detectors = sut.enabledDetectors(for: sut.dataProvider.messages[1],
|
||||
at: IndexPath(item: 0, section: 0),
|
||||
in: sut.messagesCollectionView)
|
||||
let expectedDetectors: [DetectorType] = [.url, .address, .phoneNumber, .date]
|
||||
let expectedDetectors: [DetectorType] = []
|
||||
|
||||
XCTAssertEqual(detectors, expectedDetectors)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class MockMessagesViewController: MessagesViewController, MessagesDisplayDelegate, TextMessageDisplayDelegate, MessagesLayoutDelegate {
|
||||
private class MockMessagesViewController: MessagesViewController, MessagesDisplayDelegate, MessagesLayoutDelegate {
|
||||
|
||||
func heightForLocation(message: MessageType, at indexPath: IndexPath, with maxWidth: CGFloat, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
|
||||
return 200
|
||||
}
|
||||
|
||||
|
||||
var dataProvider: MockMessagesDataSource!
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class AvatarViewTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testNoParams() {
|
||||
XCTAssertEqual(avatarView.avatar.initals, "?")
|
||||
XCTAssertEqual(avatarView.avatar.initials, "?")
|
||||
XCTAssertEqual(avatarView.layer.cornerRadius, 15.0)
|
||||
XCTAssertEqual(avatarView.backgroundColor, UIColor.gray)
|
||||
}
|
||||
@@ -49,15 +49,15 @@ class AvatarViewTests: XCTestCase {
|
||||
func testWithImage() {
|
||||
let avatar = Avatar(image: UIImage())
|
||||
avatarView.set(avatar: avatar)
|
||||
XCTAssertEqual(avatar.initals, "?")
|
||||
XCTAssertEqual(avatar.initials, "?")
|
||||
XCTAssertEqual(avatarView.layer.cornerRadius, 15.0)
|
||||
XCTAssertEqual(avatarView.backgroundColor, UIColor.gray)
|
||||
}
|
||||
|
||||
func testInitalsOnly() {
|
||||
let avatar = Avatar(initals: "DL")
|
||||
func testInitialsOnly() {
|
||||
let avatar = Avatar(initials: "DL")
|
||||
avatarView.set(avatar: avatar)
|
||||
XCTAssertEqual(avatar.initals, "DL")
|
||||
XCTAssertEqual(avatar.initials, "DL")
|
||||
XCTAssertEqual(avatarView.layer.cornerRadius, 15.0)
|
||||
XCTAssertEqual(avatarView.backgroundColor, UIColor.gray)
|
||||
}
|
||||
|
||||
@@ -51,19 +51,11 @@ class MessageCollectionViewCellTests: XCTestCase {
|
||||
XCTAssertTrue(cell.messageContainerView.layer.masksToBounds)
|
||||
}
|
||||
|
||||
func testCellTopLabelPropertySetup() {
|
||||
XCTAssertEqual(cell.cellTopLabel.enabledDetectors, [])
|
||||
}
|
||||
|
||||
func testMessageContentViewPropertiesSetup() {
|
||||
XCTAssertTrue(cell.messageContentView.clipsToBounds)
|
||||
XCTAssertTrue(cell.messageContentView.isUserInteractionEnabled)
|
||||
}
|
||||
|
||||
func testCellBottomLabelPropertiesSetup() {
|
||||
XCTAssertEqual(cell.cellBottomLabel.enabledDetectors, [])
|
||||
}
|
||||
|
||||
func testPrepareForReuse() {
|
||||
cell.prepareForReuse()
|
||||
XCTAssertNil(cell.cellTopLabel.text)
|
||||
@@ -79,10 +71,8 @@ class MessageCollectionViewCellTests: XCTestCase {
|
||||
XCTAssertEqual(cell.avatarView.frame, layoutAttributes.frame)
|
||||
XCTAssertEqual(cell.messageContainerView.frame, layoutAttributes.messageContainerFrame)
|
||||
XCTAssertEqual(cell.messageContentView.frame, cell.messageContainerView.frame)
|
||||
XCTAssertEqual(cell.cellTopLabel.frame, layoutAttributes.cellTopLabelFrame)
|
||||
XCTAssertEqual(cell.cellTopLabel.textInsets, layoutAttributes.cellTopLabelInsets)
|
||||
XCTAssertEqual(cell.cellBottomLabel.frame, layoutAttributes.cellBottomLabelFrame)
|
||||
XCTAssertEqual(cell.cellBottomLabel.textInsets, layoutAttributes.cellBottomLabelInsets)
|
||||
XCTAssertEqual(cell.cellTopLabel.frame, layoutAttributes.topLabelFrame)
|
||||
XCTAssertEqual(cell.cellBottomLabel.frame, layoutAttributes.bottomLabelFrame)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -43,23 +43,17 @@ class MessageInputBarTests: XCTestCase {
|
||||
XCTAssertFalse(sut.blurView.translatesAutoresizingMaskIntoConstraints)
|
||||
}
|
||||
|
||||
func testBlurEffectIsHidden_isTrueAfterInit() {
|
||||
XCTAssertTrue(sut.blurView.isHidden)
|
||||
}
|
||||
|
||||
func testIsTranslucent_isFalseForDefault() {
|
||||
XCTAssertFalse(sut.isTranslucent)
|
||||
}
|
||||
|
||||
func testUISetups_forIsTranslucentIsTrue() {
|
||||
sut.isTranslucent = true
|
||||
XCTAssertEqual(sut.backgroundColor, UIColor.clear)
|
||||
XCTAssertFalse(sut.blurView.isHidden)
|
||||
}
|
||||
|
||||
func testUISetups_forIsTranslucentIsFalse() {
|
||||
sut.isTranslucent = false
|
||||
XCTAssertEqual(sut.backgroundColor, UIColor.white)
|
||||
XCTAssertTrue(sut.blurView.isHidden)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user