From c1b397ebc18d399135216f700c19ab6c64425245 Mon Sep 17 00:00:00 2001 From: Andrea Antonioni Date: Sat, 5 Aug 2017 17:03:49 +0200 Subject: [PATCH 01/17] Change send button UI and implemented disabled logic. --- Sources/MessageInputBar.swift | 9 ++++++++- Sources/UIColor+Extensions.swift | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/MessageInputBar.swift b/Sources/MessageInputBar.swift index fd89376e..6ff4889e 100644 --- a/Sources/MessageInputBar.swift +++ b/Sources/MessageInputBar.swift @@ -47,7 +47,12 @@ open class MessageInputBar: UIView, UITextViewDelegate { let sendButton = UIButton() sendButton.setTitle("Send", for: .normal) - sendButton.setTitleColor(.lightGray, for: .normal) + sendButton.setTitleColor(.sendButtonBlue, for: .normal) + sendButton.setTitleColor(UIColor.sendButtonBlue.withAlphaComponent(0.3), for: .highlighted) + sendButton.setTitleColor(.lightGray, for: .disabled) + sendButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .headline) + sendButton.isEnabled = false + return sendButton }() @@ -91,6 +96,8 @@ open class MessageInputBar: UIView, UITextViewDelegate { } public func textViewDidChange(_ textView: UITextView) { + let trimmedText = textView.text.trimmingCharacters(in: .whitespacesAndNewlines) + sendButton.isEnabled = !trimmedText.isEmpty && textView.text != "New Message" invalidateIntrinsicContentSize() } diff --git a/Sources/UIColor+Extensions.swift b/Sources/UIColor+Extensions.swift index 04bc89f7..0ba08932 100644 --- a/Sources/UIColor+Extensions.swift +++ b/Sources/UIColor+Extensions.swift @@ -32,4 +32,6 @@ extension UIColor { static let inputBarGray = UIColor(colorLiteralRed: 247/255, green: 247/255, blue: 247/255, alpha: 1.0) + static let sendButtonBlue = UIColor(colorLiteralRed: 15/255, green: 135/255, blue: 255/255, alpha: 1.0) + } From 691c3eaa5331c4d3678d7ce449e6eef325d6fc7e Mon Sep 17 00:00:00 2001 From: Andrea Antonioni Date: Sun, 6 Aug 2017 00:04:29 +0200 Subject: [PATCH 02/17] Implement InputTextView with support to a placeholder --- MessageKit.xcodeproj/project.pbxproj | 4 + Sources/InputTextView.swift | 109 +++++++++++++++++++++++++++ Sources/MessageInputBar.swift | 8 +- 3 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 Sources/InputTextView.swift diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 5fd8f5d0..e7586836 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 171D5AB91F36712B0053DF69 /* InputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171D5AB81F36712B0053DF69 /* InputTextView.swift */; }; 882D75841DE507320033F95F /* MessagesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882D75831DE507320033F95F /* MessagesDataSource.swift */; }; 888CEBFC1D3FD525005178DE /* MessagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 888CEBFB1D3FD525005178DE /* MessagesViewController.swift */; }; 88916B2D1CF0DF2F00469F91 /* MessageKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 88916B221CF0DF2F00469F91 /* MessageKit.framework */; }; @@ -43,6 +44,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 171D5AB81F36712B0053DF69 /* InputTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextView.swift; sourceTree = ""; }; 882D75831DE507320033F95F /* MessagesDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDataSource.swift; sourceTree = ""; }; 888CEBFB1D3FD525005178DE /* MessagesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesViewController.swift; sourceTree = ""; }; 88916B221CF0DF2F00469F91 /* MessageKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MessageKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -157,6 +159,7 @@ B0655A2D1F23D8BC00542A83 /* MessagesCollectionView.swift */, B0655A4C1F244C0600542A83 /* MessageCollectionViewCell.swift */, B0655A371F23EE8B00542A83 /* MessageInputBar.swift */, + 171D5AB81F36712B0053DF69 /* InputTextView.swift */, B074EE921F35587100ABB8C8 /* MessageHeaderView.swift */, B074EE941F35588A00ABB8C8 /* MessageFooterView.swift */, ); @@ -328,6 +331,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 171D5AB91F36712B0053DF69 /* InputTextView.swift in Sources */, B074EE971F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift in Sources */, 882D75841DE507320033F95F /* MessagesDataSource.swift in Sources */, 888CEBFC1D3FD525005178DE /* MessagesViewController.swift in Sources */, diff --git a/Sources/InputTextView.swift b/Sources/InputTextView.swift new file mode 100644 index 00000000..d36d34bb --- /dev/null +++ b/Sources/InputTextView.swift @@ -0,0 +1,109 @@ +/* + MIT License + + Copyright (c) 2017 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +open class InputTextView: UITextView { + + // MARK: - Properties + + open var placeholder: NSString? { + didSet { + guard placeholder != oldValue else { return } + setNeedsDisplay() + } + } + + open var placeholderTextColor: UIColor = .lightGray { + didSet { + guard placeholderTextColor != oldValue else { return } + setNeedsDisplay() + } + } + + open var placeholderInsets: UIEdgeInsets = UIEdgeInsets(top: 7, + left: 5, + bottom: 7, + right: 5) { + didSet { + guard placeholderInsets != oldValue else { return } + setNeedsDisplay() + } + } + + // MARK: - Initializers + + override public init(frame: CGRect, textContainer: NSTextContainer?) { + super.init(frame: frame, textContainer: textContainer) + commonInit() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + commonInit() + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: - Methods + + override open func draw(_ rect: CGRect) { + super.draw(rect) + + guard text.isEmpty, let placeholder = placeholder else { return } + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = textAlignment + + var attributes: [String: Any] = [ + NSForegroundColorAttributeName: placeholderTextColor, + NSParagraphStyleAttributeName: paragraphStyle + ] + if let font = font { + attributes[NSFontAttributeName] = font + } + + placeholder.draw(in: UIEdgeInsetsInsetRect(rect, placeholderInsets), + withAttributes: attributes) + + } + + fileprivate func commonInit() { + NotificationCenter.default.addObserver(self, + selector: #selector(textDidChange), + name: NSNotification.Name.UITextViewTextDidChange, + object: self) + } + + func textDidChange(notification: Notification) { + guard let notificationObject = notification.object as? InputTextView, + notificationObject === self else { return } + + setNeedsDisplay() + + } + +} diff --git a/Sources/MessageInputBar.swift b/Sources/MessageInputBar.swift index 6ff4889e..cc251ab6 100644 --- a/Sources/MessageInputBar.swift +++ b/Sources/MessageInputBar.swift @@ -30,10 +30,10 @@ open class MessageInputBar: UIView, UITextViewDelegate { open let inputTextView: UITextView = { - let inputTextView = UITextView(frame: .zero) + let inputTextView = InputTextView(frame: .zero) inputTextView.font = UIFont.preferredFont(forTextStyle: .body) - inputTextView.text = "New Message" - inputTextView.textColor = .lightGray + inputTextView.textColor = .black + inputTextView.placeholder = "New Message" inputTextView.backgroundColor = .white inputTextView.layer.borderColor = UIColor.lightGray.cgColor inputTextView.layer.borderWidth = 1.0 @@ -97,7 +97,7 @@ open class MessageInputBar: UIView, UITextViewDelegate { public func textViewDidChange(_ textView: UITextView) { let trimmedText = textView.text.trimmingCharacters(in: .whitespacesAndNewlines) - sendButton.isEnabled = !trimmedText.isEmpty && textView.text != "New Message" + sendButton.isEnabled = !trimmedText.isEmpty invalidateIntrinsicContentSize() } From 8476ff64053e3aff6ddc22a8306ad2df20cd592c Mon Sep 17 00:00:00 2001 From: Andrea Antonioni Date: Sun, 6 Aug 2017 10:43:16 +0200 Subject: [PATCH 03/17] Improve performance reducing useless redraw of the component --- Sources/InputTextView.swift | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/Sources/InputTextView.swift b/Sources/InputTextView.swift index d36d34bb..17013b96 100644 --- a/Sources/InputTextView.swift +++ b/Sources/InputTextView.swift @@ -52,16 +52,18 @@ open class InputTextView: UITextView { } } + private var isPlaceholderVisibile = false + // MARK: - Initializers override public init(frame: CGRect, textContainer: NSTextContainer?) { super.init(frame: frame, textContainer: textContainer) - commonInit() + addObservers() } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) - commonInit() + addObservers() } deinit { @@ -91,19 +93,26 @@ open class InputTextView: UITextView { } - fileprivate func commonInit() { + fileprivate func addObservers() { + NotificationCenter.default.addObserver(self, + selector: #selector(textDidBeginEdit), + name: Notification.Name.UITextViewTextDidBeginEditing, + object: nil) NotificationCenter.default.addObserver(self, selector: #selector(textDidChange), - name: NSNotification.Name.UITextViewTextDidChange, - object: self) + name: Notification.Name.UITextViewTextDidChange, + object: nil) + } + + func textDidBeginEdit(notification: Notification) { + guard text.isEmpty else { return } + isPlaceholderVisibile = true } func textDidChange(notification: Notification) { - guard let notificationObject = notification.object as? InputTextView, - notificationObject === self else { return } - + guard text.isEmpty || isPlaceholderVisibile else { return } setNeedsDisplay() - + isPlaceholderVisibile = false } } From 767b0f2c5d0351a6fe41b0e14403c8fb60678620 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Mon, 7 Aug 2017 00:26:36 -0500 Subject: [PATCH 04/17] Fixes #37 - allow inset from message edge to cell --- Sources/MessagesCollectionViewFlowLayout.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Sources/MessagesCollectionViewFlowLayout.swift b/Sources/MessagesCollectionViewFlowLayout.swift index 9315ce5e..7dfa3412 100644 --- a/Sources/MessagesCollectionViewFlowLayout.swift +++ b/Sources/MessagesCollectionViewFlowLayout.swift @@ -36,6 +36,8 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { open var messageContainerInsets: UIEdgeInsets + open var messageToEdgePadding: CGFloat + fileprivate let avatarBottomSpacing: CGFloat = 4 fileprivate let avatarContainerSpacing: CGFloat = 4 @@ -56,6 +58,7 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { incomingAvatarSize = CGSize(width: 30, height: 30) outgoingAvatarSize = CGSize(width: 30, height: 30) messageContainerInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 14) + messageToEdgePadding = 30.0 super.init() sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) } @@ -154,9 +157,9 @@ extension MessagesCollectionViewFlowLayout { func containerHeightForMessage(message: MessageType) -> CGFloat { - let avatarSize = avatarSizeFor(message: message) + let avatarWidth = avatarSizeFor(message: message).width let insets = messageContainerInsets.left + messageContainerInsets.right - let availableWidth = itemWidth - avatarSize.width - avatarContainerSpacing - insets + let availableWidth = itemWidth - avatarWidth - avatarContainerSpacing - messageToEdgePadding - insets // This is a switch because support for more messages are to come switch message.data { @@ -172,9 +175,9 @@ extension MessagesCollectionViewFlowLayout { let containerHeight = containerHeightForMessage(message: message) - let avatarSize = avatarSizeFor(message: message) + let avatarWidth = avatarSizeFor(message: message).width let insets = messageContainerInsets.left + messageContainerInsets.right - let availableWidth = itemWidth - avatarSize.width - avatarContainerSpacing - insets + let availableWidth = itemWidth - avatarWidth - avatarContainerSpacing - messageToEdgePadding - insets // This is a switch because support for more messages are to come switch message.data { From dd9814455e7cfe6f8468d8419b590b949a62fd13 Mon Sep 17 00:00:00 2001 From: Andrea Antonioni Date: Mon, 7 Aug 2017 09:07:02 +0200 Subject: [PATCH 05/17] Fix bug on placeholder --- Sources/InputTextView.swift | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/Sources/InputTextView.swift b/Sources/InputTextView.swift index 17013b96..ddf7954d 100644 --- a/Sources/InputTextView.swift +++ b/Sources/InputTextView.swift @@ -90,25 +90,18 @@ open class InputTextView: UITextView { placeholder.draw(in: UIEdgeInsetsInsetRect(rect, placeholderInsets), withAttributes: attributes) + + isPlaceholderVisibile = true } fileprivate func addObservers() { - NotificationCenter.default.addObserver(self, - selector: #selector(textDidBeginEdit), - name: Notification.Name.UITextViewTextDidBeginEditing, - object: nil) NotificationCenter.default.addObserver(self, selector: #selector(textDidChange), name: Notification.Name.UITextViewTextDidChange, object: nil) } - func textDidBeginEdit(notification: Notification) { - guard text.isEmpty else { return } - isPlaceholderVisibile = true - } - func textDidChange(notification: Notification) { guard text.isEmpty || isPlaceholderVisibile else { return } setNeedsDisplay() From 5d79572fd912828dc6f59ee7e28acda6f230fe02 Mon Sep 17 00:00:00 2001 From: MacmeDan Date: Mon, 7 Aug 2017 17:46:22 -0600 Subject: [PATCH 06/17] Added the `Avatar` struct which has an image and initials. Added an `Avatar` property to the `AvatarView` Replaced the `imageView` in the `MessagesCollectionViewCell` with the `AvatarView` Moved the `Avatar.playground` to the example project so users can play around and learn it capabilities. Updated the example project and abstracted the `sampleData` to its own file. Updated the organization for the project to `MessageKit` instead of `Hex bits` --- Example/ChatExample.xcodeproj/project.pbxproj | 8 +- .../Dan-Leonard.imageset/Contents.json | 21 ++++ .../Dan-Leonard.imageset}/NiceSelfi.jpg | Bin Example/Sources/Base.lproj/Main.storyboard | 95 +----------------- .../Sources/ConversationViewController.swift | 70 ++----------- Example/Sources/InboxViewController.swift | 21 +++- Example/Sources/MockMessage.swift | 4 +- .../Avatar.playground/Contents.swift | 29 ++++++ .../Avatar.playground/Resources/NiceSelfi.jpg | Bin 0 -> 41110 bytes .../Avatar.playground}/contents.xcplayground | 2 +- .../Avatar.playground/timeline.xctimeline | 6 ++ Example/Sources/SampleData.swift | 46 +++++++++ MessageKit.xcodeproj/project.pbxproj | 8 +- Sources/Avatar.swift | 20 ++++ Sources/AvatarView.playground/Contents.swift | 37 ------- Sources/AvatarView.swift | 78 +++++++------- Sources/MessageCollectionViewCell.swift | 12 +-- Sources/MessagesDisplayDataSource.swift | 2 +- Sources/MessagesViewController.swift | 5 +- 19 files changed, 207 insertions(+), 257 deletions(-) create mode 100644 Example/Sources/Assets.xcassets/Dan-Leonard.imageset/Contents.json rename {Sources/AvatarView.playground/Resources => Example/Sources/Assets.xcassets/Dan-Leonard.imageset}/NiceSelfi.jpg (100%) create mode 100644 Example/Sources/Playgrounds/Avatar.playground/Contents.swift create mode 100644 Example/Sources/Playgrounds/Avatar.playground/Resources/NiceSelfi.jpg rename {Sources/AvatarView.playground => Example/Sources/Playgrounds/Avatar.playground}/contents.xcplayground (61%) create mode 100644 Example/Sources/Playgrounds/Avatar.playground/timeline.xctimeline create mode 100644 Example/Sources/SampleData.swift create mode 100644 Sources/Avatar.swift delete mode 100644 Sources/AvatarView.playground/Contents.swift diff --git a/Example/ChatExample.xcodeproj/project.pbxproj b/Example/ChatExample.xcodeproj/project.pbxproj index 550cf04b..432525d2 100644 --- a/Example/ChatExample.xcodeproj/project.pbxproj +++ b/Example/ChatExample.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 37D3EAC41F390E5F00DD6A55 /* SampleData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D3EAC31F390E5F00DD6A55 /* SampleData.swift */; }; 882B5E811CF7D53600B6E160 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882B5E781CF7D53600B6E160 /* AppDelegate.swift */; }; 882B5E821CF7D53600B6E160 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 882B5E791CF7D53600B6E160 /* Assets.xcassets */; }; 882B5E831CF7D53600B6E160 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 882B5E7A1CF7D53600B6E160 /* LaunchScreen.storyboard */; }; @@ -74,6 +75,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 37D3EAC31F390E5F00DD6A55 /* SampleData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleData.swift; sourceTree = ""; }; + 37F8BCD41F38F3A8003C12C2 /* Avatar.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = Avatar.playground; path = Playgrounds/Avatar.playground; sourceTree = ""; }; 882B5E331CF7D4B900B6E160 /* ChatExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ChatExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 882B5E491CF7D4B900B6E160 /* ChatExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ChatExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 882B5E541CF7D4B900B6E160 /* ChatExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ChatExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -145,7 +148,9 @@ children = ( 882B5E781CF7D53600B6E160 /* AppDelegate.swift */, B0655A321F23E90800542A83 /* ConversationViewController.swift */, + 37D3EAC31F390E5F00DD6A55 /* SampleData.swift */, 882B5E7E1CF7D53600B6E160 /* InboxViewController.swift */, + 37F8BCD41F38F3A8003C12C2 /* Avatar.playground */, B096438A1F288D47004D0129 /* MockMessage.swift */, 882B5E801CF7D53600B6E160 /* SettingsViewController.swift */, 882B5E791CF7D53600B6E160 /* Assets.xcassets */, @@ -249,7 +254,7 @@ attributes = { LastSwiftUpdateCheck = 0730; LastUpgradeCheck = 0810; - ORGANIZATIONNAME = "Hexed Bits"; + ORGANIZATIONNAME = MessageKit; TargetAttributes = { 882B5E321CF7D4B900B6E160 = { CreatedOnToolsVersion = 7.3.1; @@ -342,6 +347,7 @@ buildActionMask = 2147483647; files = ( 882B5E871CF7D53600B6E160 /* SettingsViewController.swift in Sources */, + 37D3EAC41F390E5F00DD6A55 /* SampleData.swift in Sources */, B096438B1F288D47004D0129 /* MockMessage.swift in Sources */, 882B5E811CF7D53600B6E160 /* AppDelegate.swift in Sources */, B0655A331F23E90800542A83 /* ConversationViewController.swift in Sources */, diff --git a/Example/Sources/Assets.xcassets/Dan-Leonard.imageset/Contents.json b/Example/Sources/Assets.xcassets/Dan-Leonard.imageset/Contents.json new file mode 100644 index 00000000..634187f5 --- /dev/null +++ b/Example/Sources/Assets.xcassets/Dan-Leonard.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "NiceSelfi.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Sources/AvatarView.playground/Resources/NiceSelfi.jpg b/Example/Sources/Assets.xcassets/Dan-Leonard.imageset/NiceSelfi.jpg similarity index 100% rename from Sources/AvatarView.playground/Resources/NiceSelfi.jpg rename to Example/Sources/Assets.xcassets/Dan-Leonard.imageset/NiceSelfi.jpg diff --git a/Example/Sources/Base.lproj/Main.storyboard b/Example/Sources/Base.lproj/Main.storyboard index 1c6fff35..f7cdb8d6 100644 --- a/Example/Sources/Base.lproj/Main.storyboard +++ b/Example/Sources/Base.lproj/Main.storyboard @@ -1,17 +1,18 @@ - + - + + @@ -25,9 +26,6 @@ - - - @@ -35,30 +33,11 @@ - + - - - - - - - - - - - - - - - - - - - @@ -76,71 +55,5 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index d34af97f..1adf1277 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -27,66 +27,17 @@ import MessageKit class ConversationViewController: MessagesViewController { - var messages: [MessageType] = [] + var messageList: [MockMessage] = [] override func viewDidLoad() { super.viewDidLoad() - addSampleData() - + messageList = SampleData().getMessages() messagesCollectionView.messagesDataSource = self messagesCollectionView.messageCellDelegate = self messagesCollectionView.messagesLayoutDelegate = self messageInputBar.delegate = self - - tabBarController?.tabBar.isHidden = true } - - func addSampleData() { - - let sender1 = Sender(id: "123456", displayName: "Bobby") - let sender2 = Sender(id: "654321", displayName: "Steven") - let sender3 = Sender(id: "777999", displayName: "Omar") - - let msg1 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + - "Pellentesque venenatis, ante et hendrerit rutrum" + - "Quam erat vehicula metus, et condimentum ante tellus augue." - - let msg2 = "Cras efficitur bibendum mauris sed ultrices." + - "Phasellus tellus nisl, ullamcorper quis erat." - - let msg3 = "Maecenas." - - let msg4 = "Pellentesque venenatis, ante et hendrerit rutrum" + - "Quam erat vehicula metus, et condimentum ante tellus augue." - - let msg5 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + - "Pellentesque venenatis, ante et hendrerit rutrum" + - "Quam erat vehicula metus, et condimentum ante tellus augue." - - messages.append(MockMessage(text: msg2, sender: sender2, id: NSUUID().uuidString)) - messages.append(MockMessage(text: msg4, sender: currentSender(), id: NSUUID().uuidString)) - messages.append(MockMessage(text: msg5, sender: sender3, id: NSUUID().uuidString)) - messages.append(MockMessage(text: msg1, sender: currentSender(), id: NSUUID().uuidString)) - messages.append(MockMessage(text: msg3, sender: sender1, id: NSUUID().uuidString)) - messages.append(MockMessage(text: msg3, sender: sender1, id: NSUUID().uuidString)) - messages.append(MockMessage(text: msg2, sender: sender2, id: NSUUID().uuidString)) - messages.append(MockMessage(text: msg2, sender: sender2, id: NSUUID().uuidString)) - messages.append(MockMessage(text: msg1, sender: currentSender(), id: NSUUID().uuidString)) - messages.append(MockMessage(text: msg3, sender: sender1, id: NSUUID().uuidString)) - messages.append(MockMessage(text: msg2, sender: sender2, id: NSUUID().uuidString)) - messages.append(MockMessage(text: msg4, sender: currentSender(), id: NSUUID().uuidString)) - messages.append(MockMessage(text: msg5, sender: sender3, id: NSUUID().uuidString)) - messages.append(MockMessage(text: msg4, sender: currentSender(), id: NSUUID().uuidString)) - messages.append(MockMessage(text: msg5, sender: sender3, id: NSUUID().uuidString)) - messages.append(MockMessage(text: msg4, sender: currentSender(), id: NSUUID().uuidString)) - messages.append(MockMessage(text: msg5, sender: sender3, id: NSUUID().uuidString)) - messages.append(MockMessage(text: msg1, sender: currentSender(), id: NSUUID().uuidString)) - messages.append(MockMessage(text: msg1, sender: currentSender(), id: NSUUID().uuidString)) - messages.append(MockMessage(text: msg3, sender: sender1, id: NSUUID().uuidString)) - - } - } // MARK: - MessagesDataSource @@ -94,15 +45,15 @@ class ConversationViewController: MessagesViewController { extension ConversationViewController: MessagesDataSource { func currentSender() -> Sender { - return Sender(id: "123", displayName: "Steven") + return SampleData().getCurrentSender() } func numberOfMessages(in messagesCollectionView: MessagesCollectionView) -> Int { - return messages.count + return messageList.count } func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType { - return messages[indexPath.section] + return messageList[indexPath.section] } } @@ -111,9 +62,8 @@ extension ConversationViewController: MessagesDataSource { extension ConversationViewController: MessagesDisplayDataSource { - func avatarForMessage(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarView { - let image = isFromCurrentSender(message: message) ? #imageLiteral(resourceName: "Steve-Jobs") : #imageLiteral(resourceName: "Tim-Cook") - return AvatarView(image: image) + func avatarForMessage(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Avatar { + return SampleData().getAvatarFor(sender: message.sender) } func headerForMessage(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageHeaderView? { @@ -159,13 +109,9 @@ extension ConversationViewController: MessageCellDelegate { extension ConversationViewController: MessageInputBarDelegate { func sendButtonPressed(sender: UIButton, textView: UITextView) { - guard let message = textView.text else { return } - - messages.append(MockMessage(text: message, sender: currentSender(), id: NSUUID().uuidString)) - + messageList.append(MockMessage(text: message, sender: currentSender(), messageId: UUID().uuidString)) messagesCollectionView.reloadData() - } } diff --git a/Example/Sources/InboxViewController.swift b/Example/Sources/InboxViewController.swift index 31ede66e..4f237127 100644 --- a/Example/Sources/InboxViewController.swift +++ b/Example/Sources/InboxViewController.swift @@ -27,18 +27,33 @@ import MessageKit final class InboxViewController: UITableViewController { + let cells = ["Test", "Settings"] + override func viewDidLoad() { super.viewDidLoad() + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 1 + return cells.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell() - cell.textLabel?.text = "Test" + cell.textLabel?.text = cells[indexPath.row] return cell } - + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let cell = cells[indexPath.row] + switch cell { + case "Test": + navigationController?.pushViewController(ConversationViewController(), animated: true) + case "Settings": + navigationController?.pushViewController(SettingsViewController(), animated: true) + default: + assertionFailure("You need to impliment the action for this cell: \(cell)") + return + } + } } diff --git a/Example/Sources/MockMessage.swift b/Example/Sources/MockMessage.swift index 95e7bbfd..355eb82d 100644 --- a/Example/Sources/MockMessage.swift +++ b/Example/Sources/MockMessage.swift @@ -32,10 +32,10 @@ struct MockMessage: MessageType { var sentDate: Date var data: MessageData - init(text: String, sender: Sender, id: String) { + init(text: String, sender: Sender, messageId: String) { data = .text(text) self.sender = sender - self.messageId = id + self.messageId = messageId self.sentDate = Date() } diff --git a/Example/Sources/Playgrounds/Avatar.playground/Contents.swift b/Example/Sources/Playgrounds/Avatar.playground/Contents.swift new file mode 100644 index 00000000..6292a77e --- /dev/null +++ b/Example/Sources/Playgrounds/Avatar.playground/Contents.swift @@ -0,0 +1,29 @@ +import UIKit +import MessageKit +import PlaygroundSupport + +//: Discover what is possible with the Avatar Class +//Get an image +let testImage = #imageLiteral(resourceName: "NiceSelfi.jpg") + +var avatarView = AvatarView() + +//: Uncomment any line to see how it changes the `Avatar`. Change the parameters and see the effects. + +//: By default its a circlular avatar with a gray background and initals of "?" + +//: Create an avatar object and set it for the view. +//var avatarObject = Avatar(image: testImage) +//avatarView.set(avatar: avatarObject) + +//: If you don't have a picture for the user you can pass in there initals instead. +//avatarObject = Avatar(initals: "DL") +//avatarView.set(avatar: avatarObject) + +//: Want rounded squares instead of circles just adjust the radius with the method .setCorner(radius: CGFLoat)`. +//avatarView.setCorner(radius: 5) + +//: Everything has a default so if you dont want to set it then you dont have to. + +//Helper method. +PlaygroundPage.current.liveView = avatarView diff --git a/Example/Sources/Playgrounds/Avatar.playground/Resources/NiceSelfi.jpg b/Example/Sources/Playgrounds/Avatar.playground/Resources/NiceSelfi.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bec3cfeca48df384efb960dba611610227bedcb6 GIT binary patch literal 41110 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 literal 0 HcmV?d00001 diff --git a/Sources/AvatarView.playground/contents.xcplayground b/Example/Sources/Playgrounds/Avatar.playground/contents.xcplayground similarity index 61% rename from Sources/AvatarView.playground/contents.xcplayground rename to Example/Sources/Playgrounds/Avatar.playground/contents.xcplayground index 5da2641c..89da2d47 100644 --- a/Sources/AvatarView.playground/contents.xcplayground +++ b/Example/Sources/Playgrounds/Avatar.playground/contents.xcplayground @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/Example/Sources/Playgrounds/Avatar.playground/timeline.xctimeline b/Example/Sources/Playgrounds/Avatar.playground/timeline.xctimeline new file mode 100644 index 00000000..bf468afe --- /dev/null +++ b/Example/Sources/Playgrounds/Avatar.playground/timeline.xctimeline @@ -0,0 +1,6 @@ + + + + + diff --git a/Example/Sources/SampleData.swift b/Example/Sources/SampleData.swift new file mode 100644 index 00000000..06fce8ff --- /dev/null +++ b/Example/Sources/SampleData.swift @@ -0,0 +1,46 @@ +// +// SampleData.swift +// ChatExample +// +// Created by Dan Leonard on 8/7/17. +// Copyright © 2017 MessageKit. All rights reserved. +// + +import MessageKit + +struct SampleData { + let Dan = Sender(id: "123456", displayName: "Dan Leonard") + let Steven = Sender(id: "654321", displayName: "Steven") + let Jobs = Sender(id: "000001", displayName: "Steve Jobs") + let Cook = Sender(id: "656361", displayName: "Tim Cook") + + func getMessages() -> [MockMessage] { + let msg1 = MockMessage(text: "Check out this awesome UI library for Chat", sender: Dan, messageId: UUID().uuidString) + let msg2 = MockMessage(text: "This is insane.", sender: Steven, messageId: UUID().uuidString) + let msg3 = MockMessage(text: "Companies that get confused, that think their goal is revenue or stock price or something. You have to focus on the things that lead to those.", sender: Cook, messageId: UUID().uuidString) + let msg4 = MockMessage(text: "My favorite things in life don’t cost any money. It’s really clear that the most precious resource we all have is time.", sender: Jobs, messageId: UUID().uuidString) + let msg5 = MockMessage(text: "You know, this iPhone, as a matter of fact, the engine in here is made in America. And not only are the engines in here made in America, but engines are made in America and are exported. The glass on this phone is made in Kentucky. And so we've been working for years on doing more and more in the United States.", sender: Cook, messageId: UUID().uuidString) + let msg6 = MockMessage(text: "I think if you do something and it turns out pretty good, then you should go do something else wonderful, not dwell on it for too long. Just figure out what’s next.", sender: Jobs, messageId: UUID().uuidString) + + return [msg1, msg2, msg3, msg4, msg5, msg6] + } + + func getCurrentSender() -> Sender { + return Dan + } + + func getAvatarFor(sender: Sender) -> Avatar { + switch sender { + case Dan: + return Avatar(image: #imageLiteral(resourceName: "Dan-Leonard"), initals: "DL") + case Steven: + return Avatar(initals: "S") + case Jobs: + return Avatar(image: #imageLiteral(resourceName: "Steve-Jobs"), initals: "SJ") + case Cook: + return Avatar(image: #imageLiteral(resourceName: "Tim-Cook")) + default: + return Avatar() + } + } +} diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 3a25e9fc..1176f005 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 372F6AEB1F36C15600B57FBD /* AvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372F6AEA1F36C15600B57FBD /* AvatarView.swift */; }; 372F6AEF1F36C61000B57FBD /* AvatarViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372F6AEE1F36C61000B57FBD /* AvatarViewTests.swift */; }; + 37C936981F38F6AC00853DF2 /* Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C936971F38F6AC00853DF2 /* Avatar.swift */; }; 882D75841DE507320033F95F /* MessagesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882D75831DE507320033F95F /* MessagesDataSource.swift */; }; 888CEBFC1D3FD525005178DE /* MessagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 888CEBFB1D3FD525005178DE /* MessagesViewController.swift */; }; 88916B2D1CF0DF2F00469F91 /* MessageKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 88916B221CF0DF2F00469F91 /* MessageKit.framework */; }; @@ -45,8 +46,8 @@ /* Begin PBXFileReference section */ 372F6AEA1F36C15600B57FBD /* AvatarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarView.swift; sourceTree = ""; }; - 372F6AED1F36C1C100B57FBD /* AvatarView.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = AvatarView.playground; sourceTree = ""; }; 372F6AEE1F36C61000B57FBD /* AvatarViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarViewTests.swift; sourceTree = ""; }; + 37C936971F38F6AC00853DF2 /* Avatar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = ""; }; 882D75831DE507320033F95F /* MessagesDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDataSource.swift; sourceTree = ""; }; 888CEBFB1D3FD525005178DE /* MessagesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesViewController.swift; sourceTree = ""; }; 88916B221CF0DF2F00469F91 /* MessageKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MessageKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -149,6 +150,7 @@ children = ( B0655A291F23D77200542A83 /* Sender.swift */, B0655A2B1F23D81600542A83 /* MessageData.swift */, + 37C936971F38F6AC00853DF2 /* Avatar.swift */, B0655A271F23D71400542A83 /* MessageDirection.swift */, ); name = Models; @@ -160,7 +162,6 @@ B0655A2D1F23D8BC00542A83 /* MessagesCollectionView.swift */, B0655A4C1F244C0600542A83 /* MessageCollectionViewCell.swift */, 372F6AEA1F36C15600B57FBD /* AvatarView.swift */, - 372F6AED1F36C1C100B57FBD /* AvatarView.playground */, B0655A371F23EE8B00542A83 /* MessageInputBar.swift */, B074EE921F35587100ABB8C8 /* MessageHeaderView.swift */, B074EE941F35588A00ABB8C8 /* MessageFooterView.swift */, @@ -266,7 +267,7 @@ attributes = { LastSwiftUpdateCheck = 0730; LastUpgradeCheck = 0810; - ORGANIZATIONNAME = "Hexed Bits"; + ORGANIZATIONNAME = MessageKit; TargetAttributes = { 88916B211CF0DF2F00469F91 = { CreatedOnToolsVersion = 7.3.1; @@ -351,6 +352,7 @@ 372F6AEB1F36C15600B57FBD /* AvatarView.swift in Sources */, 88916B471CF0DFE600469F91 /* MessageType.swift in Sources */, B03FF9AF1F31BB1200754FE5 /* MessageCellDelegate.swift in Sources */, + 37C936981F38F6AC00853DF2 /* Avatar.swift in Sources */, B096438E1F2890FB004D0129 /* MessagesDisplayDataSource.swift in Sources */, B015E8191F24623D007EDFB6 /* MessagesCollectionViewLayoutAttributes.swift in Sources */, ); diff --git a/Sources/Avatar.swift b/Sources/Avatar.swift new file mode 100644 index 00000000..e9bb305a --- /dev/null +++ b/Sources/Avatar.swift @@ -0,0 +1,20 @@ +// +// Avatar.swift +// MessageKit +// +// Created by Dan Leonard on 8/7/17. +// Copyright © 2017 MessageKit. All rights reserved. +// + +import Foundation +public struct Avatar { + + public let image: UIImage? + public var initals: String = "?" + + public init(image: UIImage? = nil, initals: String = "?") { + self.image = image + self.initals = initals + } + +} diff --git a/Sources/AvatarView.playground/Contents.swift b/Sources/AvatarView.playground/Contents.swift deleted file mode 100644 index e5e0b2d8..00000000 --- a/Sources/AvatarView.playground/Contents.swift +++ /dev/null @@ -1,37 +0,0 @@ -import UIKit -import MessageKit -import PlaygroundSupport - -//: Discover what is possible with the Avatar Class -//Get an image -let testImage = #imageLiteral(resourceName: "NiceSelfi.jpg") - -let view = UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 200)) - -view.backgroundColor = UIColor.white - -//: Uncomment any line to see how it changes the `Avatar`. -//: By default its a circlular avatar with a gray background and initals of ? -let avatar = AvatarView() - -//: Configure any one of the initilization parameters and delete the ones you dont want to set. -//let avatar = AvatarView(size: 50, image: testImage, highlightedImage: testImage, initals: "PL", cornerRounding: 9) - -//: Throw in just an image. -//let avatar = AvatarView(image: testImage) - -//: Dont have an image just add the users initals -//let avatar = AvatarView(initals: "PL") - -//: Want rounded squares instead of circles just change the `cornderRounding`. -//let avatar = AvatarView(image: testImage, cornerRounding: 9) - -//:Change its size -//let avatar = AvatarView(size: 5) - -//let avatar = AvatarView(size: 100) - -//: Everything has a default so if you dont want to set it then you dont have to. - -//Helper method. -PlaygroundPage.current.liveView = avatar diff --git a/Sources/AvatarView.swift b/Sources/AvatarView.swift index 79c41c96..7f91caab 100644 --- a/Sources/AvatarView.swift +++ b/Sources/AvatarView.swift @@ -26,9 +26,8 @@ import Foundation open class AvatarView: UIView { // MARK: - Properties - internal var initalsLabel = UILabel() + internal var avatar: Avatar = Avatar() internal var imageView = UIImageView() - internal var initals: String = "?" // MARK: - initializers override init(frame: CGRect) { @@ -36,25 +35,35 @@ open class AvatarView: UIView { prepareView() } - convenience public init(size: CGFloat = 30, image: UIImage? = nil, highlightedImage: UIImage? = nil, initals inInitals: String = "?", cornerRounding: CGFloat? = nil) { - let frame = CGRect(x: 0, y: 0, width: size, height: size) - self.init(frame: frame) - setCorner(radius: cornerRounding) - setBackground(color: UIColor.gray) - imageView.image = image - imageView.highlightedImage = highlightedImage - initals = inInitals - prepareView() - } - convenience public init() { let frame = CGRect(x: 0, y: 0, width: 30, height: 30) self.init(frame: frame) - setBackground(color: UIColor.gray) - setCorner(radius: nil) prepareView() } + func getImageFrom(initals: String, withColor color: UIColor = UIColor.white, fontSize: CGFloat = 14) -> UIImage { + _ = UIGraphicsBeginImageContext(CGSize(width: 30, height: 30)) + //// General Declarations + let context = UIGraphicsGetCurrentContext()! + + //// Color Declarations + let white = UIColor.white + + //// Text Drawing + let textRect = CGRect(x: 5, y: 6, width: 20, height: 20) + let textStyle = NSMutableParagraphStyle() + textStyle.alignment = .center + let textFontAttributes = [NSFontAttributeName: UIFont.systemFont(ofSize: fontSize), NSForegroundColorAttributeName: white, NSParagraphStyleAttributeName: textStyle] + + let textTextHeight: CGFloat = initals.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) + context.restoreGState() + guard let renderedImage = UIGraphicsGetImageFromCurrentImageContext() else { assertionFailure("Could not create image from context"); return UIImage()} + return renderedImage + } + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -62,48 +71,27 @@ open class AvatarView: UIView { // MARK: - internal methods internal func prepareView() { - prepareInitalsLabel() - prepareImageView() - imageView.isHidden = imageView.image == nil - } - - internal func prepareInitalsLabel() { - initalsLabel.text = initals - initalsLabel.textAlignment = .center - setInitalsFont() - addSubview(initalsLabel) - initalsLabel.center = center - initalsLabel.frame = frame - } - - internal func prepareImageView() { + setBackground(color: UIColor.gray) contentMode = .scaleAspectFill layer.masksToBounds = true clipsToBounds = true addSubview(imageView) imageView.contentMode = .scaleAspectFill imageView.frame = frame + imageView.image = avatar.image ?? getImageFrom(initals: avatar.initals) + setCorner(radius: nil) } - // MARK: - Open methods + // MARK: - Open setters - open func set(image: UIImage) { - imageView.image = image - } - - open func setInitalsFont(size: CGFloat = 16, color: UIColor = .white) { - initalsLabel.font = UIFont.systemFont(ofSize: size) - initalsLabel.textColor = color + open func set(avatar: Avatar) { + imageView.image = avatar.image ?? getImageFrom(initals: avatar.initals) } open func setBackground(color: UIColor) { backgroundColor = color } - open func getImage() -> UIImage? { - return imageView.image - } - open func setCorner(radius: CGFloat?) { guard let radius = radius else { //if corner radius not set default to Circle @@ -112,4 +100,10 @@ open class AvatarView: UIView { } layer.cornerRadius = radius } + + // MARK: - Open getters + + open func getImage() -> UIImage? { + return imageView.image + } } diff --git a/Sources/MessageCollectionViewCell.swift b/Sources/MessageCollectionViewCell.swift index baf2ac95..3f816f85 100644 --- a/Sources/MessageCollectionViewCell.swift +++ b/Sources/MessageCollectionViewCell.swift @@ -29,25 +29,15 @@ open class MessageCollectionViewCell: UICollectionViewCell { // MARK: - Properties open let messageContainerView: UIView = { - let messageContainerView = UIView() messageContainerView.layer.cornerRadius = 12.0 messageContainerView.layer.masksToBounds = true return messageContainerView }() - open let avatarImageView: UIImageView = { - - let avatarImageView = UIImageView() - avatarImageView.contentMode = .scaleAspectFill - avatarImageView.backgroundColor = .lightGray - avatarImageView.layer.masksToBounds = true - avatarImageView.clipsToBounds = true - return avatarImageView - }() + open var avatarImageView: AvatarView = AvatarView() open let messageLabel: UILabel = { - let messageLabel = UILabel() messageLabel.numberOfLines = 0 messageLabel.backgroundColor = .clear diff --git a/Sources/MessagesDisplayDataSource.swift b/Sources/MessagesDisplayDataSource.swift index 067df3b6..35c92b82 100644 --- a/Sources/MessagesDisplayDataSource.swift +++ b/Sources/MessagesDisplayDataSource.swift @@ -28,7 +28,7 @@ public protocol MessagesDisplayDataSource: class, MessagesDataSource { func messageColorFor(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor - func avatarForMessage(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> AvatarView + func avatarForMessage(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> Avatar func headerForMessage(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageHeaderView? diff --git a/Sources/MessagesViewController.swift b/Sources/MessagesViewController.swift index 6e4ea2df..1c31691c 100644 --- a/Sources/MessagesViewController.swift +++ b/Sources/MessagesViewController.swift @@ -25,7 +25,7 @@ SOFTWARE. import UIKit open class MessagesViewController: UIViewController { - + // MARK: - Properties open var messagesCollectionView = MessagesCollectionView(frame: .zero, collectionViewLayout: MessagesCollectionViewFlowLayout()) @@ -172,8 +172,7 @@ extension MessagesViewController: UICollectionViewDataSource { let message = displayDataSource.messageForItem(at: indexPath, in: messagesCollectionView) let messageColor = displayDataSource.messageColorFor(message, at: indexPath, in: messagesCollectionView) let avatar = displayDataSource.avatarForMessage(message, at: indexPath, in: messagesCollectionView) - //TODO: replace this completely - cell.avatarImageView.image = avatar.getImage() + cell.avatarImageView.set(avatar: avatar) cell.messageContainerView.backgroundColor = messageColor cell.configure(with: message) From 7f83b2c0bc363290117dc2f4c36037f37f6dccdb Mon Sep 17 00:00:00 2001 From: MacmeDan Date: Mon, 7 Aug 2017 18:00:37 -0600 Subject: [PATCH 07/17] Fixed tests --- Sources/AvatarView.swift | 6 +--- Tests/AvatarViewTests.swift | 70 ++++++++++++++++--------------------- 2 files changed, 31 insertions(+), 45 deletions(-) diff --git a/Sources/AvatarView.swift b/Sources/AvatarView.swift index 7f91caab..537cc32a 100644 --- a/Sources/AvatarView.swift +++ b/Sources/AvatarView.swift @@ -43,17 +43,13 @@ open class AvatarView: UIView { func getImageFrom(initals: String, withColor color: UIColor = UIColor.white, fontSize: CGFloat = 14) -> UIImage { _ = UIGraphicsBeginImageContext(CGSize(width: 30, height: 30)) - //// General Declarations let context = UIGraphicsGetCurrentContext()! - //// Color Declarations - let white = UIColor.white - //// Text Drawing let textRect = CGRect(x: 5, y: 6, width: 20, height: 20) let textStyle = NSMutableParagraphStyle() textStyle.alignment = .center - let textFontAttributes = [NSFontAttributeName: UIFont.systemFont(ofSize: fontSize), NSForegroundColorAttributeName: white, NSParagraphStyleAttributeName: textStyle] + let textFontAttributes = [NSFontAttributeName: UIFont.systemFont(ofSize: fontSize), NSForegroundColorAttributeName: color, NSParagraphStyleAttributeName: textStyle] let textTextHeight: CGFloat = initals.boundingRect(with: CGSize(width: textRect.width, height: CGFloat.infinity), options: .usesLineFragmentOrigin, attributes: textFontAttributes, context: nil).height context.saveGState() diff --git a/Tests/AvatarViewTests.swift b/Tests/AvatarViewTests.swift index 1387f5e4..0c089d3a 100644 --- a/Tests/AvatarViewTests.swift +++ b/Tests/AvatarViewTests.swift @@ -20,61 +20,51 @@ class AvatarViewTests: XCTestCase { } func testNoParams() { - let avatar = AvatarView() - XCTAssertEqual(avatar.initals, "?") - XCTAssertEqual(avatar.initalsLabel.text, "?") - XCTAssertEqual(avatar.layer.cornerRadius, 15.0) - XCTAssertNil(avatar.imageView.image) - XCTAssertTrue(avatar.imageView.isHidden) - XCTAssertEqual(avatar.initalsLabel.textColor, UIColor.white) + let avatarView = AvatarView() + XCTAssertEqual(avatarView.avatar.initals, "?") + XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) + XCTAssertEqual(avatarView.backgroundColor, UIColor.gray) } func testWithImage() { - let avatar = AvatarView(image: UIImage()) + let avatarView = AvatarView() + let avatar = Avatar(image: UIImage()) + avatarView.set(avatar: avatar) XCTAssertEqual(avatar.initals, "?") - XCTAssertEqual(avatar.initalsLabel.text, "?") - XCTAssertEqual(avatar.layer.cornerRadius, 15.0) - XCTAssertFalse(avatar.imageView.isHidden) - XCTAssertEqual(avatar.initalsLabel.textColor, UIColor.white) - XCTAssertEqual(avatar.backgroundColor, UIColor.gray) + XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) + XCTAssertEqual(avatarView.backgroundColor, UIColor.gray) } - func testCustom() { - let avatar = AvatarView(size: 50, image: UIImage(), highlightedImage: nil, initals: "lol", cornerRounding: 6) - XCTAssertEqual(avatar.initals, "lol") - XCTAssertEqual(avatar.initalsLabel.text, "lol") - XCTAssertEqual(avatar.layer.cornerRadius, 6.0) - XCTAssertFalse(avatar.imageView.isHidden) - XCTAssertEqual(avatar.initalsLabel.textColor, UIColor.white) - XCTAssertEqual(avatar.backgroundColor, UIColor.gray) + func testInitalsOnly() { + let avatarView = AvatarView() + let avatar = Avatar(initals: "DL") + avatarView.set(avatar: avatar) + XCTAssertEqual(avatar.initals, "DL") + XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) + XCTAssertEqual(avatarView.backgroundColor, UIColor.gray) } func testSetBackground() { - let avatar = AvatarView(image: UIImage()) - XCTAssertEqual(avatar.backgroundColor, UIColor.gray) - avatar.setBackground(color: UIColor.red) - XCTAssertEqual(avatar.backgroundColor, UIColor.red) + let avatarView = AvatarView() + XCTAssertEqual(avatarView.backgroundColor, UIColor.gray) + avatarView.setBackground(color: UIColor.red) + XCTAssertEqual(avatarView.backgroundColor, UIColor.red) } func testGetImage() { let image = UIImage() - let avatar = AvatarView(image: image) - XCTAssertEqual(avatar.getImage(), image) + let avatar = Avatar(image: image) + let avatarView = AvatarView() + avatarView.set(avatar: avatar) + XCTAssertEqual(avatarView.getImage(), image) } func testRoundedCorners() { - let avatar = AvatarView(image: UIImage()) - XCTAssertEqual(avatar.layer.cornerRadius, 15.0) - avatar.setCorner(radius: 2) - XCTAssertEqual(avatar.layer.cornerRadius, 2.0) - } - - func testInitalsFont() { - let avatar = AvatarView(image: UIImage()) - XCTAssertEqual(avatar.initalsLabel.textColor, UIColor.white) - XCTAssertEqual(avatar.initalsLabel.font.pointSize, 16) - avatar.setInitalsFont(size: 20, color: UIColor.blue) - XCTAssertEqual(avatar.initalsLabel.textColor, UIColor.blue) - XCTAssertEqual(avatar.initalsLabel.font.pointSize, 20) + let avatarView = AvatarView() + let avatar = Avatar(image: UIImage()) + avatarView.set(avatar: avatar) + XCTAssertEqual(avatarView.layer.cornerRadius, 15.0) + avatarView.setCorner(radius: 2) + XCTAssertEqual(avatarView.layer.cornerRadius, 2.0) } } From 7b405e0a1491315a6ce005552c81a1f093c6b4df Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Mon, 7 Aug 2017 21:49:05 -0500 Subject: [PATCH 08/17] Fix MessageInputBar constraint bug --- Sources/MessagesViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/MessagesViewController.swift b/Sources/MessagesViewController.swift index 6e4ea2df..5dcd4687 100644 --- a/Sources/MessagesViewController.swift +++ b/Sources/MessagesViewController.swift @@ -114,7 +114,7 @@ open class MessagesViewController: UIViewController { toItem: view, attribute: .trailing, multiplier: 1, constant: 0)) view.addConstraint(NSLayoutConstraint(item: messagesCollectionView, attribute: .bottom, relatedBy: .equal, - toItem: bottomLayoutGuide, attribute: .top, multiplier: 1, constant: -48)) + toItem: bottomLayoutGuide, attribute: .top, multiplier: 1, constant: -46)) } From 7fc023c571154c45234e2082b91b2dac984f9cb5 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Mon, 7 Aug 2017 23:43:03 -0500 Subject: [PATCH 09/17] Create MessageLabel subclass --- MessageKit.xcodeproj/project.pbxproj | 4 ++ Sources/MessageCollectionViewCell.swift | 14 ++----- Sources/MessageLabel.swift | 53 +++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 Sources/MessageLabel.swift diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 7164a679..cc2b61fc 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -30,6 +30,7 @@ B074EE931F35587100ABB8C8 /* MessageHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B074EE921F35587100ABB8C8 /* MessageHeaderView.swift */; }; B074EE951F35588A00ABB8C8 /* MessageFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B074EE941F35588A00ABB8C8 /* MessageFooterView.swift */; }; B074EE971F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B074EE961F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift */; }; + B074EEA81F3971A600ABB8C8 /* MessageLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B074EEA71F3971A600ABB8C8 /* MessageLabel.swift */; }; B09643861F286C9E004D0129 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B09643851F286C9E004D0129 /* String+Extensions.swift */; }; B096438E1F2890FB004D0129 /* MessagesDisplayDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B096438D1F2890FB004D0129 /* MessagesDisplayDataSource.swift */; }; B09643901F289142004D0129 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B096438F1F289142004D0129 /* UIColor+Extensions.swift */; }; @@ -72,6 +73,7 @@ B074EE921F35587100ABB8C8 /* MessageHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHeaderView.swift; sourceTree = ""; }; B074EE941F35588A00ABB8C8 /* MessageFooterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageFooterView.swift; sourceTree = ""; }; B074EE961F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesLayoutDelegate.swift; sourceTree = ""; }; + B074EEA71F3971A600ABB8C8 /* MessageLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageLabel.swift; sourceTree = ""; }; B09643851F286C9E004D0129 /* String+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; B096438D1F2890FB004D0129 /* MessagesDisplayDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDisplayDataSource.swift; sourceTree = ""; }; B096438F1F289142004D0129 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; @@ -168,6 +170,7 @@ 171D5AB81F36712B0053DF69 /* InputTextView.swift */, B074EE921F35587100ABB8C8 /* MessageHeaderView.swift */, B074EE941F35588A00ABB8C8 /* MessageFooterView.swift */, + B074EEA71F3971A600ABB8C8 /* MessageLabel.swift */, ); name = Views; sourceTree = ""; @@ -353,6 +356,7 @@ B074EE951F35588A00ABB8C8 /* MessageFooterView.swift in Sources */, B09643901F289142004D0129 /* UIColor+Extensions.swift in Sources */, B0655A381F23EE8B00542A83 /* MessageInputBar.swift in Sources */, + B074EEA81F3971A600ABB8C8 /* MessageLabel.swift in Sources */, 372F6AEB1F36C15600B57FBD /* AvatarView.swift in Sources */, 88916B471CF0DFE600469F91 /* MessageType.swift in Sources */, B03FF9AF1F31BB1200754FE5 /* MessageCellDelegate.swift in Sources */, diff --git a/Sources/MessageCollectionViewCell.swift b/Sources/MessageCollectionViewCell.swift index 3f816f85..faf781a6 100644 --- a/Sources/MessageCollectionViewCell.swift +++ b/Sources/MessageCollectionViewCell.swift @@ -37,13 +37,7 @@ open class MessageCollectionViewCell: UICollectionViewCell { open var avatarImageView: AvatarView = AvatarView() - open let messageLabel: UILabel = { - let messageLabel = UILabel() - messageLabel.numberOfLines = 0 - messageLabel.backgroundColor = .clear - messageLabel.isOpaque = false - return messageLabel - }() + open var messageLabel: MessageLabel = MessageLabel() open weak var delegate: MessageCellDelegate? @@ -120,9 +114,9 @@ open class MessageCollectionViewCell: UICollectionViewCell { private func setMessageLabelFor(attributes: MessagesCollectionViewLayoutAttributes) { - let frame = CGRect(x: 0, y: 0, width: attributes.messageContainerSize.width, height: attributes.messageContainerSize.height) - let insetFrame = UIEdgeInsetsInsetRect(frame, attributes.messageContainerInsets) - messageLabel.frame = insetFrame + let frame = CGRect(origin: .zero, size: CGSize(width: attributes.messageContainerSize.width, height: attributes.messageContainerSize.height)) + messageLabel.frame = frame + messageLabel.textInsets = attributes.messageContainerInsets } diff --git a/Sources/MessageLabel.swift b/Sources/MessageLabel.swift new file mode 100644 index 00000000..f46c5284 --- /dev/null +++ b/Sources/MessageLabel.swift @@ -0,0 +1,53 @@ +/* + MIT License + + Copyright (c) 2017 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +open class MessageLabel: UILabel { + + // MARK: - Properties + + open var textInsets: UIEdgeInsets = .zero { + didSet { + guard textInsets != oldValue else { return } + setNeedsDisplay() + } + } + + public override init(frame: CGRect) { + super.init(frame: frame) + numberOfLines = 0 + } + + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Methods + + open override func draw(_ rect: CGRect) { + super.drawText(in: UIEdgeInsetsInsetRect(rect, textInsets)) + } + +} From f911cf3c392d334fd90b20107f5a49e69e4b2697 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 8 Aug 2017 03:10:07 -0500 Subject: [PATCH 10/17] Add cell top + bottom labels and refactor layout engine --- Sources/MessageCollectionViewCell.swift | 102 ++++--- .../MessagesCollectionViewFlowLayout.swift | 248 +++++++++++++----- ...ssagesCollectionViewLayoutAttributes.swift | 42 ++- Sources/MessagesDisplayDataSource.swift | 12 + Sources/MessagesViewController.swift | 2 +- 5 files changed, 287 insertions(+), 119 deletions(-) diff --git a/Sources/MessageCollectionViewCell.swift b/Sources/MessageCollectionViewCell.swift index faf781a6..a41bfc7e 100644 --- a/Sources/MessageCollectionViewCell.swift +++ b/Sources/MessageCollectionViewCell.swift @@ -35,10 +35,14 @@ open class MessageCollectionViewCell: UICollectionViewCell { return messageContainerView }() - open var avatarImageView: AvatarView = AvatarView() + open var avatarView: AvatarView = AvatarView() + + open var cellTopLabel: MessageLabel = MessageLabel() open var messageLabel: MessageLabel = MessageLabel() + open var cellBottomLabel: MessageLabel = MessageLabel() + open weak var delegate: MessageCellDelegate? // MARK: - Initializer @@ -59,7 +63,7 @@ open class MessageCollectionViewCell: UICollectionViewCell { private func setupSubviews() { contentView.addSubview(messageContainerView) - contentView.addSubview(avatarImageView) + contentView.addSubview(avatarView) messageContainerView.addSubview(messageLabel) } @@ -69,54 +73,80 @@ open class MessageCollectionViewCell: UICollectionViewCell { guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return } - messageLabel.font = attributes.messageFont + cellTopLabel.font = attributes.cellTopLabelFont + cellTopLabel.frame = cellTopLabelFrame(for: attributes) + //cellTopLabel.textInsets = attributes.cellTopLabelInsets + + messageContainerView.frame = messageContainerFrame(for: attributes) + messageLabel.frame = CGRect(origin: .zero, size: attributes.messageContainerSize) + messageLabel.textInsets = attributes.messageLabelInsets + + avatarView.frame = avatarViewFrame(for: attributes) + + cellBottomLabel.font = attributes.cellBottomLabelFont + cellBottomLabel.frame = cellTopLabelFrame(for: attributes) + //cellBottomLabel.textInsets = attributes.cellBottomLabelInsets + - setAvatarFrameFor(attributes: attributes) - setMessageContainerFrameFor(attributes: attributes) - setMessageLabelFor(attributes: attributes) } - private func setMessageContainerFrameFor(attributes: MessagesCollectionViewLayoutAttributes) { + func cellTopLabelFrame(for attributes: MessagesCollectionViewLayoutAttributes) -> CGRect { - switch attributes.direction { - case .incoming: - let x = attributes.avatarSize.width + attributes.avatarContainerSpacing - messageContainerView.frame = CGRect(x: x, - y: 0, - width: attributes.messageContainerSize.width, - height: attributes.messageContainerSize.height) - case .outgoing: - let x = contentView.frame.width - attributes.avatarSize.width - attributes.avatarContainerSpacing - attributes.messageContainerSize.width - messageContainerView.frame = CGRect(x: x, - y: 0, - width: attributes.messageContainerSize.width, - height: attributes.messageContainerSize.height) + var origin: CGPoint = .zero + + if attributes.topLabelBeginsAfterAvatar { + origin = CGPoint(x: 0, y: attributes.avatarSize.width) } + return CGRect(origin: origin, size: attributes.cellTopLabelSize) } - private func setAvatarFrameFor(attributes: MessagesCollectionViewLayoutAttributes) { + func cellBottomLabelFrame(for attributes: MessagesCollectionViewLayoutAttributes) -> CGRect { - switch attributes.direction { - case .incoming: - let y = frame.height - attributes.avatarSize.height - attributes.avatarBottomSpacing - avatarImageView.frame = CGRect(x: 0, y: y, width: attributes.avatarSize.width, height: attributes.avatarSize.height) - case .outgoing: - let y = frame.height - attributes.avatarSize.height - attributes.avatarBottomSpacing - let x = contentView.frame.width - attributes.avatarSize.width - avatarImageView.frame = CGRect(x: x, y: y, width: attributes.avatarSize.width, height: attributes.avatarSize.height) + var origin: CGPoint = .zero + + if attributes.bottomLabelBeginsAfterAvatar { + origin = CGPoint(x: 0, y: attributes.avatarSize.width) } - avatarImageView.layer.cornerRadius = avatarImageView.frame.width / 2 + return CGRect(origin: origin, size: attributes.cellBottomLabelSize) + } + + func messageContainerFrame(for attributes: MessagesCollectionViewLayoutAttributes) -> CGRect { + + var origin: CGPoint = .zero + + let yPosition = attributes.cellTopLabelSize.height + + switch attributes.direction { + case .outgoing: + let xPosition = contentView.frame.width - attributes.avatarSize.width - attributes.avatarMessagePadding - attributes.messageContainerSize.width + origin = CGPoint(x: xPosition, y: yPosition) + case .incoming: + let xPosition = attributes.avatarSize.width + attributes.avatarMessagePadding + origin = CGPoint(x: xPosition, y: yPosition) + } + + return CGRect(origin: origin, size: attributes.messageContainerSize) } - private func setMessageLabelFor(attributes: MessagesCollectionViewLayoutAttributes) { + func avatarViewFrame(for attributes: MessagesCollectionViewLayoutAttributes) -> CGRect { - let frame = CGRect(origin: .zero, size: CGSize(width: attributes.messageContainerSize.width, height: attributes.messageContainerSize.height)) - messageLabel.frame = frame - messageLabel.textInsets = attributes.messageContainerInsets + var origin: CGPoint = .zero + + let yPosition = contentView.frame.height - attributes.avatarSize.height - attributes.avatarBottomPadding + + switch attributes.direction { + case .outgoing: + let xPosition = contentView.frame.width - attributes.avatarSize.width + origin = CGPoint(x: xPosition, y: yPosition) + case .incoming: + origin = CGPoint(x: 0, y: yPosition) + } + + return CGRect(origin: origin, size: attributes.avatarSize) } @@ -132,8 +162,8 @@ open class MessageCollectionViewCell: UICollectionViewCell { func setupGestureRecognizers() { let avatarTapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapAvatar)) - avatarImageView.addGestureRecognizer(avatarTapGesture) - avatarImageView.isUserInteractionEnabled = true + avatarView.addGestureRecognizer(avatarTapGesture) + avatarView.isUserInteractionEnabled = true let messageTapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapMessage)) messageContainerView.addGestureRecognizer(messageTapGesture) diff --git a/Sources/MessagesCollectionViewFlowLayout.swift b/Sources/MessagesCollectionViewFlowLayout.swift index 7dfa3412..a6d3088f 100644 --- a/Sources/MessagesCollectionViewFlowLayout.swift +++ b/Sources/MessagesCollectionViewFlowLayout.swift @@ -28,19 +28,27 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { // MARK: - Properties - open var messageFont: UIFont + open var messageLabelFont: UIFont + open var messageLabelInsets: UIEdgeInsets + open var messageToViewEdgePadding: CGFloat + + open var cellTopLabelFont: UIFont + open var cellTopLabelInsets: UIEdgeInsets + open var topLabelBeginsAfterAvatar: Bool + + open var cellBottomLabelFont: UIFont + open var cellBottomLabelInsets: UIEdgeInsets + open var bottomLabelBeginsAfterAvatar: Bool open var incomingAvatarSize: CGSize - open var outgoingAvatarSize: CGSize - open var messageContainerInsets: UIEdgeInsets + fileprivate var avatarBottomPadding: CGFloat = 2 + fileprivate var avatarMessagePadding: CGFloat = 4 - open var messageToEdgePadding: CGFloat - - fileprivate let avatarBottomSpacing: CGFloat = 4 - - fileprivate let avatarContainerSpacing: CGFloat = 4 + fileprivate var messagesCollectionView: MessagesCollectionView? { + return collectionView as? MessagesCollectionView + } fileprivate var itemWidth: CGFloat { guard let collectionView = collectionView else { return 0 } @@ -54,12 +62,24 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { // MARK: - Initializers override public init() { - messageFont = UIFont.preferredFont(forTextStyle: .body) + + messageLabelFont = UIFont.preferredFont(forTextStyle: .body) + messageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 14) + messageToViewEdgePadding = 30.0 + + cellTopLabelFont = UIFont.preferredFont(forTextStyle: .caption2) + cellTopLabelInsets = .zero + topLabelBeginsAfterAvatar = false + + cellBottomLabelFont = UIFont.preferredFont(forTextStyle: .caption1) + cellBottomLabelInsets = .zero + bottomLabelBeginsAfterAvatar = false + incomingAvatarSize = CGSize(width: 30, height: 30) outgoingAvatarSize = CGSize(width: 30, height: 30) - messageContainerInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 14) - messageToEdgePadding = 30.0 + super.init() + sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) } @@ -96,22 +116,33 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { private func configure(attributes: MessagesCollectionViewLayoutAttributes) { - guard let collectionView = collectionView as? MessagesCollectionView, let dataSource = collectionView.messagesDataSource else { return } + guard let messagesCollectionView = messagesCollectionView else { return } + guard let dataSource = messagesCollectionView.messagesDataSource else { return } let indexPath = attributes.indexPath - let message = dataSource.messageForItem(at: indexPath, in: collectionView) - let direction: MessageDirection = dataSource.isFromCurrentSender(message: message) ? .outgoing : .incoming - let avatarSize = avatarSizeFor(message: message) - let messageContainerSize = containerSizeFor(message: message) + let message = dataSource.messageForItem(at: indexPath, in: messagesCollectionView) - attributes.direction = direction - attributes.messageFont = messageFont - attributes.messageContainerSize = messageContainerSize - attributes.messageContainerInsets = messageContainerInsets - attributes.avatarSize = avatarSize - attributes.avatarBottomSpacing = avatarBottomSpacing - attributes.avatarContainerSpacing = avatarContainerSpacing + attributes.messageContainerSize = messageContainerSize(for: message, at: indexPath) + attributes.messageLabelFont = messageLabelFont + attributes.messageLabelInsets = messageLabelInsets + attributes.messageToViewEdgePadding = messageToViewEdgePadding + + attributes.cellTopLabelSize = cellTopLabelSize(for: message, at: indexPath) + attributes.cellTopLabelFont = cellTopLabelFont + attributes.cellTopLabelInsets = cellTopLabelInsets + attributes.topLabelBeginsAfterAvatar = topLabelBeginsAfterAvatar + + attributes.cellBottomLabelSize = cellBottomLabelSize(for: message, at: indexPath) + attributes.cellBottomLabelFont = cellBottomLabelFont + attributes.cellBottomLabelInsets = cellBottomLabelInsets + attributes.bottomLabelBeginsAfterAvatar = bottomLabelBeginsAfterAvatar + + attributes.avatarSize = avatarSize(for: message) + attributes.avatarBottomPadding = avatarBottomPadding + attributes.avatarMessagePadding = avatarMessagePadding + + attributes.direction = dataSource.isFromCurrentSender(message: message) ? .outgoing : .incoming } @@ -132,91 +163,170 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { extension MessagesCollectionViewFlowLayout { - func avatarSizeFor(message: MessageType) -> CGSize { + // MARK: - Size Calculations - guard let collectionView = collectionView as? MessagesCollectionView, let dataSource = collectionView.messagesDataSource else { return .zero } + func avatarSize(for message: MessageType) -> CGSize { - return dataSource.isFromCurrentSender(message: message) ? outgoingAvatarSize : incomingAvatarSize + guard let messagesCollectionView = messagesCollectionView else { return .zero } + guard let messagesDataSource = messagesCollectionView.messagesDataSource else { return .zero } + + let isOutgoingMessage = messagesDataSource.isFromCurrentSender(message: message) + + return isOutgoingMessage ? outgoingAvatarSize : incomingAvatarSize + } + + func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + + let messageWidth = messageContainerWidth(for: message, at: indexPath) + let messageHeight = messageContainerHeight(for: message, at: indexPath) + + return CGSize(width: messageWidth, height: messageHeight) + } + + func cellTopLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + + let topLabelWidth = cellTopLabelWidth(for: message) + let topLabelHeight = cellTopLabelHeight(for: message, at: indexPath) + + return CGSize(width: topLabelWidth, height: topLabelHeight) } - func minimumCellHeightFor(message: MessageType) -> CGFloat { + func cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - guard let collectionView = collectionView as? MessagesCollectionView, let dataSource = collectionView.messagesDataSource else { return 0 } + let bottomLabelWidth = cellBottomLabelWidth(for: message) + let bottomLabelHeight = cellBottomLabelHeight(for: message, at: indexPath) - let messageDirection: MessageDirection = dataSource.isFromCurrentSender(message: message) ? .outgoing : .incoming + return CGSize(width: bottomLabelWidth, height: bottomLabelHeight) - switch messageDirection { - case .incoming: - return incomingAvatarSize.height + avatarBottomSpacing - case .outgoing: - return outgoingAvatarSize.height + avatarBottomSpacing + } + + // MARK: - Width Calculations + + + func cellTopLabelWidth(for message: MessageType) -> CGFloat { + if topLabelBeginsAfterAvatar { + return itemWidth - avatarSize(for: message).width + } else { + return itemWidth } + } + + func cellBottomLabelWidth(for message: MessageType) -> CGFloat { + if bottomLabelBeginsAfterAvatar { + return itemWidth - avatarSize(for: message).width + } else { + return itemWidth + } + } + + func availableWidthForMessageContainer(considering message: MessageType) -> CGFloat { + let avatarWidth = avatarSize(for: message).width + let avatarWidthPlusPadding = avatarWidth == 0 ? 0 : avatarWidth + avatarMessagePadding + let horizontalMessageInsets = messageLabelInsets.left + messageLabelInsets.right + let availableWidth = itemWidth - avatarWidthPlusPadding - horizontalMessageInsets - messageToViewEdgePadding + return availableWidth + } + + // MARK: - View Height Calculations + + func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + + guard let messagesCollectionView = messagesCollectionView else { return 0 } + guard let displayDataSource = messagesCollectionView.messagesDataSource as? MessagesDisplayDataSource else { return 0 } + guard let topLabelText = displayDataSource.cellTopLabelTextForMessage(message, at: indexPath) else { return 0 } + + let availableWidth = cellTopLabelWidth(for: message) + + let estimatedHeight = topLabelText.height(considering: availableWidth, and: cellTopLabelFont) + + return estimatedHeight.rounded(.up) } - func containerHeightForMessage(message: MessageType) -> CGFloat { + func cellBottomLabelHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - let avatarWidth = avatarSizeFor(message: message).width - let insets = messageContainerInsets.left + messageContainerInsets.right - let availableWidth = itemWidth - avatarWidth - avatarContainerSpacing - messageToEdgePadding - insets + guard let messagesCollectionView = messagesCollectionView else { return 0 } + guard let displayDataSource = messagesCollectionView.messagesDataSource as? MessagesDisplayDataSource else { return 0 } + guard let bottomLabelText = displayDataSource.cellBottomLabelTextForMessage(message, at: indexPath) else { return 0 } - // This is a switch because support for more messages are to come + let availableWidth = cellBottomLabelWidth(for: message) + + let estimatedHeight = bottomLabelText.height(considering: availableWidth, and: cellBottomLabelFont) + + return estimatedHeight.rounded(.up) + } + + func minimumCellHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + + let size = avatarSize(for: message) + let avatarHeightPlusBottomPadding = size.height == 0 ? 0 : size.height + avatarBottomPadding + let bottomLabelHeight = cellBottomLabelHeight(for: message, at: indexPath) + let topLabelHeight = cellTopLabelHeight(for: message, at: indexPath) + + let minimumHeight = topLabelHeight + avatarHeightPlusBottomPadding + bottomLabelHeight + + return minimumHeight + + } + + func messageContainerHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat{ + + // This is a switch because we will support other message types in the future switch message.data { case .text(let text): - let estimatedHeight = text.height(considering: availableWidth, and: messageFont) - let insets = messageContainerInsets.top + messageContainerInsets.bottom - return estimatedHeight.rounded(.up) + insets //+ 1 + let availableWidth = availableWidthForMessageContainer(considering: message) + let estimatedHeight = text.height(considering: availableWidth, and: messageLabelFont) + let verticalMessageInsets = messageLabelInsets.top + messageLabelInsets.bottom + let finalHeight = estimatedHeight.rounded(.up) + verticalMessageInsets + return finalHeight } } - func containerWidthForMessage(message: MessageType) -> CGFloat { + // MARK: - View Width Calculations - let containerHeight = containerHeightForMessage(message: message) + func messageContainerWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - let avatarWidth = avatarSizeFor(message: message).width - let insets = messageContainerInsets.left + messageContainerInsets.right - let availableWidth = itemWidth - avatarWidth - avatarContainerSpacing - messageToEdgePadding - insets - - // This is a switch because support for more messages are to come + // This is a switch because we will support other message types in the future switch message.data { case .text(let text): - let estimatedWidth = text.width(considering: containerHeight, and: messageFont).rounded(.up) - let insets = messageContainerInsets.left + messageContainerInsets.right - let finalWidth = estimatedWidth > availableWidth ? availableWidth : estimatedWidth - return finalWidth + insets + let containerHeight = messageContainerHeight(for: message, at: indexPath) + let estimatedWidth = text.width(considering: containerHeight, and: messageLabelFont) + let availableWidth = availableWidthForMessageContainer(considering: message) + let horizontalMessageInsets = messageLabelInsets.left + messageLabelInsets.right + let widthToUse = estimatedWidth.rounded(.up) > availableWidth ? availableWidth : estimatedWidth + let finalWidth = widthToUse + horizontalMessageInsets + return finalWidth } } - func estimatedCellHeightForMessage(message: MessageType) -> CGFloat { + // MARK: - Cell Size Calculations - let messageContainerHeight = containerHeightForMessage(message: message) - return messageContainerHeight + func estimatedCellHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - } + let containerHeight = messageContainerHeight(for: message, at: indexPath) + let topLabelHeight = cellTopLabelHeight(for: message, at: indexPath) + let bottomLabelheight = cellBottomLabelHeight(for: message, at: indexPath) - func containerSizeFor(message: MessageType) -> CGSize { - - let containerHeight = containerHeightForMessage(message: message) - let containerWidth = containerWidthForMessage(message: message) - - return CGSize(width: containerWidth, height: containerHeight) + return topLabelHeight + containerHeight + bottomLabelheight } func sizeForItem(at indexPath: IndexPath) -> CGSize { - guard let collectionView = collectionView as? MessagesCollectionView, let dataSource = collectionView.messagesDataSource else { return .zero } + guard let messagesCollectionView = messagesCollectionView else { return .zero } + guard let dataSource = messagesCollectionView.messagesDataSource else { return .zero } - let message = dataSource.messageForItem(at: indexPath, in: collectionView) + let message = dataSource.messageForItem(at: indexPath, in: messagesCollectionView) - let minHeight = minimumCellHeightFor(message: message) - let estimatedHeight = estimatedCellHeightForMessage(message: message) - let actualHeight = estimatedHeight < minHeight ? minHeight : estimatedHeight + let minimumHeight = minimumCellHeight(for: message, at: indexPath) + let estimatedHeight = estimatedCellHeight(for: message, at: indexPath) + let finalHeight = estimatedHeight < minimumHeight ? minimumHeight : estimatedHeight + + return CGSize(width: itemWidth, height: finalHeight) - return CGSize(width: itemWidth, height: actualHeight) } diff --git a/Sources/MessagesCollectionViewLayoutAttributes.swift b/Sources/MessagesCollectionViewLayoutAttributes.swift index 4cbfa6b4..7038f2ff 100644 --- a/Sources/MessagesCollectionViewLayoutAttributes.swift +++ b/Sources/MessagesCollectionViewLayoutAttributes.swift @@ -28,32 +28,48 @@ final class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttrib // MARK: - Properties - var direction: MessageDirection = .outgoing - - var messageFont: UIFont = UIFont.preferredFont(forTextStyle: .body) - var messageContainerSize: CGSize = .zero + var messageLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .body) + var messageLabelInsets: UIEdgeInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 14) + var messageToViewEdgePadding: CGFloat = 30.0 - var messageContainerInsets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) + var cellTopLabelSize: CGSize = .zero + var cellTopLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .caption1) + var cellTopLabelInsets: UIEdgeInsets = .zero + var topLabelBeginsAfterAvatar = false + + var cellBottomLabelSize: CGSize = .zero + var cellBottomLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .caption2) + var cellBottomLabelInsets: UIEdgeInsets = .zero + var bottomLabelBeginsAfterAvatar = false var avatarSize: CGSize = CGSize(width: 30, height: 30) + var avatarBottomPadding: CGFloat = 4.0 + var avatarMessagePadding: CGFloat = 4.0 - var avatarBottomSpacing: CGFloat = 4 - - var avatarContainerSpacing: CGFloat = 4 + var direction: MessageDirection = .incoming // MARK: - Methods override func copy(with zone: NSZone? = nil) -> Any { // swiftlint:disable force_cast let copy = super.copy(with: zone) as! MessagesCollectionViewLayoutAttributes - copy.direction = direction - copy.messageFont = messageFont copy.messageContainerSize = messageContainerSize + copy.messageLabelFont = messageLabelFont + copy.messageLabelInsets = messageLabelInsets + copy.messageToViewEdgePadding = messageToViewEdgePadding + copy.cellTopLabelSize = cellTopLabelSize + copy.cellTopLabelFont = cellTopLabelFont + copy.cellTopLabelInsets = cellTopLabelInsets + copy.topLabelBeginsAfterAvatar = topLabelBeginsAfterAvatar + copy.cellBottomLabelSize = cellBottomLabelSize + copy.cellBottomLabelFont = cellBottomLabelFont + copy.cellBottomLabelInsets = cellBottomLabelInsets + copy.bottomLabelBeginsAfterAvatar = bottomLabelBeginsAfterAvatar copy.avatarSize = avatarSize - copy.messageContainerInsets = messageContainerInsets - copy.avatarBottomSpacing = avatarBottomSpacing - copy.avatarContainerSpacing = avatarContainerSpacing + copy.avatarBottomPadding = avatarBottomPadding + copy.avatarMessagePadding = avatarMessagePadding + copy.direction = direction return copy // swiftlint:enable force_cast } diff --git a/Sources/MessagesDisplayDataSource.swift b/Sources/MessagesDisplayDataSource.swift index 35c92b82..74b15992 100644 --- a/Sources/MessagesDisplayDataSource.swift +++ b/Sources/MessagesDisplayDataSource.swift @@ -34,6 +34,10 @@ public protocol MessagesDisplayDataSource: class, MessagesDataSource { func footerForMessage(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageFooterView? + func cellTopLabelTextForMessage(_ message: MessageType, at indexPath: IndexPath) -> String? + + func cellBottomLabelTextForMessage(_ message: MessageType, at indexPath: IndexPath) -> String? + } public extension MessagesDisplayDataSource { @@ -50,4 +54,12 @@ public extension MessagesDisplayDataSource { return nil } + func cellTopLabelTextForMessage(_ message: MessageType, at indexPath: IndexPath) -> String? { + return nil + } + + func cellBottomLabelTextForMessage(_ message: MessageType, at indexPath: IndexPath) -> String? { + return nil + } + } diff --git a/Sources/MessagesViewController.swift b/Sources/MessagesViewController.swift index f432fb05..780bc10a 100644 --- a/Sources/MessagesViewController.swift +++ b/Sources/MessagesViewController.swift @@ -172,7 +172,7 @@ extension MessagesViewController: UICollectionViewDataSource { let message = displayDataSource.messageForItem(at: indexPath, in: messagesCollectionView) let messageColor = displayDataSource.messageColorFor(message, at: indexPath, in: messagesCollectionView) let avatar = displayDataSource.avatarForMessage(message, at: indexPath, in: messagesCollectionView) - cell.avatarImageView.set(avatar: avatar) + cell.avatarView.set(avatar: avatar) cell.messageContainerView.backgroundColor = messageColor cell.configure(with: message) From a1a09195a6bccc43bf18456607a291af433ee513 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Tue, 8 Aug 2017 03:58:36 -0500 Subject: [PATCH 11/17] Fix misplaced frames and update example app --- Example/Sources/AppDelegate.swift | 4 +- .../Sources/ConversationViewController.swift | 10 +++++ Example/Sources/SampleData.swift | 34 ++++++++++++---- Sources/MessageCollectionViewCell.swift | 20 +++++----- .../MessagesCollectionViewFlowLayout.swift | 12 +++--- Sources/MessagesViewController.swift | 5 +++ Tests/AvatarViewTests.swift | 30 ++++++++++---- Tests/MessageKitTests.swift | 40 +++++++++++-------- 8 files changed, 104 insertions(+), 51 deletions(-) diff --git a/Example/Sources/AppDelegate.swift b/Example/Sources/AppDelegate.swift index 4e359f93..3233a33f 100644 --- a/Example/Sources/AppDelegate.swift +++ b/Example/Sources/AppDelegate.swift @@ -29,8 +29,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - func application(_ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool { return true } + } diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index 1adf1277..e18c712e 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -74,6 +74,16 @@ extension ConversationViewController: MessagesDisplayDataSource { return messagesCollectionView.dequeueMessageFooterView(for: indexPath) } + func cellTopLabelTextForMessage(_ message: MessageType, at indexPath: IndexPath) -> String? { + return message.sender.displayName + } + + func cellBottomLabelTextForMessage(_ message: MessageType, at indexPath: IndexPath) -> String? { + let formatter = DateFormatter() + formatter.dateStyle = .medium + return formatter.string(from: message.sentDate) + } + } // MARK: - MessagesLayoutDelegate diff --git a/Example/Sources/SampleData.swift b/Example/Sources/SampleData.swift index 06fce8ff..5fbf6a95 100644 --- a/Example/Sources/SampleData.swift +++ b/Example/Sources/SampleData.swift @@ -1,10 +1,26 @@ -// -// SampleData.swift -// ChatExample -// -// Created by Dan Leonard on 8/7/17. -// Copyright © 2017 MessageKit. All rights reserved. -// +/* + 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 MessageKit @@ -21,8 +37,10 @@ struct SampleData { let msg4 = MockMessage(text: "My favorite things in life don’t cost any money. It’s really clear that the most precious resource we all have is time.", sender: Jobs, messageId: UUID().uuidString) let msg5 = MockMessage(text: "You know, this iPhone, as a matter of fact, the engine in here is made in America. And not only are the engines in here made in America, but engines are made in America and are exported. The glass on this phone is made in Kentucky. And so we've been working for years on doing more and more in the United States.", sender: Cook, messageId: UUID().uuidString) let msg6 = MockMessage(text: "I think if you do something and it turns out pretty good, then you should go do something else wonderful, not dwell on it for too long. Just figure out what’s next.", sender: Jobs, messageId: UUID().uuidString) + let msg7 = MockMessage(text: "Remembering that I'll be dead soon is the most important tool I've ever encountered to help me make the big choices in life. Because almost everything - all external expectations, all pride, all fear of embarrassment or failure - these things just fall away in the face of death, leaving only what is truly important.", sender: Jobs, messageId: UUID().uuidString) + let msg8 = MockMessage(text: "Price is rarely the most important thing. A cheap product might sell some units. Somebody gets it home and they feel great when they pay the money, but then they get it home and use it and the joy is gone.", sender: Cook, messageId: UUID().uuidString) - return [msg1, msg2, msg3, msg4, msg5, msg6] + return [msg1, msg2, msg3, msg4, msg5, msg6, msg7, msg8] } func getCurrentSender() -> Sender { diff --git a/Sources/MessageCollectionViewCell.swift b/Sources/MessageCollectionViewCell.swift index a41bfc7e..872735e5 100644 --- a/Sources/MessageCollectionViewCell.swift +++ b/Sources/MessageCollectionViewCell.swift @@ -62,9 +62,11 @@ open class MessageCollectionViewCell: UICollectionViewCell { private func setupSubviews() { + contentView.addSubview(cellTopLabel) contentView.addSubview(messageContainerView) - contentView.addSubview(avatarView) messageContainerView.addSubview(messageLabel) + contentView.addSubview(avatarView) + contentView.addSubview(cellBottomLabel) } @@ -75,7 +77,7 @@ open class MessageCollectionViewCell: UICollectionViewCell { cellTopLabel.font = attributes.cellTopLabelFont cellTopLabel.frame = cellTopLabelFrame(for: attributes) - //cellTopLabel.textInsets = attributes.cellTopLabelInsets + cellTopLabel.textInsets = attributes.cellTopLabelInsets messageContainerView.frame = messageContainerFrame(for: attributes) messageLabel.frame = CGRect(origin: .zero, size: attributes.messageContainerSize) @@ -84,10 +86,8 @@ open class MessageCollectionViewCell: UICollectionViewCell { avatarView.frame = avatarViewFrame(for: attributes) cellBottomLabel.font = attributes.cellBottomLabelFont - cellBottomLabel.frame = cellTopLabelFrame(for: attributes) - //cellBottomLabel.textInsets = attributes.cellBottomLabelInsets - - + cellBottomLabel.frame = cellBottomLabelFrame(for: attributes) + cellBottomLabel.textInsets = attributes.cellBottomLabelInsets } @@ -96,7 +96,7 @@ open class MessageCollectionViewCell: UICollectionViewCell { var origin: CGPoint = .zero if attributes.topLabelBeginsAfterAvatar { - origin = CGPoint(x: 0, y: attributes.avatarSize.width) + origin = CGPoint(x: attributes.avatarSize.width + attributes.avatarMessagePadding, y: 0) } return CGRect(origin: origin, size: attributes.cellTopLabelSize) @@ -104,10 +104,10 @@ open class MessageCollectionViewCell: UICollectionViewCell { func cellBottomLabelFrame(for attributes: MessagesCollectionViewLayoutAttributes) -> CGRect { - var origin: CGPoint = .zero + var origin: CGPoint = CGPoint(x: 0, y: contentView.frame.height - attributes.cellBottomLabelSize.height) if attributes.bottomLabelBeginsAfterAvatar { - origin = CGPoint(x: 0, y: attributes.avatarSize.width) + origin.x = attributes.avatarSize.width + attributes.avatarMessagePadding } return CGRect(origin: origin, size: attributes.cellBottomLabelSize) @@ -136,7 +136,7 @@ open class MessageCollectionViewCell: UICollectionViewCell { var origin: CGPoint = .zero - let yPosition = contentView.frame.height - attributes.avatarSize.height - attributes.avatarBottomPadding + let yPosition = contentView.frame.height - attributes.avatarSize.height - attributes.avatarBottomPadding - attributes.cellBottomLabelSize.height switch attributes.direction { case .outgoing: diff --git a/Sources/MessagesCollectionViewFlowLayout.swift b/Sources/MessagesCollectionViewFlowLayout.swift index a6d3088f..3425b4d2 100644 --- a/Sources/MessagesCollectionViewFlowLayout.swift +++ b/Sources/MessagesCollectionViewFlowLayout.swift @@ -67,13 +67,13 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { messageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 14) messageToViewEdgePadding = 30.0 - cellTopLabelFont = UIFont.preferredFont(forTextStyle: .caption2) + cellTopLabelFont = UIFont.preferredFont(forTextStyle: .caption1) cellTopLabelInsets = .zero - topLabelBeginsAfterAvatar = false + topLabelBeginsAfterAvatar = true - cellBottomLabelFont = UIFont.preferredFont(forTextStyle: .caption1) + cellBottomLabelFont = UIFont.preferredFont(forTextStyle: .caption2) cellBottomLabelInsets = .zero - bottomLabelBeginsAfterAvatar = false + bottomLabelBeginsAfterAvatar = true incomingAvatarSize = CGSize(width: 30, height: 30) outgoingAvatarSize = CGSize(width: 30, height: 30) @@ -203,7 +203,6 @@ extension MessagesCollectionViewFlowLayout { // MARK: - Width Calculations - func cellTopLabelWidth(for message: MessageType) -> CGFloat { if topLabelBeginsAfterAvatar { return itemWidth - avatarSize(for: message).width @@ -270,7 +269,7 @@ extension MessagesCollectionViewFlowLayout { } - func messageContainerHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat{ + func messageContainerHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { // This is a switch because we will support other message types in the future switch message.data { @@ -327,7 +326,6 @@ extension MessagesCollectionViewFlowLayout { return CGSize(width: itemWidth, height: finalHeight) - } } diff --git a/Sources/MessagesViewController.swift b/Sources/MessagesViewController.swift index 780bc10a..d7d0397e 100644 --- a/Sources/MessagesViewController.swift +++ b/Sources/MessagesViewController.swift @@ -172,6 +172,11 @@ extension MessagesViewController: UICollectionViewDataSource { let message = displayDataSource.messageForItem(at: indexPath, in: messagesCollectionView) let messageColor = displayDataSource.messageColorFor(message, at: indexPath, in: messagesCollectionView) let avatar = displayDataSource.avatarForMessage(message, at: indexPath, in: messagesCollectionView) + let topLabelText = displayDataSource.cellTopLabelTextForMessage(message, at: indexPath) + let bottomLabelText = displayDataSource.cellBottomLabelTextForMessage(message, at: indexPath) + + cell.cellTopLabel.text = topLabelText + cell.cellBottomLabel.text = bottomLabelText cell.avatarView.set(avatar: avatar) cell.messageContainerView.backgroundColor = messageColor cell.configure(with: message) diff --git a/Tests/AvatarViewTests.swift b/Tests/AvatarViewTests.swift index 0c089d3a..43e27ce2 100644 --- a/Tests/AvatarViewTests.swift +++ b/Tests/AvatarViewTests.swift @@ -1,10 +1,26 @@ -// -// AvatarViewTests.swift -// MessageKit -// -// Created by Dan Leonard on 8/5/17. -// Copyright © 2017 Hexed Bits. All rights reserved. -// +/* + 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 XCTest @testable import MessageKit diff --git a/Tests/MessageKitTests.swift b/Tests/MessageKitTests.swift index f4c98757..2deaacec 100644 --- a/Tests/MessageKitTests.swift +++ b/Tests/MessageKitTests.swift @@ -1,20 +1,26 @@ -// -// Created by Jesse Squires -// http://www.jessesquires.com -// -// -// Documentation -// http://messagekit.github.io -// -// -// GitHub -// https://github.com/MessageKit/MessageKit -// -// -// License -// Copyright (c) 2016-present Jesse Squires -// Released under an MIT license: http://opensource.org/licenses/MIT -// +/* + 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 XCTest From 824748126a3ed1d0d20444da9805e0a76d731cf6 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Wed, 9 Aug 2017 00:22:47 -0500 Subject: [PATCH 12/17] Update topLabel & bottomLabel constraints and add text alignment --- Sources/MessageCollectionViewCell.swift | 13 ++++++++++-- Sources/MessageInputBar.swift | 4 ++-- .../MessagesCollectionViewFlowLayout.swift | 20 +++++++++---------- ...ssagesCollectionViewLayoutAttributes.swift | 8 ++++---- Sources/MessagesViewController.swift | 17 +++++----------- 5 files changed, 32 insertions(+), 30 deletions(-) diff --git a/Sources/MessageCollectionViewCell.swift b/Sources/MessageCollectionViewCell.swift index 872735e5..6851e4df 100644 --- a/Sources/MessageCollectionViewCell.swift +++ b/Sources/MessageCollectionViewCell.swift @@ -89,13 +89,22 @@ open class MessageCollectionViewCell: UICollectionViewCell { cellBottomLabel.frame = cellBottomLabelFrame(for: attributes) cellBottomLabel.textInsets = attributes.cellBottomLabelInsets + switch attributes.direction { + case .incoming: + cellTopLabel.textAlignment = .left + cellBottomLabel.textAlignment = .right + case .outgoing: + cellTopLabel.textAlignment = .right + cellBottomLabel.textAlignment = .left + } + } func cellTopLabelFrame(for attributes: MessagesCollectionViewLayoutAttributes) -> CGRect { var origin: CGPoint = .zero - if attributes.topLabelBeginsAfterAvatar { + if attributes.topLabelPinnedUnderMessage { origin = CGPoint(x: attributes.avatarSize.width + attributes.avatarMessagePadding, y: 0) } @@ -106,7 +115,7 @@ open class MessageCollectionViewCell: UICollectionViewCell { var origin: CGPoint = CGPoint(x: 0, y: contentView.frame.height - attributes.cellBottomLabelSize.height) - if attributes.bottomLabelBeginsAfterAvatar { + if attributes.bottomLabelPinnedUnderMessage { origin.x = attributes.avatarSize.width + attributes.avatarMessagePadding } diff --git a/Sources/MessageInputBar.swift b/Sources/MessageInputBar.swift index cc251ab6..f3001cf8 100644 --- a/Sources/MessageInputBar.swift +++ b/Sources/MessageInputBar.swift @@ -103,8 +103,8 @@ open class MessageInputBar: UIView, UITextViewDelegate { override open var intrinsicContentSize: CGSize { let sizeToFit = inputTextView.sizeThatFits(CGSize(width: inputTextView.bounds.width, height: .greatestFiniteMagnitude)) - let heightToFit = sizeToFit.height.rounded() - return CGSize(width: bounds.width, height: heightToFit + 8) + let heightToFit = sizeToFit.height.rounded() + 8 // constraint padding + return CGSize(width: bounds.width, height: heightToFit) } private func setupSubviews() { diff --git a/Sources/MessagesCollectionViewFlowLayout.swift b/Sources/MessagesCollectionViewFlowLayout.swift index 3425b4d2..63c0a93b 100644 --- a/Sources/MessagesCollectionViewFlowLayout.swift +++ b/Sources/MessagesCollectionViewFlowLayout.swift @@ -34,11 +34,11 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { open var cellTopLabelFont: UIFont open var cellTopLabelInsets: UIEdgeInsets - open var topLabelBeginsAfterAvatar: Bool + open var topLabelPinnedUnderMessage: Bool open var cellBottomLabelFont: UIFont open var cellBottomLabelInsets: UIEdgeInsets - open var bottomLabelBeginsAfterAvatar: Bool + open var bottomLabelPinnedUnderMessage: Bool open var incomingAvatarSize: CGSize open var outgoingAvatarSize: CGSize @@ -69,11 +69,11 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { cellTopLabelFont = UIFont.preferredFont(forTextStyle: .caption1) cellTopLabelInsets = .zero - topLabelBeginsAfterAvatar = true + topLabelPinnedUnderMessage = true cellBottomLabelFont = UIFont.preferredFont(forTextStyle: .caption2) cellBottomLabelInsets = .zero - bottomLabelBeginsAfterAvatar = true + bottomLabelPinnedUnderMessage = true incomingAvatarSize = CGSize(width: 30, height: 30) outgoingAvatarSize = CGSize(width: 30, height: 30) @@ -131,12 +131,12 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { attributes.cellTopLabelSize = cellTopLabelSize(for: message, at: indexPath) attributes.cellTopLabelFont = cellTopLabelFont attributes.cellTopLabelInsets = cellTopLabelInsets - attributes.topLabelBeginsAfterAvatar = topLabelBeginsAfterAvatar + attributes.topLabelPinnedUnderMessage = topLabelPinnedUnderMessage attributes.cellBottomLabelSize = cellBottomLabelSize(for: message, at: indexPath) attributes.cellBottomLabelFont = cellBottomLabelFont attributes.cellBottomLabelInsets = cellBottomLabelInsets - attributes.bottomLabelBeginsAfterAvatar = bottomLabelBeginsAfterAvatar + attributes.bottomLabelPinnedUnderMessage = bottomLabelPinnedUnderMessage attributes.avatarSize = avatarSize(for: message) attributes.avatarBottomPadding = avatarBottomPadding @@ -204,16 +204,16 @@ extension MessagesCollectionViewFlowLayout { // MARK: - Width Calculations func cellTopLabelWidth(for message: MessageType) -> CGFloat { - if topLabelBeginsAfterAvatar { - return itemWidth - avatarSize(for: message).width + if topLabelPinnedUnderMessage { + return itemWidth - avatarSize(for: message).width - avatarMessagePadding - messageToViewEdgePadding } else { return itemWidth } } func cellBottomLabelWidth(for message: MessageType) -> CGFloat { - if bottomLabelBeginsAfterAvatar { - return itemWidth - avatarSize(for: message).width + if bottomLabelPinnedUnderMessage { + return itemWidth - avatarSize(for: message).width - avatarMessagePadding - messageToViewEdgePadding } else { return itemWidth } diff --git a/Sources/MessagesCollectionViewLayoutAttributes.swift b/Sources/MessagesCollectionViewLayoutAttributes.swift index 7038f2ff..56e196b2 100644 --- a/Sources/MessagesCollectionViewLayoutAttributes.swift +++ b/Sources/MessagesCollectionViewLayoutAttributes.swift @@ -36,12 +36,12 @@ final class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttrib var cellTopLabelSize: CGSize = .zero var cellTopLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .caption1) var cellTopLabelInsets: UIEdgeInsets = .zero - var topLabelBeginsAfterAvatar = false + var topLabelPinnedUnderMessage = true var cellBottomLabelSize: CGSize = .zero var cellBottomLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .caption2) var cellBottomLabelInsets: UIEdgeInsets = .zero - var bottomLabelBeginsAfterAvatar = false + var bottomLabelPinnedUnderMessage = true var avatarSize: CGSize = CGSize(width: 30, height: 30) var avatarBottomPadding: CGFloat = 4.0 @@ -61,11 +61,11 @@ final class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttrib copy.cellTopLabelSize = cellTopLabelSize copy.cellTopLabelFont = cellTopLabelFont copy.cellTopLabelInsets = cellTopLabelInsets - copy.topLabelBeginsAfterAvatar = topLabelBeginsAfterAvatar + copy.topLabelPinnedUnderMessage = topLabelPinnedUnderMessage copy.cellBottomLabelSize = cellBottomLabelSize copy.cellBottomLabelFont = cellBottomLabelFont copy.cellBottomLabelInsets = cellBottomLabelInsets - copy.bottomLabelBeginsAfterAvatar = bottomLabelBeginsAfterAvatar + copy.bottomLabelPinnedUnderMessage = bottomLabelPinnedUnderMessage copy.avatarSize = avatarSize copy.avatarBottomPadding = avatarBottomPadding copy.avatarMessagePadding = avatarMessagePadding diff --git a/Sources/MessagesViewController.swift b/Sources/MessagesViewController.swift index d7d0397e..8a748244 100644 --- a/Sources/MessagesViewController.swift +++ b/Sources/MessagesViewController.swift @@ -65,15 +65,14 @@ open class MessagesViewController: UIViewController { open override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - // depends on inputAccessoryView frame thus must be called here addKeyboardObservers() - messagesCollectionView.scrollToBottom(animated: false) } - open override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - removeKeyboardObservers() - } + // MARK: - Initializers + + deinit { + removeKeyboardObservers() + } // MARK: - Methods @@ -133,8 +132,6 @@ extension MessagesViewController: UICollectionViewDelegateFlowLayout { } -//swiftlint:enable line_length - // MARK: - UICollectionViewDataSource Conformance extension MessagesViewController: UICollectionViewDataSource { @@ -155,8 +152,6 @@ extension MessagesViewController: UICollectionViewDataSource { } - //swiftlint:disable line_length - public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MessageCell", for: indexPath) as? MessageCollectionViewCell ?? MessageCollectionViewCell() @@ -223,8 +218,6 @@ extension MessagesViewController: UICollectionViewDataSource { return messagesLayoutDelegate.footerSizeFor(message, at: indexPath, in: messagesCollectionView) } - //swiftlint:enable line_length - } // MARK: - Keyboard Handling From 0aa724ee4f08f674a7abe62f6119c44318145d99 Mon Sep 17 00:00:00 2001 From: Andrea Antonioni Date: Wed, 9 Aug 2017 09:05:42 +0200 Subject: [PATCH 13/17] Fix #41 --- Sources/InputTextView.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sources/InputTextView.swift b/Sources/InputTextView.swift index ddf7954d..55ae79ca 100644 --- a/Sources/InputTextView.swift +++ b/Sources/InputTextView.swift @@ -100,6 +100,10 @@ open class InputTextView: UITextView { selector: #selector(textDidChange), name: Notification.Name.UITextViewTextDidChange, object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(self.orientationChanged(notification:)), + name: Notification.Name.UIDeviceOrientationDidChange, + object: nil) } func textDidChange(notification: Notification) { @@ -107,5 +111,9 @@ open class InputTextView: UITextView { setNeedsDisplay() isPlaceholderVisibile = false } + + func orientationChanged(notification: Notification) { + setNeedsDisplay() + } } From eaa3dd15c7ad7b7c4517a5764e2b06657328e74e Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Wed, 9 Aug 2017 20:47:45 -0500 Subject: [PATCH 14/17] Update cellTopLabel and cellBottomLabel to use NSAttributedString --- .../Sources/ConversationViewController.swift | 10 +++-- MessageKit.xcodeproj/project.pbxproj | 4 ++ Sources/MessageCollectionViewCell.swift | 2 - .../MessagesCollectionViewFlowLayout.swift | 14 ++---- ...ssagesCollectionViewLayoutAttributes.swift | 4 -- Sources/MessagesDisplayDataSource.swift | 8 ++-- Sources/MessagesViewController.swift | 11 ++--- Sources/NSAttributedString+Extensions.swift | 45 +++++++++++++++++++ 8 files changed, 69 insertions(+), 29 deletions(-) create mode 100644 Sources/NSAttributedString+Extensions.swift diff --git a/Example/Sources/ConversationViewController.swift b/Example/Sources/ConversationViewController.swift index e18c712e..e4453d26 100644 --- a/Example/Sources/ConversationViewController.swift +++ b/Example/Sources/ConversationViewController.swift @@ -74,14 +74,16 @@ extension ConversationViewController: MessagesDisplayDataSource { return messagesCollectionView.dequeueMessageFooterView(for: indexPath) } - func cellTopLabelTextForMessage(_ message: MessageType, at indexPath: IndexPath) -> String? { - return message.sender.displayName + func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + let name = message.sender.displayName + return NSAttributedString(string: name, attributes: [NSFontAttributeName: UIFont.preferredFont(forTextStyle: .caption1)]) } - func cellBottomLabelTextForMessage(_ message: MessageType, at indexPath: IndexPath) -> String? { + func cellBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { let formatter = DateFormatter() formatter.dateStyle = .medium - return formatter.string(from: message.sentDate) + let dateString = formatter.string(from: message.sentDate) + return NSAttributedString(string: dateString, attributes: [NSFontAttributeName: UIFont.preferredFont(forTextStyle: .caption2)]) } } diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index cc2b61fc..42f3fefe 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -31,6 +31,7 @@ B074EE951F35588A00ABB8C8 /* MessageFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B074EE941F35588A00ABB8C8 /* MessageFooterView.swift */; }; B074EE971F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B074EE961F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift */; }; B074EEA81F3971A600ABB8C8 /* MessageLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B074EEA71F3971A600ABB8C8 /* MessageLabel.swift */; }; + B074EEAA1F3BE8F500ABB8C8 /* NSAttributedString+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B074EEA91F3BE8F300ABB8C8 /* NSAttributedString+Extensions.swift */; }; B09643861F286C9E004D0129 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B09643851F286C9E004D0129 /* String+Extensions.swift */; }; B096438E1F2890FB004D0129 /* MessagesDisplayDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B096438D1F2890FB004D0129 /* MessagesDisplayDataSource.swift */; }; B09643901F289142004D0129 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B096438F1F289142004D0129 /* UIColor+Extensions.swift */; }; @@ -74,6 +75,7 @@ B074EE941F35588A00ABB8C8 /* MessageFooterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageFooterView.swift; sourceTree = ""; }; B074EE961F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesLayoutDelegate.swift; sourceTree = ""; }; B074EEA71F3971A600ABB8C8 /* MessageLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageLabel.swift; sourceTree = ""; }; + B074EEA91F3BE8F300ABB8C8 /* NSAttributedString+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Extensions.swift"; sourceTree = ""; }; B09643851F286C9E004D0129 /* String+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; B096438D1F2890FB004D0129 /* MessagesDisplayDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesDisplayDataSource.swift; sourceTree = ""; }; B096438F1F289142004D0129 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; @@ -145,6 +147,7 @@ children = ( B09643851F286C9E004D0129 /* String+Extensions.swift */, B096438F1F289142004D0129 /* UIColor+Extensions.swift */, + B074EEA91F3BE8F300ABB8C8 /* NSAttributedString+Extensions.swift */, ); name = Extensions; sourceTree = ""; @@ -341,6 +344,7 @@ buildActionMask = 2147483647; files = ( 171D5AB91F36712B0053DF69 /* InputTextView.swift in Sources */, + B074EEAA1F3BE8F500ABB8C8 /* NSAttributedString+Extensions.swift in Sources */, B074EE971F355FBC00ABB8C8 /* MessagesLayoutDelegate.swift in Sources */, 882D75841DE507320033F95F /* MessagesDataSource.swift in Sources */, 888CEBFC1D3FD525005178DE /* MessagesViewController.swift in Sources */, diff --git a/Sources/MessageCollectionViewCell.swift b/Sources/MessageCollectionViewCell.swift index 6851e4df..2b97ea81 100644 --- a/Sources/MessageCollectionViewCell.swift +++ b/Sources/MessageCollectionViewCell.swift @@ -75,7 +75,6 @@ open class MessageCollectionViewCell: UICollectionViewCell { guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return } - cellTopLabel.font = attributes.cellTopLabelFont cellTopLabel.frame = cellTopLabelFrame(for: attributes) cellTopLabel.textInsets = attributes.cellTopLabelInsets @@ -85,7 +84,6 @@ open class MessageCollectionViewCell: UICollectionViewCell { avatarView.frame = avatarViewFrame(for: attributes) - cellBottomLabel.font = attributes.cellBottomLabelFont cellBottomLabel.frame = cellBottomLabelFrame(for: attributes) cellBottomLabel.textInsets = attributes.cellBottomLabelInsets diff --git a/Sources/MessagesCollectionViewFlowLayout.swift b/Sources/MessagesCollectionViewFlowLayout.swift index 63c0a93b..bfeebc99 100644 --- a/Sources/MessagesCollectionViewFlowLayout.swift +++ b/Sources/MessagesCollectionViewFlowLayout.swift @@ -32,11 +32,9 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { open var messageLabelInsets: UIEdgeInsets open var messageToViewEdgePadding: CGFloat - open var cellTopLabelFont: UIFont open var cellTopLabelInsets: UIEdgeInsets open var topLabelPinnedUnderMessage: Bool - open var cellBottomLabelFont: UIFont open var cellBottomLabelInsets: UIEdgeInsets open var bottomLabelPinnedUnderMessage: Bool @@ -67,11 +65,9 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { messageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 14) messageToViewEdgePadding = 30.0 - cellTopLabelFont = UIFont.preferredFont(forTextStyle: .caption1) cellTopLabelInsets = .zero topLabelPinnedUnderMessage = true - cellBottomLabelFont = UIFont.preferredFont(forTextStyle: .caption2) cellBottomLabelInsets = .zero bottomLabelPinnedUnderMessage = true @@ -129,12 +125,10 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { attributes.messageToViewEdgePadding = messageToViewEdgePadding attributes.cellTopLabelSize = cellTopLabelSize(for: message, at: indexPath) - attributes.cellTopLabelFont = cellTopLabelFont attributes.cellTopLabelInsets = cellTopLabelInsets attributes.topLabelPinnedUnderMessage = topLabelPinnedUnderMessage attributes.cellBottomLabelSize = cellBottomLabelSize(for: message, at: indexPath) - attributes.cellBottomLabelFont = cellBottomLabelFont attributes.cellBottomLabelInsets = cellBottomLabelInsets attributes.bottomLabelPinnedUnderMessage = bottomLabelPinnedUnderMessage @@ -233,11 +227,11 @@ extension MessagesCollectionViewFlowLayout { guard let messagesCollectionView = messagesCollectionView else { return 0 } guard let displayDataSource = messagesCollectionView.messagesDataSource as? MessagesDisplayDataSource else { return 0 } - guard let topLabelText = displayDataSource.cellTopLabelTextForMessage(message, at: indexPath) else { return 0 } + guard let topLabelText = displayDataSource.cellTopLabelAttributedText(for: message, at: indexPath) else { return 0 } let availableWidth = cellTopLabelWidth(for: message) - let estimatedHeight = topLabelText.height(considering: availableWidth, and: cellTopLabelFont) + let estimatedHeight = topLabelText.height(considering: availableWidth) return estimatedHeight.rounded(.up) @@ -247,11 +241,11 @@ extension MessagesCollectionViewFlowLayout { guard let messagesCollectionView = messagesCollectionView else { return 0 } guard let displayDataSource = messagesCollectionView.messagesDataSource as? MessagesDisplayDataSource else { return 0 } - guard let bottomLabelText = displayDataSource.cellBottomLabelTextForMessage(message, at: indexPath) else { return 0 } + guard let bottomLabelText = displayDataSource.cellBottomLabelAttributedText(for: message, at: indexPath) else { return 0 } let availableWidth = cellBottomLabelWidth(for: message) - let estimatedHeight = bottomLabelText.height(considering: availableWidth, and: cellBottomLabelFont) + let estimatedHeight = bottomLabelText.height(considering: availableWidth) return estimatedHeight.rounded(.up) } diff --git a/Sources/MessagesCollectionViewLayoutAttributes.swift b/Sources/MessagesCollectionViewLayoutAttributes.swift index 56e196b2..f7b2628c 100644 --- a/Sources/MessagesCollectionViewLayoutAttributes.swift +++ b/Sources/MessagesCollectionViewLayoutAttributes.swift @@ -34,12 +34,10 @@ final class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttrib var messageToViewEdgePadding: CGFloat = 30.0 var cellTopLabelSize: CGSize = .zero - var cellTopLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .caption1) var cellTopLabelInsets: UIEdgeInsets = .zero var topLabelPinnedUnderMessage = true var cellBottomLabelSize: CGSize = .zero - var cellBottomLabelFont: UIFont = UIFont.preferredFont(forTextStyle: .caption2) var cellBottomLabelInsets: UIEdgeInsets = .zero var bottomLabelPinnedUnderMessage = true @@ -59,11 +57,9 @@ final class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttrib copy.messageLabelInsets = messageLabelInsets copy.messageToViewEdgePadding = messageToViewEdgePadding copy.cellTopLabelSize = cellTopLabelSize - copy.cellTopLabelFont = cellTopLabelFont copy.cellTopLabelInsets = cellTopLabelInsets copy.topLabelPinnedUnderMessage = topLabelPinnedUnderMessage copy.cellBottomLabelSize = cellBottomLabelSize - copy.cellBottomLabelFont = cellBottomLabelFont copy.cellBottomLabelInsets = cellBottomLabelInsets copy.bottomLabelPinnedUnderMessage = bottomLabelPinnedUnderMessage copy.avatarSize = avatarSize diff --git a/Sources/MessagesDisplayDataSource.swift b/Sources/MessagesDisplayDataSource.swift index 74b15992..cdc33650 100644 --- a/Sources/MessagesDisplayDataSource.swift +++ b/Sources/MessagesDisplayDataSource.swift @@ -34,9 +34,9 @@ public protocol MessagesDisplayDataSource: class, MessagesDataSource { func footerForMessage(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageFooterView? - func cellTopLabelTextForMessage(_ message: MessageType, at indexPath: IndexPath) -> String? + func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? - func cellBottomLabelTextForMessage(_ message: MessageType, at indexPath: IndexPath) -> String? + func cellBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? } @@ -54,11 +54,11 @@ public extension MessagesDisplayDataSource { return nil } - func cellTopLabelTextForMessage(_ message: MessageType, at indexPath: IndexPath) -> String? { + func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { return nil } - func cellBottomLabelTextForMessage(_ message: MessageType, at indexPath: IndexPath) -> String? { + func cellBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { return nil } diff --git a/Sources/MessagesViewController.swift b/Sources/MessagesViewController.swift index 8a748244..5ad5c1ad 100644 --- a/Sources/MessagesViewController.swift +++ b/Sources/MessagesViewController.swift @@ -167,11 +167,11 @@ extension MessagesViewController: UICollectionViewDataSource { let message = displayDataSource.messageForItem(at: indexPath, in: messagesCollectionView) let messageColor = displayDataSource.messageColorFor(message, at: indexPath, in: messagesCollectionView) let avatar = displayDataSource.avatarForMessage(message, at: indexPath, in: messagesCollectionView) - let topLabelText = displayDataSource.cellTopLabelTextForMessage(message, at: indexPath) - let bottomLabelText = displayDataSource.cellBottomLabelTextForMessage(message, at: indexPath) + let topLabelText = displayDataSource.cellTopLabelAttributedText(for: message, at: indexPath) + let bottomLabelText = displayDataSource.cellBottomLabelAttributedText(for: message, at: indexPath) - cell.cellTopLabel.text = topLabelText - cell.cellBottomLabel.text = bottomLabelText + cell.cellTopLabel.attributedText = topLabelText + cell.cellBottomLabel.attributedText = bottomLabelText cell.avatarView.set(avatar: avatar) cell.messageContainerView.backgroundColor = messageColor cell.configure(with: message) @@ -249,7 +249,8 @@ extension MessagesViewController { let keyboardRect = keyboardSizeValue.cgRectValue let messageInputBarHeight = inputAccessoryView?.bounds.size.height ?? 0 - let keyboardHeight = keyboardRect.height - messageInputBarHeight + let keyboardHeight = keyboardRect.height - messageInputBarHeight + print(keyboardHeight) messagesCollectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0) } diff --git a/Sources/NSAttributedString+Extensions.swift b/Sources/NSAttributedString+Extensions.swift new file mode 100644 index 00000000..03fddefd --- /dev/null +++ b/Sources/NSAttributedString+Extensions.swift @@ -0,0 +1,45 @@ +/* + MIT License + + Copyright (c) 2017 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import Foundation + +extension NSAttributedString { + + func height(considering width: CGFloat) -> CGFloat { + + let constraintBox = CGSize(width: width, height: .greatestFiniteMagnitude) + let rect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, context: nil) + return rect.height + + } + + func width(considering height: CGFloat, and font: UIFont) -> CGFloat { + + let constraintBox = CGSize(width: .greatestFiniteMagnitude, height: height) + let rect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, context: nil) + return rect.width + + } + +} From 8f1aab0374485465c3b9390823647cd576477d5f Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Wed, 9 Aug 2017 20:56:58 -0500 Subject: [PATCH 15/17] Provide support for NSAttributedString in MessageLabel --- Sources/MessageCollectionViewCell.swift | 2 + Sources/MessageData.swift | 3 +- .../MessagesCollectionViewFlowLayout.swift | 39 ++++++++++++------- Sources/NSAttributedString+Extensions.swift | 2 +- 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/Sources/MessageCollectionViewCell.swift b/Sources/MessageCollectionViewCell.swift index 2b97ea81..8b8f8fcc 100644 --- a/Sources/MessageCollectionViewCell.swift +++ b/Sources/MessageCollectionViewCell.swift @@ -162,6 +162,8 @@ open class MessageCollectionViewCell: UICollectionViewCell { switch message.data { case .text(let text): messageLabel.text = text + case .attributedText(let text): + messageLabel.attributedText = text } } diff --git a/Sources/MessageData.swift b/Sources/MessageData.swift index 86130944..4aa448c2 100644 --- a/Sources/MessageData.swift +++ b/Sources/MessageData.swift @@ -28,11 +28,10 @@ import Foundation public enum MessageData { case text(String) + case attributedText(NSAttributedString) // MARK: - Not supported yet -// case attributedText(NSAttributedString) -// // case audio(Data) // // case location(CLLocation) diff --git a/Sources/MessagesCollectionViewFlowLayout.swift b/Sources/MessagesCollectionViewFlowLayout.swift index bfeebc99..3f2c85e6 100644 --- a/Sources/MessagesCollectionViewFlowLayout.swift +++ b/Sources/MessagesCollectionViewFlowLayout.swift @@ -265,34 +265,45 @@ extension MessagesCollectionViewFlowLayout { func messageContainerHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - // This is a switch because we will support other message types in the future + let availableWidth = availableWidthForMessageContainer(considering: message) + let verticalMessageInsets = messageLabelInsets.top + messageLabelInsets.bottom + var estimatedHeight: CGFloat = 0 + switch message.data { case .text(let text): - let availableWidth = availableWidthForMessageContainer(considering: message) - let estimatedHeight = text.height(considering: availableWidth, and: messageLabelFont) - let verticalMessageInsets = messageLabelInsets.top + messageLabelInsets.bottom - let finalHeight = estimatedHeight.rounded(.up) + verticalMessageInsets - return finalHeight + estimatedHeight = text.height(considering: availableWidth, and: messageLabelFont) + case .attributedText(let text): + estimatedHeight = text.height(considering: availableWidth) } + let finalHeight = estimatedHeight.rounded(.up) + verticalMessageInsets + + return finalHeight + } // MARK: - View Width Calculations func messageContainerWidth(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - // This is a switch because we will support other message types in the future + let containerHeight = messageContainerHeight(for: message, at: indexPath) + let availableWidth = availableWidthForMessageContainer(considering: message) + let horizontalMessageInsets = messageLabelInsets.left + messageLabelInsets.right + var estimatedWidth: CGFloat = 0 + switch message.data { case .text(let text): - let containerHeight = messageContainerHeight(for: message, at: indexPath) - let estimatedWidth = text.width(considering: containerHeight, and: messageLabelFont) - let availableWidth = availableWidthForMessageContainer(considering: message) - let horizontalMessageInsets = messageLabelInsets.left + messageLabelInsets.right - let widthToUse = estimatedWidth.rounded(.up) > availableWidth ? availableWidth : estimatedWidth - let finalWidth = widthToUse + horizontalMessageInsets - return finalWidth + estimatedWidth = text.width(considering: containerHeight, and: messageLabelFont) + case .attributedText(let text): + estimatedWidth = text.width(considering: containerHeight) } + let widthToUse = estimatedWidth.rounded(.up) > availableWidth ? availableWidth : estimatedWidth + + let finalWidth = widthToUse + horizontalMessageInsets + + return finalWidth + } // MARK: - Cell Size Calculations diff --git a/Sources/NSAttributedString+Extensions.swift b/Sources/NSAttributedString+Extensions.swift index 03fddefd..316d9540 100644 --- a/Sources/NSAttributedString+Extensions.swift +++ b/Sources/NSAttributedString+Extensions.swift @@ -34,7 +34,7 @@ extension NSAttributedString { } - func width(considering height: CGFloat, and font: UIFont) -> CGFloat { + func width(considering height: CGFloat) -> CGFloat { let constraintBox = CGSize(width: .greatestFiniteMagnitude, height: height) let rect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, context: nil) From 2f17535beee7ede6249da539605d7f5e2c341068 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Wed, 9 Aug 2017 21:38:50 -0500 Subject: [PATCH 16/17] Update CHANGELOG to only log changes after first non-prerelease version --- CHANGELOG.md | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b9cfba3..70178dbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,24 +7,14 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa ## Upcoming release ---------------- +## [Prerelease] 0.3.0 + +This release closes the [0.3 milestone](https://github.com/MessageKit/MessageKit/milestone/3?closed=1). + ## [Prerelease] 0.2.0 This release closes the [0.2 milestone](https://github.com/MessageKit/MessageKit/milestone/2?closed=1). -### API Breaking - -- `MessagesDataSource` & `MessagesDisplayDataSource` collectionView params are now typed as `MessagesCollectionView`. - -### Enahancements - -- Resizing `UITextView` in `MessageInputBar`. -- Adds basic support for `MessageHeaderView` and `MessageFooterView`. -- Adds `MessageCellDelegate` to handle tap events on message container or avatar. - -### Bugfixes - -- Fixes layout for `Landscape` orientation. - ## [Prerelease] 0.1.0 This release closes the [0.1 milestone](https://github.com/MessageKit/MessageKit/milestone/1?closed=1). From 01776bfb33627710cc95d95248888fdcbd7ea045 Mon Sep 17 00:00:00 2001 From: Steven Deutsch Date: Wed, 9 Aug 2017 21:39:06 -0500 Subject: [PATCH 17/17] Update MessageKit.podspec for v0.3.0 --- MessageKit.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MessageKit.podspec b/MessageKit.podspec index 11882e6f..722e6954 100644 --- a/MessageKit.podspec +++ b/MessageKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'MessageKit' - s.version = '0.2.0' + s.version = '0.3.0' s.license = { :type => "MIT", :file => "LICENSE.md" } s.summary = 'An elegant messages UI library for iOS.'