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). 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/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/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..e4453d26 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? { @@ -124,6 +74,18 @@ extension ConversationViewController: MessagesDisplayDataSource { return messagesCollectionView.dequeueMessageFooterView(for: indexPath) } + 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 cellBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + let formatter = DateFormatter() + formatter.dateStyle = .medium + let dateString = formatter.string(from: message.sentDate) + return NSAttributedString(string: dateString, attributes: [NSFontAttributeName: UIFont.preferredFont(forTextStyle: .caption2)]) + } + } // MARK: - MessagesLayoutDelegate @@ -159,13 +121,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 00000000..bec3cfec Binary files /dev/null and b/Example/Sources/Playgrounds/Avatar.playground/Resources/NiceSelfi.jpg differ 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..5fbf6a95 --- /dev/null +++ b/Example/Sources/SampleData.swift @@ -0,0 +1,64 @@ +/* + 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 + +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) + 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, msg7, msg8] + } + + 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.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.' diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index 3a25e9fc..42f3fefe 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -7,8 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 171D5AB91F36712B0053DF69 /* InputTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171D5AB81F36712B0053DF69 /* InputTextView.swift */; }; 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 */; }; @@ -28,6 +30,8 @@ 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 */; }; + 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 */; }; @@ -44,9 +48,10 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 171D5AB81F36712B0053DF69 /* InputTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputTextView.swift; sourceTree = ""; }; 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; }; @@ -69,6 +74,8 @@ 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 = ""; }; + 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 = ""; }; @@ -140,6 +147,7 @@ children = ( B09643851F286C9E004D0129 /* String+Extensions.swift */, B096438F1F289142004D0129 /* UIColor+Extensions.swift */, + B074EEA91F3BE8F300ABB8C8 /* NSAttributedString+Extensions.swift */, ); name = Extensions; sourceTree = ""; @@ -149,6 +157,7 @@ children = ( B0655A291F23D77200542A83 /* Sender.swift */, B0655A2B1F23D81600542A83 /* MessageData.swift */, + 37C936971F38F6AC00853DF2 /* Avatar.swift */, B0655A271F23D71400542A83 /* MessageDirection.swift */, ); name = Models; @@ -160,10 +169,11 @@ B0655A2D1F23D8BC00542A83 /* MessagesCollectionView.swift */, B0655A4C1F244C0600542A83 /* MessageCollectionViewCell.swift */, 372F6AEA1F36C15600B57FBD /* AvatarView.swift */, - 372F6AED1F36C1C100B57FBD /* AvatarView.playground */, B0655A371F23EE8B00542A83 /* MessageInputBar.swift */, + 171D5AB81F36712B0053DF69 /* InputTextView.swift */, B074EE921F35587100ABB8C8 /* MessageHeaderView.swift */, B074EE941F35588A00ABB8C8 /* MessageFooterView.swift */, + B074EEA71F3971A600ABB8C8 /* MessageLabel.swift */, ); name = Views; sourceTree = ""; @@ -266,7 +276,7 @@ attributes = { LastSwiftUpdateCheck = 0730; LastUpgradeCheck = 0810; - ORGANIZATIONNAME = "Hexed Bits"; + ORGANIZATIONNAME = MessageKit; TargetAttributes = { 88916B211CF0DF2F00469F91 = { CreatedOnToolsVersion = 7.3.1; @@ -333,6 +343,8 @@ isa = PBXSourcesBuildPhase; 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 */, @@ -348,9 +360,11 @@ 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 */, + 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..537cc32a 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,31 @@ 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)) + let context = UIGraphicsGetCurrentContext()! + + //// 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: 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() + 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 +67,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 +96,10 @@ open class AvatarView: UIView { } layer.cornerRadius = radius } + + // MARK: - Open getters + + open func getImage() -> UIImage? { + return imageView.image + } } diff --git a/Sources/InputTextView.swift b/Sources/InputTextView.swift new file mode 100644 index 00000000..55ae79ca --- /dev/null +++ b/Sources/InputTextView.swift @@ -0,0 +1,119 @@ +/* + 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() + } + } + + private var isPlaceholderVisibile = false + + // MARK: - Initializers + + override public init(frame: CGRect, textContainer: NSTextContainer?) { + super.init(frame: frame, textContainer: textContainer) + addObservers() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + addObservers() + } + + 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) + + isPlaceholderVisibile = true + + } + + fileprivate func addObservers() { + NotificationCenter.default.addObserver(self, + 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) { + guard text.isEmpty || isPlaceholderVisibile else { return } + setNeedsDisplay() + isPlaceholderVisibile = false + } + + func orientationChanged(notification: Notification) { + setNeedsDisplay() + } + +} diff --git a/Sources/MessageCollectionViewCell.swift b/Sources/MessageCollectionViewCell.swift index baf2ac95..8b8f8fcc 100644 --- a/Sources/MessageCollectionViewCell.swift +++ b/Sources/MessageCollectionViewCell.swift @@ -29,31 +29,19 @@ 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 = { + open var avatarView: AvatarView = AvatarView() - let avatarImageView = UIImageView() - avatarImageView.contentMode = .scaleAspectFill - avatarImageView.backgroundColor = .lightGray - avatarImageView.layer.masksToBounds = true - avatarImageView.clipsToBounds = true - return avatarImageView - }() + open var cellTopLabel: MessageLabel = MessageLabel() - open let messageLabel: UILabel = { + open var messageLabel: MessageLabel = MessageLabel() - let messageLabel = UILabel() - messageLabel.numberOfLines = 0 - messageLabel.backgroundColor = .clear - messageLabel.isOpaque = false - return messageLabel - }() + open var cellBottomLabel: MessageLabel = MessageLabel() open weak var delegate: MessageCellDelegate? @@ -74,9 +62,11 @@ open class MessageCollectionViewCell: UICollectionViewCell { private func setupSubviews() { + contentView.addSubview(cellTopLabel) contentView.addSubview(messageContainerView) - contentView.addSubview(avatarImageView) messageContainerView.addSubview(messageLabel) + contentView.addSubview(avatarView) + contentView.addSubview(cellBottomLabel) } @@ -85,54 +75,85 @@ open class MessageCollectionViewCell: UICollectionViewCell { guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return } - messageLabel.font = attributes.messageFont + cellTopLabel.frame = cellTopLabelFrame(for: attributes) + cellTopLabel.textInsets = attributes.cellTopLabelInsets - setAvatarFrameFor(attributes: attributes) - setMessageContainerFrameFor(attributes: attributes) - setMessageLabelFor(attributes: attributes) + messageContainerView.frame = messageContainerFrame(for: attributes) + messageLabel.frame = CGRect(origin: .zero, size: attributes.messageContainerSize) + messageLabel.textInsets = attributes.messageLabelInsets - } + avatarView.frame = avatarViewFrame(for: attributes) - private func setMessageContainerFrameFor(attributes: MessagesCollectionViewLayoutAttributes) { + cellBottomLabel.frame = cellBottomLabelFrame(for: attributes) + cellBottomLabel.textInsets = attributes.cellBottomLabelInsets 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) + cellTopLabel.textAlignment = .left + cellBottomLabel.textAlignment = .right 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) + cellTopLabel.textAlignment = .right + cellBottomLabel.textAlignment = .left } } - private func setAvatarFrameFor(attributes: MessagesCollectionViewLayoutAttributes) { + func cellTopLabelFrame(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.topLabelPinnedUnderMessage { + origin = CGPoint(x: attributes.avatarSize.width + attributes.avatarMessagePadding, y: 0) } - avatarImageView.layer.cornerRadius = avatarImageView.frame.width / 2 + return CGRect(origin: origin, size: attributes.cellTopLabelSize) + } + + func cellBottomLabelFrame(for attributes: MessagesCollectionViewLayoutAttributes) -> CGRect { + + var origin: CGPoint = CGPoint(x: 0, y: contentView.frame.height - attributes.cellBottomLabelSize.height) + + if attributes.bottomLabelPinnedUnderMessage { + origin.x = attributes.avatarSize.width + attributes.avatarMessagePadding + } + + 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(x: 0, y: 0, width: attributes.messageContainerSize.width, height: attributes.messageContainerSize.height) - let insetFrame = UIEdgeInsetsInsetRect(frame, attributes.messageContainerInsets) - messageLabel.frame = insetFrame + var origin: CGPoint = .zero + + let yPosition = contentView.frame.height - attributes.avatarSize.height - attributes.avatarBottomPadding - attributes.cellBottomLabelSize.height + + 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) } @@ -141,6 +162,8 @@ open class MessageCollectionViewCell: UICollectionViewCell { switch message.data { case .text(let text): messageLabel.text = text + case .attributedText(let text): + messageLabel.attributedText = text } } @@ -148,8 +171,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/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/MessageInputBar.swift b/Sources/MessageInputBar.swift index fd89376e..f3001cf8 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 @@ -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,13 +96,15 @@ open class MessageInputBar: UIView, UITextViewDelegate { } public func textViewDidChange(_ textView: UITextView) { + let trimmedText = textView.text.trimmingCharacters(in: .whitespacesAndNewlines) + sendButton.isEnabled = !trimmedText.isEmpty invalidateIntrinsicContentSize() } 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/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)) + } + +} diff --git a/Sources/MessagesCollectionViewFlowLayout.swift b/Sources/MessagesCollectionViewFlowLayout.swift index 9315ce5e..3f2c85e6 100644 --- a/Sources/MessagesCollectionViewFlowLayout.swift +++ b/Sources/MessagesCollectionViewFlowLayout.swift @@ -28,17 +28,25 @@ open class MessagesCollectionViewFlowLayout: UICollectionViewFlowLayout { // MARK: - Properties - open var messageFont: UIFont + open var messageLabelFont: UIFont + open var messageLabelInsets: UIEdgeInsets + open var messageToViewEdgePadding: CGFloat + + open var cellTopLabelInsets: UIEdgeInsets + open var topLabelPinnedUnderMessage: Bool + + open var cellBottomLabelInsets: UIEdgeInsets + open var bottomLabelPinnedUnderMessage: Bool open var incomingAvatarSize: CGSize - open var outgoingAvatarSize: CGSize - open var messageContainerInsets: UIEdgeInsets + fileprivate var avatarBottomPadding: CGFloat = 2 + fileprivate var avatarMessagePadding: CGFloat = 4 - 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 } @@ -52,11 +60,22 @@ 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 + + cellTopLabelInsets = .zero + topLabelPinnedUnderMessage = true + + cellBottomLabelInsets = .zero + bottomLabelPinnedUnderMessage = true + incomingAvatarSize = CGSize(width: 30, height: 30) outgoingAvatarSize = CGSize(width: 30, height: 30) - messageContainerInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 14) + super.init() + sectionInset = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) } @@ -93,22 +112,31 @@ 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.cellTopLabelInsets = cellTopLabelInsets + attributes.topLabelPinnedUnderMessage = topLabelPinnedUnderMessage + + attributes.cellBottomLabelSize = cellBottomLabelSize(for: message, at: indexPath) + attributes.cellBottomLabelInsets = cellBottomLabelInsets + attributes.bottomLabelPinnedUnderMessage = bottomLabelPinnedUnderMessage + + attributes.avatarSize = avatarSize(for: message) + attributes.avatarBottomPadding = avatarBottomPadding + attributes.avatarMessagePadding = avatarMessagePadding + + attributes.direction = dataSource.isFromCurrentSender(message: message) ? .outgoing : .incoming } @@ -129,91 +157,179 @@ 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 topLabelPinnedUnderMessage { + return itemWidth - avatarSize(for: message).width - avatarMessagePadding - messageToViewEdgePadding + } else { + return itemWidth } + } + + func cellBottomLabelWidth(for message: MessageType) -> CGFloat { + if bottomLabelPinnedUnderMessage { + return itemWidth - avatarSize(for: message).width - avatarMessagePadding - messageToViewEdgePadding + } 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.cellTopLabelAttributedText(for: message, at: indexPath) else { return 0 } + + let availableWidth = cellTopLabelWidth(for: message) + + let estimatedHeight = topLabelText.height(considering: availableWidth) + + return estimatedHeight.rounded(.up) } - func containerHeightForMessage(message: MessageType) -> CGFloat { + func cellBottomLabelHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - let avatarSize = avatarSizeFor(message: message) - let insets = messageContainerInsets.left + messageContainerInsets.right - let availableWidth = itemWidth - avatarSize.width - avatarContainerSpacing - insets + guard let messagesCollectionView = messagesCollectionView else { return 0 } + guard let displayDataSource = messagesCollectionView.messagesDataSource as? MessagesDisplayDataSource 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) + + 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 { + + let availableWidth = availableWidthForMessageContainer(considering: message) + let verticalMessageInsets = messageLabelInsets.top + messageLabelInsets.bottom + var estimatedHeight: CGFloat = 0 - // This is a switch because support for more messages are to come 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 + estimatedHeight = text.height(considering: availableWidth, and: messageLabelFont) + case .attributedText(let text): + estimatedHeight = text.height(considering: availableWidth) } + 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 avatarSize = avatarSizeFor(message: message) - let insets = messageContainerInsets.left + messageContainerInsets.right - let availableWidth = itemWidth - avatarSize.width - avatarContainerSpacing - insets + let containerHeight = messageContainerHeight(for: message, at: indexPath) + let availableWidth = availableWidthForMessageContainer(considering: message) + let horizontalMessageInsets = messageLabelInsets.left + messageLabelInsets.right + var estimatedWidth: CGFloat = 0 - // This is a switch because support for more messages are to come 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 + estimatedWidth = text.width(considering: containerHeight, and: messageLabelFont) + case .attributedText(let text): + estimatedWidth = text.width(considering: containerHeight) } - } + let widthToUse = estimatedWidth.rounded(.up) > availableWidth ? availableWidth : estimatedWidth - func estimatedCellHeightForMessage(message: MessageType) -> CGFloat { + let finalWidth = widthToUse + horizontalMessageInsets - let messageContainerHeight = containerHeightForMessage(message: message) - return messageContainerHeight + return finalWidth } - func containerSizeFor(message: MessageType) -> CGSize { + // MARK: - Cell Size Calculations - let containerHeight = containerHeightForMessage(message: message) - let containerWidth = containerWidthForMessage(message: message) + func estimatedCellHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { - return CGSize(width: containerWidth, height: containerHeight) + let containerHeight = messageContainerHeight(for: message, at: indexPath) + let topLabelHeight = cellTopLabelHeight(for: message, at: indexPath) + let bottomLabelheight = cellBottomLabelHeight(for: message, at: indexPath) + + 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: actualHeight) + return CGSize(width: itemWidth, height: finalHeight) } diff --git a/Sources/MessagesCollectionViewLayoutAttributes.swift b/Sources/MessagesCollectionViewLayoutAttributes.swift index 4cbfa6b4..f7b2628c 100644 --- a/Sources/MessagesCollectionViewLayoutAttributes.swift +++ b/Sources/MessagesCollectionViewLayoutAttributes.swift @@ -28,32 +28,44 @@ 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 cellTopLabelInsets: UIEdgeInsets = .zero + var topLabelPinnedUnderMessage = true + + var cellBottomLabelSize: CGSize = .zero + var cellBottomLabelInsets: UIEdgeInsets = .zero + var bottomLabelPinnedUnderMessage = true 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.cellTopLabelInsets = cellTopLabelInsets + copy.topLabelPinnedUnderMessage = topLabelPinnedUnderMessage + copy.cellBottomLabelSize = cellBottomLabelSize + copy.cellBottomLabelInsets = cellBottomLabelInsets + copy.bottomLabelPinnedUnderMessage = bottomLabelPinnedUnderMessage 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 067df3b6..cdc33650 100644 --- a/Sources/MessagesDisplayDataSource.swift +++ b/Sources/MessagesDisplayDataSource.swift @@ -28,12 +28,16 @@ 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? func footerForMessage(_ message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageFooterView? + func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? + + func cellBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? + } public extension MessagesDisplayDataSource { @@ -50,4 +54,12 @@ public extension MessagesDisplayDataSource { return nil } + func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + return nil + } + + func cellBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + return nil + } + } diff --git a/Sources/MessagesViewController.swift b/Sources/MessagesViewController.swift index 6e4ea2df..5ad5c1ad 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()) @@ -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 @@ -114,7 +113,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)) } @@ -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() @@ -172,8 +167,12 @@ 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() + let topLabelText = displayDataSource.cellTopLabelAttributedText(for: message, at: indexPath) + let bottomLabelText = displayDataSource.cellBottomLabelAttributedText(for: message, at: indexPath) + + cell.cellTopLabel.attributedText = topLabelText + cell.cellBottomLabel.attributedText = bottomLabelText + cell.avatarView.set(avatar: avatar) cell.messageContainerView.backgroundColor = messageColor cell.configure(with: message) @@ -219,8 +218,6 @@ extension MessagesViewController: UICollectionViewDataSource { return messagesLayoutDelegate.footerSizeFor(message, at: indexPath, in: messagesCollectionView) } - //swiftlint:enable line_length - } // MARK: - Keyboard Handling @@ -252,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..316d9540 --- /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) -> CGFloat { + + let constraintBox = CGSize(width: .greatestFiniteMagnitude, height: height) + let rect = self.boundingRect(with: constraintBox, options: .usesLineFragmentOrigin, context: nil) + return rect.width + + } + +} 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) + } diff --git a/Tests/AvatarViewTests.swift b/Tests/AvatarViewTests.swift index 1387f5e4..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 @@ -20,61 +36,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) } } 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