diff --git a/Assets/CellStructure.png b/Assets/CellStructure.png new file mode 100644 index 00000000..90e453a7 Binary files /dev/null and b/Assets/CellStructure.png differ diff --git a/Assets/ExampleA.png b/Assets/ExampleA.png new file mode 100644 index 00000000..88b0a4dc Binary files /dev/null and b/Assets/ExampleA.png differ diff --git a/Assets/ExampleB.png b/Assets/ExampleB.png new file mode 100644 index 00000000..5b5ce8c6 Binary files /dev/null and b/Assets/ExampleB.png differ diff --git a/Assets/InputBarAccessoryViewLayout.png b/Assets/InputBarAccessoryViewLayout.png new file mode 100644 index 00000000..c0cb30bb Binary files /dev/null and b/Assets/InputBarAccessoryViewLayout.png differ diff --git a/Assets/MessageInputBarLayout.png b/Assets/MessageInputBarLayout.png deleted file mode 100644 index 1ef5e1c3..00000000 Binary files a/Assets/MessageInputBarLayout.png and /dev/null differ diff --git a/Assets/TypingIndicator.png b/Assets/TypingIndicator.png new file mode 100644 index 00000000..94c6fd86 Binary files /dev/null and b/Assets/TypingIndicator.png differ diff --git a/Documentation/MANUAL_INSTALLATION.md b/Documentation/MANUAL_INSTALLATION.md new file mode 100644 index 00000000..4c7cdfdc --- /dev/null +++ b/Documentation/MANUAL_INSTALLATION.md @@ -0,0 +1,22 @@ +## Embedded Framework Installation + +- `cd` to your project directory, initialize git, and Add MessageKit as a git [submodule](https://git-scm.com/docs/git-submodule) by running the following command: + +```bash +$ git submodule add https://github.com/MessageKit/MessageKit.git +``` + +- `cd` to the new `MessageKit` folder and trigger [carthage](https://github.com/Carthage/Carthage) update by the following command: + +```bash +$ carthage update --platform iOS +``` + +- Open `MessageKit` folder, and drag the `MessageKit.xcodeproj` into the Project Navigator of your application's Xcode project. It should appear nested underneath your application's blue project icon. +- Select the `MessageKit.xcodeproj` in the Project Navigator and verify the deployment target matches that of your application target. +- Next, select your application project in the Project Navigator (blue project icon), navigate to the target configuration window and select the application target. +- In the tab bar at the top of that window, open the "General" panel. +- Click on the `+` button under the "Embedded Binaries" section. +- You will see two different `MessageKit.xcodeproj` folders each with two different versions of the `MessageKit.framework` nested inside a `Products` folder. +- Select the top `MessageKit.framework` for iOS and the bottom one for OS X. +- Voila! Now you can `import MessageKit` and build the project. diff --git a/Example/ChatExample.xcodeproj/project.pbxproj b/Example/ChatExample.xcodeproj/project.pbxproj index 63b5cfe8..bfff37ea 100644 --- a/Example/ChatExample.xcodeproj/project.pbxproj +++ b/Example/ChatExample.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 385C2946211FF38F0010B4BA /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C293F211FF38F0010B4BA /* LaunchViewController.swift */; }; 385C2947211FF38F0010B4BA /* BasicExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C2940211FF38F0010B4BA /* BasicExampleViewController.swift */; }; 385C2948211FF38F0010B4BA /* ChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 385C2941211FF38F0010B4BA /* ChatViewController.swift */; }; + 38CCCC592258419300DD5482 /* AutocompleteExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38CCCC582258419300DD5482 /* AutocompleteExampleViewController.swift */; }; 50739F9621C5090A008CA369 /* BasicAudioController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50739F9521C5090A008CA369 /* BasicAudioController.swift */; }; 5074EF4E2163555900D82952 /* sound2.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 5074EF4C2163555900D82952 /* sound2.m4a */; }; 5074EF4F2163555900D82952 /* sound1.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 5074EF4D2163555900D82952 /* sound1.m4a */; }; @@ -89,6 +90,7 @@ 385C293F211FF38F0010B4BA /* LaunchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = ""; }; 385C2940211FF38F0010B4BA /* BasicExampleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicExampleViewController.swift; sourceTree = ""; }; 385C2941211FF38F0010B4BA /* ChatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatViewController.swift; sourceTree = ""; }; + 38CCCC582258419300DD5482 /* AutocompleteExampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteExampleViewController.swift; sourceTree = ""; }; 3B316705C4717C3B4C916D62 /* Pods_ChatExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ChatExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 50739F9521C5090A008CA369 /* BasicAudioController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicAudioController.swift; sourceTree = ""; }; 5074EF4C2163555900D82952 /* sound2.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = sound2.m4a; sourceTree = ""; }; @@ -189,6 +191,7 @@ isa = PBXGroup; children = ( 385C293B211FF38E0010B4BA /* AdvancedExampleViewController.swift */, + 38CCCC582258419300DD5482 /* AutocompleteExampleViewController.swift */, 385C2940211FF38F0010B4BA /* BasicExampleViewController.swift */, 385C2941211FF38F0010B4BA /* ChatViewController.swift */, 385C293F211FF38F0010B4BA /* LaunchViewController.swift */, @@ -550,6 +553,7 @@ 385C2947211FF38F0010B4BA /* BasicExampleViewController.swift in Sources */, 882B5E811CF7D53600B6E160 /* AppDelegate.swift in Sources */, 385C292D211FF3520010B4BA /* CustomMessageFlowLayout.swift in Sources */, + 38CCCC592258419300DD5482 /* AutocompleteExampleViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Example/Sources/Data Generation/SampleData.swift b/Example/Sources/Data Generation/SampleData.swift index 02666fd5..9fa50d29 100644 --- a/Example/Sources/Data Generation/SampleData.swift +++ b/Example/Sources/Data Generation/SampleData.swift @@ -63,7 +63,7 @@ final internal class SampleData { ] var currentSender: MockUser { - return nathan + return steven } var now = Date() @@ -156,7 +156,7 @@ final internal class SampleData { func randomMessage(allowedSenders: [MockUser]) -> MockMessage { let randomNumberSender = Int(arc4random_uniform(UInt32(allowedSenders.count))) - let uniqueID = NSUUID().uuidString + let uniqueID = UUID().uuidString let user = allowedSenders[randomNumberSender] let date = dateAddingRandomTime() @@ -204,7 +204,11 @@ final internal class SampleData { // Disable Custom Messages UserDefaults.standard.set(false, forKey: "Custom Messages") for _ in 0.. Bool { + return indexPath.section % 3 == 0 && !isPreviousMessageSameSender(at: indexPath) + } + + func isPreviousMessageSameSender(at indexPath: IndexPath) -> Bool { + guard indexPath.section - 1 >= 0 else { return false } + return messageList[indexPath.section].user == messageList[indexPath.section - 1].user + } + + func isNextMessageSameSender(at indexPath: IndexPath) -> Bool { + guard indexPath.section + 1 < messageList.count else { return false } + return messageList[indexPath.section].user == messageList[indexPath.section + 1].user + } + + func setTypingIndicatorViewHidden(_ isHidden: Bool, performUpdates updates: (() -> Void)? = nil) { + setTypingIndicatorViewHidden(isHidden, animated: true, whilePerforming: updates) { [weak self] success in + if success, self?.isLastSectionVisible() == true { + self?.messagesCollectionView.scrollToBottom(animated: true) + } + } + } + + private func makeButton(named: String) -> InputBarButtonItem { + return InputBarButtonItem() + .configure { + $0.spacing = .fixed(10) + $0.image = UIImage(named: named)?.withRenderingMode(.alwaysTemplate) + $0.setSize(CGSize(width: 25, height: 25), animated: false) + $0.tintColor = UIColor(white: 0.8, alpha: 1) + }.onSelected { + $0.tintColor = .primaryColor + }.onDeselected { + $0.tintColor = UIColor(white: 0.8, alpha: 1) + }.onTouchUpInside { _ in + print("Item Tapped") + } + } + + // MARK: - MessagesDataSource + + override func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + if isTimeLabelVisible(at: indexPath) { + return NSAttributedString(string: MessageKitDateFormatter.shared.string(from: message.sentDate), attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10), NSAttributedString.Key.foregroundColor: UIColor.darkGray]) + } + return nil + } + + override func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + if !isPreviousMessageSameSender(at: indexPath) { + let name = message.sender.displayName + return NSAttributedString(string: name, attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)]) + } + return nil + } + + override func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + + if !isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message) { + return NSAttributedString(string: "Delivered", attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)]) + } + return nil + } + + // Async autocomplete requires the manager to reload + func inputBar(_ inputBar: InputBarAccessoryView, textViewTextDidChangeTo text: String) { + guard autocompleteManager.currentSession != nil, autocompleteManager.currentSession?.prefix == "#" else { return } + // Load some data asyncronously for the given session.prefix + DispatchQueue.global(qos: .default).async { + // fake background loading task + var array: [AutocompleteCompletion] = [] + for _ in 1...10 { + array.append(AutocompleteCompletion(text: Lorem.word())) + } + sleep(1) + DispatchQueue.main.async { [weak self] in + self?.asyncCompletions = array + self?.autocompleteManager.reloadData() + } + } + } +} + +extension AutocompleteExampleViewController: AutocompleteManagerDelegate, AutocompleteManagerDataSource { + + // MARK: - AutocompleteManagerDataSource + + func autocompleteManager(_ manager: AutocompleteManager, autocompleteSourceFor prefix: String) -> [AutocompleteCompletion] { + + if prefix == "@" { + return SampleData.shared.senders + .map { user in + return AutocompleteCompletion(text: user.displayName, + context: ["id": user.senderId]) + } + } else if prefix == "#" { + return hastagAutocompletes + asyncCompletions + } + return [] + } + + func autocompleteManager(_ manager: AutocompleteManager, tableView: UITableView, cellForRowAt indexPath: IndexPath, for session: AutocompleteSession) -> UITableViewCell { + + guard let cell = tableView.dequeueReusableCell(withIdentifier: AutocompleteCell.reuseIdentifier, for: indexPath) as? AutocompleteCell else { + fatalError("Oops, some unknown error occurred") + } + let users = SampleData.shared.senders + let id = session.completion?.context?["id"] as? String + let user = users.filter { return $0.senderId == id }.first + if let sender = user { + cell.imageView?.image = SampleData.shared.getAvatarFor(sender: sender).image + } + cell.imageViewEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) + cell.imageView?.layer.cornerRadius = 14 + cell.imageView?.layer.borderColor = UIColor.primaryColor.cgColor + cell.imageView?.layer.borderWidth = 1 + cell.imageView?.clipsToBounds = true + cell.textLabel?.attributedText = manager.attributedText(matching: session, fontSize: 15) + return cell + } + + // MARK: - AutocompleteManagerDelegate + + func autocompleteManager(_ manager: AutocompleteManager, shouldBecomeVisible: Bool) { + setAutocompleteManager(active: shouldBecomeVisible) + } + + // Optional + func autocompleteManager(_ manager: AutocompleteManager, shouldRegister prefix: String, at range: NSRange) -> Bool { + return true + } + + // Optional + func autocompleteManager(_ manager: AutocompleteManager, shouldUnregister prefix: String) -> Bool { + return true + } + + // Optional + func autocompleteManager(_ manager: AutocompleteManager, shouldComplete prefix: String, with text: String) -> Bool { + return true + } + + // MARK: - AutocompleteManagerDelegate Helper + + func setAutocompleteManager(active: Bool) { + let topStackView = messageInputBar.topStackView + if active && !topStackView.arrangedSubviews.contains(autocompleteManager.tableView) { + topStackView.insertArrangedSubview(autocompleteManager.tableView, at: topStackView.arrangedSubviews.count) + topStackView.layoutIfNeeded() + } else if !active && topStackView.arrangedSubviews.contains(autocompleteManager.tableView) { + topStackView.removeArrangedSubview(autocompleteManager.tableView) + topStackView.layoutIfNeeded() + } + messageInputBar.invalidateIntrinsicContentSize() + } +} + +// MARK: - MessagesDisplayDelegate + +extension AutocompleteExampleViewController: MessagesDisplayDelegate { + + // MARK: - Text Messages + + func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { + return isFromCurrentSender(message: message) ? .white : .darkText + } + + func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any] { + switch detector { + case .hashtag, .mention: + if isFromCurrentSender(message: message) { + return [.foregroundColor: UIColor.white] + } else { + return [.foregroundColor: UIColor.primaryColor] + } + default: return MessageLabel.defaultAttributes + } + } + + func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] { + return [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag] + } + + // MARK: - All Messages + + func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { + return isFromCurrentSender(message: message) ? .primaryColor : UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1) + } + + func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle { + return .bubble + } + + func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { + let avatar = SampleData.shared.getAvatarFor(sender: message.sender) + avatarView.set(avatar: avatar) + avatarView.isHidden = isNextMessageSameSender(at: indexPath) + avatarView.layer.borderWidth = 2 + avatarView.layer.borderColor = UIColor.primaryColor.cgColor + } + + func configureAccessoryView(_ accessoryView: UIView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { + // Cells are reused, so only add a button here once. For real use you would need to + // ensure any subviews are removed if not needed + accessoryView.subviews.forEach { $0.removeFromSuperview() } + + let button = UIButton(type: .infoLight) + button.tintColor = .primaryColor + accessoryView.addSubview(button) + button.frame = accessoryView.bounds + button.isUserInteractionEnabled = false // respond to accessoryView tap through `MessageCellDelegate` + accessoryView.layer.cornerRadius = accessoryView.frame.height / 2 + accessoryView.backgroundColor = UIColor.primaryColor.withAlphaComponent(0.3) + } +} + +// MARK: - MessagesLayoutDelegate + +extension AutocompleteExampleViewController: MessagesLayoutDelegate { + + func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + if isTimeLabelVisible(at: indexPath) { + return 18 + } + return 0 + } + + func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + if isFromCurrentSender(message: message) { + return !isPreviousMessageSameSender(at: indexPath) ? 20 : 0 + } else { + return !isPreviousMessageSameSender(at: indexPath) ? 20 : 0 + } + } + + func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { + return (!isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message)) ? 16 : 0 + } + +} diff --git a/Example/Sources/View Controllers/BasicExampleViewController.swift b/Example/Sources/View Controllers/BasicExampleViewController.swift index a498e237..98e81d55 100644 --- a/Example/Sources/View Controllers/BasicExampleViewController.swift +++ b/Example/Sources/View Controllers/BasicExampleViewController.swift @@ -27,14 +27,11 @@ import MapKit import MessageKit final class BasicExampleViewController: ChatViewController { - override func configureMessageCollectionView() { super.configureMessageCollectionView() - messagesCollectionView.messagesLayoutDelegate = self messagesCollectionView.messagesDisplayDelegate = self } - } // MARK: - MessagesDisplayDelegate diff --git a/Example/Sources/View Controllers/ChatViewController.swift b/Example/Sources/View Controllers/ChatViewController.swift index d257e1c0..817e8027 100644 --- a/Example/Sources/View Controllers/ChatViewController.swift +++ b/Example/Sources/View Controllers/ChatViewController.swift @@ -58,7 +58,7 @@ class ChatViewController: MessagesViewController, MessagesDataSource { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - MockSocket.shared.connect(with: [SampleData.shared.steven, SampleData.shared.wu]) + MockSocket.shared.connect(with: [SampleData.shared.nathan, SampleData.shared.wu]) .onNewMessage { [weak self] message in self?.insertMessage(message) } @@ -298,8 +298,37 @@ extension ChatViewController: InputBarAccessoryViewDelegate { func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) { - for component in inputBar.inputTextView.components { + // Here we can parse for which substrings were autocompleted + let attributedText = messageInputBar.inputTextView.attributedText! + let range = NSRange(location: 0, length: attributedText.length) + attributedText.enumerateAttribute(.autocompleted, in: range, options: []) { (_, range, _) in + let substring = attributedText.attributedSubstring(from: range) + let context = substring.attribute(.autocompletedContext, at: 0, effectiveRange: nil) + print("Autocompleted: `", substring, "` with context: ", context ?? []) + } + + let components = inputBar.inputTextView.components + messageInputBar.inputTextView.text = String() + messageInputBar.invalidatePlugins() + + // Send button activity animation + messageInputBar.sendButton.startAnimating() + messageInputBar.inputTextView.placeholder = "Sending..." + DispatchQueue.global(qos: .default).async { + // fake send request task + sleep(1) + DispatchQueue.main.async { [weak self] in + self?.messageInputBar.sendButton.stopAnimating() + self?.messageInputBar.inputTextView.placeholder = "Aa" + self?.insertMessages(components) + self?.messagesCollectionView.scrollToBottom(animated: true) + } + } + } + + private func insertMessages(_ data: [Any]) { + for component in data { let user = SampleData.shared.currentSender if let str = component as? String { let message = MockMessage(text: str, user: user, messageId: UUID().uuidString, date: Date()) @@ -308,9 +337,6 @@ extension ChatViewController: InputBarAccessoryViewDelegate { let message = MockMessage(image: img, user: user, messageId: UUID().uuidString, date: Date()) insertMessage(message) } - } - inputBar.inputTextView.text = String() - messagesCollectionView.scrollToBottom(animated: true) } } diff --git a/Example/Sources/View Controllers/LaunchViewController.swift b/Example/Sources/View Controllers/LaunchViewController.swift index fbfb71d2..50794115 100644 --- a/Example/Sources/View Controllers/LaunchViewController.swift +++ b/Example/Sources/View Controllers/LaunchViewController.swift @@ -32,7 +32,7 @@ final internal class LaunchViewController: UITableViewController { return .lightContent } - let cells = ["Basic Example", "Advanced Example", "Embedded Example", "Settings", "Source Code", "Contributors"] + let cells = ["Basic Example", "Advanced Example", "Autocomplete Example", "Embedded Example", "Settings", "Source Code", "Contributors"] // MARK: - View Life Cycle @@ -80,6 +80,8 @@ final internal class LaunchViewController: UITableViewController { navigationController?.pushViewController(BasicExampleViewController(), animated: true) case "Advanced Example": navigationController?.pushViewController(AdvancedExampleViewController(), animated: true) + case "Autocomplete Example": + navigationController?.pushViewController(AutocompleteExampleViewController(), animated: true) case "Embedded Example": navigationController?.pushViewController(MessageContainerController(), animated: true) case "Settings": diff --git a/MessageKit.xcodeproj/project.pbxproj b/MessageKit.xcodeproj/project.pbxproj index d178c1d6..cfaac857 100644 --- a/MessageKit.xcodeproj/project.pbxproj +++ b/MessageKit.xcodeproj/project.pbxproj @@ -625,7 +625,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; + shellScript = "if which swiftlint >/dev/null; then\n if [ -z \"$CARTHAGE\" ]; then\n swiftlint\n fi\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/README.md b/README.md index 90959930..b970ec77 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@

+

+ +

[![CircleCI](https://circleci.com/gh/MessageKit/MessageKit.svg?style=svg)](https://circleci.com/gh/MessageKit/MessageKit) [![codecov](https://codecov.io/gh/MessageKit/MessageKit/branch/master/graph/badge.svg)](https://codecov.io/gh/MessageKit/MessageKit) @@ -26,18 +29,6 @@ alt="Contributions Welcome"> -## Table of Contents - -* [**Goal**](#goals)📈 -* [**Contributing**](#contributing) -* [**Requirements**](#requirements) -* [**Code of Conduct**](https://github.com/MessageKit/MessageKit/blob/master/CODE_OF_CONDUCT.md) -* [**What's Next**](#whats-next) -* [**Contact**](#contact) -* [**Apps using this library**](#apps-using-this-library) -* [**License**](#license) - - ## Goals - Provide a :rotating_light:safe:rotating_light: environment for others to learn and grow through Open Source. @@ -49,30 +40,14 @@ ## Vision See [VISION.md](https://github.com/MessageKit/MessageKit/blob/master/VISION.md) for Goals, Scope, & Technical Considerations. - ## Installation ### [CocoaPods](https://cocoapods.org/) **Recommended** ````ruby +# Swift 4.2 pod 'MessageKit' ```` -If your project is still using Swift 3, you can add the following code to your Podfile: - -````ruby -target 'TARGET_NAME' do - pod 'MessageKit' - ... - post_install do |installer| - installer.pods_project.targets.each do |target| - if target.name == 'MessageKit' - target.build_configurations.each do |config| - config.build_settings['SWIFT_VERSION'] = '4.0' - end - end - end - end -end -```` +> If you are already using Swift 5, use the `3.0.0-swift5` branch until the offical release is made ### [Carthage](https://github.com/Carthage/Carthage) @@ -82,42 +57,69 @@ To integrate MessageKit using Carthage, add the following to your `Cartfile`: github "MessageKit/MessageKit" ```` -### Manual - -#### Embedded Framework - -- `cd` to your project directory, initialize git, and Add MessageKit as a git [submodule](https://git-scm.com/docs/git-submodule) by running the following command: - - ```bash - $ git submodule add https://github.com/MessageKit/MessageKit.git - ``` - -- `cd` to the new `MessageKit` folder and trigger [carthage](https://github.com/Carthage/Carthage) update by the following command: - - ```bash - $ carthage update --platform iOS - ``` - -- Open `MessageKit` folder, and drag the `MessageKit.xcodeproj` into the Project Navigator of your application's Xcode project. It should appear nested underneath your application's blue project icon. -- Select the `MessageKit.xcodeproj` in the Project Navigator and verify the deployment target matches that of your application target. -- Next, select your application project in the Project Navigator (blue project icon), navigate to the target configuration window and select the application target. -- In the tab bar at the top of that window, open the "General" panel. -- Click on the `+` button under the "Embedded Binaries" section. -- You will see two different `MessageKit.xcodeproj` folders each with two different versions of the `MessageKit.framework` nested inside a `Products` folder. -- Select the top `MessageKit.framework` for iOS and the bottom one for OS X. -- Voila! Now you can `import MessageKit` and build the project. - - -## Getting Started - -Please have a look at the [Quick Start guide](https://github.com/MessageKit/MessageKit/blob/master/Documentation/QuickStart.md), the [FAQs](https://github.com/MessageKit/MessageKit/blob/master/Documentation/FAQs.md) and the [MessageInputBar docs](https://github.com/MessageKit/MessageKit/blob/master/Documentation/MessageInputBar.md). - -If you have any issues have a look at the [Example](https://github.com/MessageKit/MessageKit/tree/master/Example) project or write a question with the "messagekit" tag on [Stack Overflow](https://stackoverflow.com/questions/tagged/messagekit). - +### [Manual]([https://github.com/MessageKit/MessageKit/blob/master/Documentation/MANUAL_INSTALLATION.md) ## Requirements - **iOS9** or later +- **Swift 4.2** or later + + +## Getting Started + +### Cell Structure +

+ +

+ +Each default cell is a subclass of [`MessageContentCell`](https://github.com/MessageKit/MessageKit/blob/master/Sources/Views/Cells/MessageContentCell.swift) which has 7 parts. From top down we have a: `cellTopLabel`, `messageTopLabel`, `messageContainerView`, `messageBottomLabel`, `cellBottomLabel` with the `avatarView` and `accessoryView` on either side respectively. Above we see the basic [`TextMessageCell`](https://github.com/MessageKit/MessageKit/blob/master/Sources/Views/Cells/TextMessageCell.swift) which uses a `MessageLabel` as its main content. + +This structure will allow you to create a layout that suits your needs as you can customize the size, appearance and padding of each. If you need something more advanced you can implement a custom cell, which we show how to do in the [Example](https://github.com/MessageKit/MessageKit/tree/master/Example) project. + +### MessageInputBar Structure +

+ +

+ +The `MessageInputBar`, derrived from [InputBarAccessoryView](https://github.com/nathantannar4/InputBarAccessoryView) is a flexible and robust way of creating any kind of input layout you wish. It is self-sizing which means as the user types it will grow to fill available space. It is centered around the `middleContentView` which by default holds the `InputTextView`. This is surrounded by `InputStackView`'s that will also grow in high based on the needs of their subviews `intrinsicContentSize`. See the [Example](https://github.com/MessageKit/MessageKit/tree/master/Example) project for examples on how to taylor the layout for your own needs. + +### Guides + +Please have a look at the [Quick Start guide](https://github.com/MessageKit/MessageKit/blob/master/Documentation/QuickStart.md) and the [FAQs](https://github.com/MessageKit/MessageKit/blob/master/Documentation/FAQs.md). + +We recommend you start by looking at the [Example](https://github.com/MessageKit/MessageKit/tree/master/Example) project or write a question with the "messagekit" tag on [Stack Overflow](https://stackoverflow.com/questions/tagged/messagekit). You can also look at previous issues here on GitHib with the **"Question"** tag. + +For more on how to use the MessageInputBar, see the dependency it is based on [InputBarAccessoryView](https://github.com/nathantannar4/InputBarAccessoryView). You can also see this [short guide]([https://github.com/MessageKit/MessageKit/blob/master/Documentation/MessageInputBar.md) + +## Default Cells + +

+ + +

+ +The type of cell rendered for a given message is based on the `MessageKind` + +```swift +public enum MessageKind { + case text(String) // TextMessageCell + case attributedText(NSAttributedString) // TextMessageCell + case photo(MediaItem) // MediaMessageCell + case video(MediaItem) // MediaMessageCell + case location(LocationItem) // LocationMessageCell + case emoji(String) // TextMessageCell + case audio(AudioItem) // AudioMessageCell + case contact(ContactItem) // ContactMessageCell + + /// A custom message. + /// - Note: Using this case requires that you implement the following methods and handle this case: + /// - MessagesDataSource: customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell + /// - MessagesLayoutDelegate: customCellSizeCalculator(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator + case custom(Any?) +} +``` + +If you choose to use the `.custom` kind you are responsible for all of the cells layout. Any `UICollectionViewCell` can be returned for custom cells which means any of the styling you provide from the `MessageDisplayDelegate` will not effect your custom cell. Even if you subclass your cell from `MessageContentCell`. ## Contributing diff --git a/Sources/Views/MessageLabel.swift b/Sources/Views/MessageLabel.swift index f1eff08f..3ea89490 100644 --- a/Sources/Views/MessageLabel.swift +++ b/Sources/Views/MessageLabel.swift @@ -461,6 +461,7 @@ open class MessageLabel: UILabel { return false } + // swiftlint:disable cyclomatic_complexity private func handleGesture(for detectorType: DetectorType, value: MessageTextCheckingType) { switch value { @@ -499,6 +500,7 @@ open class MessageLabel: UILabel { } } } + // swiftlint:enable cyclomatic_complexity private func handleAddress(_ addressComponents: [String: String]) { delegate?.didSelectAddress(addressComponents)