From d9d64fd31e57f08bbeba3ccd5ccbf4ac25c8d6dc Mon Sep 17 00:00:00 2001 From: Nathan Tannar Date: Sun, 3 Jun 2018 19:54:08 -0700 Subject: [PATCH] Plugin Support and Examples --- Example/Podfile | 3 + Example/Podfile.lock | 12 +- MessageInputBar.podspec | 54 ++- MessageInputBar.xcodeproj/project.pbxproj | 140 +++++++ .../AttachmentManager/AttachmentManager.swift | 222 ++++++++++++ .../AttachmentManagerDataSource.swift | 38 ++ .../Protocols/AttachmentManagerDelegate.swift | 78 ++++ .../Views/AttachmentCell.swift | 129 +++++++ .../Views/AttachmentCollectionView.swift | 76 ++++ .../Views/ImageAttachmentCell.swift | 64 ++++ .../AutocompleteManager.swift | 342 ++++++++++++++++++ .../Extensions/String+Extensions.swift | 63 ++++ .../Extensions/UITextView+Extensions.swift | 63 ++++ .../Models/AutocompleteCompletion.swift | 43 +++ .../Models/AutocompleteSession.swift | 41 +++ .../AutocompleteManagerDataSource.swift | 64 ++++ .../AutocompleteManagerDelegate.swift | 79 ++++ .../Views/AutocompleteCell.swift | 102 ++++++ .../Views/AutocompleteTableView.swift | 42 +++ ...NSMutableAttributedString+Extensions.swift | 73 ++++ Sources/MessageInputBar.swift | 8 + Sources/Protocols/InputPlugin.swift | 40 ++ Sources/Views/InputTextView.swift | 8 +- 23 files changed, 1759 insertions(+), 25 deletions(-) create mode 100755 Plugins/AttachmentManager/AttachmentManager.swift create mode 100755 Plugins/AttachmentManager/Protocols/AttachmentManagerDataSource.swift create mode 100755 Plugins/AttachmentManager/Protocols/AttachmentManagerDelegate.swift create mode 100755 Plugins/AttachmentManager/Views/AttachmentCell.swift create mode 100755 Plugins/AttachmentManager/Views/AttachmentCollectionView.swift create mode 100755 Plugins/AttachmentManager/Views/ImageAttachmentCell.swift create mode 100755 Plugins/AutocompleteManager/AutocompleteManager.swift create mode 100755 Plugins/AutocompleteManager/Extensions/String+Extensions.swift create mode 100755 Plugins/AutocompleteManager/Extensions/UITextView+Extensions.swift create mode 100644 Plugins/AutocompleteManager/Models/AutocompleteCompletion.swift create mode 100644 Plugins/AutocompleteManager/Models/AutocompleteSession.swift create mode 100755 Plugins/AutocompleteManager/Protocols/AutocompleteManagerDataSource.swift create mode 100755 Plugins/AutocompleteManager/Protocols/AutocompleteManagerDelegate.swift create mode 100755 Plugins/AutocompleteManager/Views/AutocompleteCell.swift create mode 100755 Plugins/AutocompleteManager/Views/AutocompleteTableView.swift create mode 100755 Sources/Extensions/NSMutableAttributedString+Extensions.swift create mode 100755 Sources/Protocols/InputPlugin.swift diff --git a/Example/Podfile b/Example/Podfile index 5f7486f..829a381 100644 --- a/Example/Podfile +++ b/Example/Podfile @@ -2,6 +2,9 @@ platform :ios, '9.0' target 'Example' do use_frameworks! + pod 'MessageInputBar', :path => '../' + pod 'MessageInputBar/AttachmentManager', :path => '../' + pod 'MessageInputBar/AutocompleteManager', :path => '../' end diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 9e9b05c..5b2c27c 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,16 +1,22 @@ PODS: - - MessageInputBar (0.1.0) + - MessageInputBar (0.1.0): + - MessageInputBar/AttachmentManager (= 0.1.0) + - MessageInputBar/AutocompleteManager (= 0.1.0) + - MessageInputBar/AttachmentManager (0.1.0) + - MessageInputBar/AutocompleteManager (0.1.0) DEPENDENCIES: - MessageInputBar (from `../`) + - MessageInputBar/AttachmentManager (from `../`) + - MessageInputBar/AutocompleteManager (from `../`) EXTERNAL SOURCES: MessageInputBar: :path: "../" SPEC CHECKSUMS: - MessageInputBar: 3e04412ea8a386b41088012f8c9bd67e665a70c1 + MessageInputBar: 355bc391ea24f79b0eb47e278aa3a94165d63f53 -PODFILE CHECKSUM: 226791787b6af407d42da6f779d208a2567eec5a +PODFILE CHECKSUM: 59ceff645e2e779969697ee2390f43a1a67332f9 COCOAPODS: 1.5.2 diff --git a/MessageInputBar.podspec b/MessageInputBar.podspec index bf88382..80243ad 100644 --- a/MessageInputBar.podspec +++ b/MessageInputBar.podspec @@ -1,28 +1,42 @@ Pod::Spec.new do |s| - # 1 - Specs - s.platform = :ios - s.name = 'MessageInputBar' - s.summary = 'A powerful InputAccessoryView ideal for messaging applications.' - s.source = { :git => 'https://github.com/MessageKit/MessageInputBar.git', :tag => s.version } - s.homepage = 'https://github.com/MessageKit/MessageInputBar' - s.requires_arc = true - - # 2 - Version - s.version = '0.1.0' - s.pod_target_xcconfig = { + # 1 - Specs + s.platform = :ios + s.name = 'MessageInputBar' + s.summary = 'A powerful InputAccessoryView ideal for messaging applications.' + s.source = { :git => 'https://github.com/MessageKit/MessageInputBar.git', :tag => s.version } + s.homepage = 'https://github.com/MessageKit/MessageInputBar' + s.requires_arc = true + + # 2 - Version + s.version = '0.1.0' + s.pod_target_xcconfig = { "SWIFT_VERSION" => "4.0", - } - s.ios.deployment_target = '9.0' + } + s.ios.deployment_target = '9.0' + + # 3 - License + s.license = { :type => "MIT", :file => "LICENSE.md" } + + # 4 - Author + s.social_media_url = 'https://twitter.com/nathantannar4' + s.author = { "Nathan Tannar" => "nathantannar4@gmail.com" } + + # 5 - Source Files + s.default_subspecs = 'Core' - # 3 - License - s.license = { :type => "MIT", :file => "LICENSE.md" } + s.subspec 'Core' do |ss| + ss.source_files = 'Sources/**/*.swift' + end - # 4 - Author - s.social_media_url = 'https://twitter.com/nathantannar4' - s.author = { "Nathan Tannar" => "nathantannar4@gmail.com" } + s.subspec 'AttachmentManager' do |b| + b.source_files = "Plugins/AttachmentManager/**/*.swift" + b.dependency 'MessageInputBar/Core' + end - # 5 - Source Files - s.source_files = 'Sources/**/*.swift' + s.subspec 'AutocompleteManager' do |c| + c.source_files = "Plugins/AutocompleteManager/**/*.swift" + c.dependency 'MessageInputBar/Core' + end end diff --git a/MessageInputBar.xcodeproj/project.pbxproj b/MessageInputBar.xcodeproj/project.pbxproj index f96e5a1..88cccf3 100644 --- a/MessageInputBar.xcodeproj/project.pbxproj +++ b/MessageInputBar.xcodeproj/project.pbxproj @@ -8,6 +8,23 @@ /* Begin PBXBuildFile section */ 380F6E7520C4A9A80095975F /* InputItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F6E7420C4A9A80095975F /* InputItem.swift */; }; + 380F6E7C20C4BB0E0095975F /* InputPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F6E7B20C4BB0E0095975F /* InputPlugin.swift */; }; + 380F6E8020C4BB950095975F /* AutocompleteManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F6E7E20C4BB950095975F /* AutocompleteManager.swift */; }; + 380F6E8520C4BD480095975F /* AutocompleteCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F6E8420C4BD480095975F /* AutocompleteCompletion.swift */; }; + 380F6E8720C4BD720095975F /* AutocompleteSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F6E8620C4BD720095975F /* AutocompleteSession.swift */; }; + 380F6E8A20C4BE5C0095975F /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F6E8820C4BE5C0095975F /* String+Extensions.swift */; }; + 380F6E8B20C4BE5C0095975F /* UITextView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F6E8920C4BE5C0095975F /* UITextView+Extensions.swift */; }; + 380F6E8E20C4BE6F0095975F /* AutocompleteManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F6E8C20C4BE6E0095975F /* AutocompleteManagerDelegate.swift */; }; + 380F6E8F20C4BE6F0095975F /* AutocompleteManagerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F6E8D20C4BE6E0095975F /* AutocompleteManagerDataSource.swift */; }; + 380F6E9420C4BE980095975F /* AutocompleteTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F6E9320C4BE980095975F /* AutocompleteTableView.swift */; }; + 380F6E9620C4BEA60095975F /* AutocompleteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F6E9520C4BEA50095975F /* AutocompleteCell.swift */; }; + 380F6E9920C4BF0E0095975F /* NSMutableAttributedString+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F6E9820C4BF0D0095975F /* NSMutableAttributedString+Extensions.swift */; }; + 380F6E9C20C4D84A0095975F /* AttachmentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F6E9A20C4D84A0095975F /* AttachmentCell.swift */; }; + 380F6E9D20C4D84A0095975F /* ImageAttachmentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F6E9B20C4D84A0095975F /* ImageAttachmentCell.swift */; }; + 380F6E9F20C4D8530095975F /* AttachmentCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F6E9E20C4D8530095975F /* AttachmentCollectionView.swift */; }; + 380F6EA220C4D85A0095975F /* AttachmentManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F6EA020C4D85A0095975F /* AttachmentManagerDelegate.swift */; }; + 380F6EA320C4D85A0095975F /* AttachmentManagerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F6EA120C4D85A0095975F /* AttachmentManagerDataSource.swift */; }; + 380F6EA520C4D86D0095975F /* AttachmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 380F6EA420C4D86D0095975F /* AttachmentManager.swift */; }; 386FB18620C496C1006A93BA /* MessageInputBar.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 386FB17C20C496C1006A93BA /* MessageInputBar.framework */; }; 386FB18D20C496C1006A93BA /* MessageInputBar.h in Headers */ = {isa = PBXBuildFile; fileRef = 386FB17F20C496C1006A93BA /* MessageInputBar.h */; settings = {ATTRIBUTES = (Public, ); }; }; 386FB19C20C499B3006A93BA /* InputBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 386FB19620C499B2006A93BA /* InputBarButtonItem.swift */; }; @@ -38,6 +55,23 @@ 380F6E7020C4A6FE0095975F /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 380F6E7120C4A6FE0095975F /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; 380F6E7420C4A9A80095975F /* InputItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputItem.swift; sourceTree = ""; }; + 380F6E7B20C4BB0E0095975F /* InputPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputPlugin.swift; sourceTree = ""; }; + 380F6E7E20C4BB950095975F /* AutocompleteManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutocompleteManager.swift; sourceTree = ""; }; + 380F6E8420C4BD480095975F /* AutocompleteCompletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteCompletion.swift; sourceTree = ""; }; + 380F6E8620C4BD720095975F /* AutocompleteSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteSession.swift; sourceTree = ""; }; + 380F6E8820C4BE5C0095975F /* String+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; + 380F6E8920C4BE5C0095975F /* UITextView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITextView+Extensions.swift"; sourceTree = ""; }; + 380F6E8C20C4BE6E0095975F /* AutocompleteManagerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutocompleteManagerDelegate.swift; sourceTree = ""; }; + 380F6E8D20C4BE6E0095975F /* AutocompleteManagerDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutocompleteManagerDataSource.swift; sourceTree = ""; }; + 380F6E9320C4BE980095975F /* AutocompleteTableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutocompleteTableView.swift; sourceTree = ""; }; + 380F6E9520C4BEA50095975F /* AutocompleteCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutocompleteCell.swift; sourceTree = ""; }; + 380F6E9820C4BF0D0095975F /* NSMutableAttributedString+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSMutableAttributedString+Extensions.swift"; sourceTree = ""; }; + 380F6E9A20C4D84A0095975F /* AttachmentCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentCell.swift; sourceTree = ""; }; + 380F6E9B20C4D84A0095975F /* ImageAttachmentCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageAttachmentCell.swift; sourceTree = ""; }; + 380F6E9E20C4D8530095975F /* AttachmentCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentCollectionView.swift; sourceTree = ""; }; + 380F6EA020C4D85A0095975F /* AttachmentManagerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentManagerDelegate.swift; sourceTree = ""; }; + 380F6EA120C4D85A0095975F /* AttachmentManagerDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentManagerDataSource.swift; sourceTree = ""; }; + 380F6EA420C4D86D0095975F /* AttachmentManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentManager.swift; sourceTree = ""; }; 386FB17C20C496C1006A93BA /* MessageInputBar.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MessageInputBar.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 386FB17F20C496C1006A93BA /* MessageInputBar.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MessageInputBar.h; sourceTree = ""; }; 386FB18020C496C1006A93BA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -79,6 +113,7 @@ isa = PBXGroup; children = ( 380F6E7420C4A9A80095975F /* InputItem.swift */, + 380F6E7B20C4BB0E0095975F /* InputPlugin.swift */, 386FB19920C499B3006A93BA /* MessageInputBarDelegate.swift */, ); path = Protocols; @@ -88,6 +123,7 @@ isa = PBXGroup; children = ( 386FB1A420C49B39006A93BA /* UIView+Extensions.swift */, + 380F6E9820C4BF0D0095975F /* NSMutableAttributedString+Extensions.swift */, ); path = Extensions; sourceTree = ""; @@ -118,12 +154,99 @@ path = Models; sourceTree = ""; }; + 380F6E7D20C4BB730095975F /* Plugins */ = { + isa = PBXGroup; + children = ( + 380F6E8220C4BBAD0095975F /* AttachmentManager */, + 380F6E8320C4BBB50095975F /* AutocompleteManager */, + ); + path = Plugins; + sourceTree = ""; + }; + 380F6E8220C4BBAD0095975F /* AttachmentManager */ = { + isa = PBXGroup; + children = ( + 380F6EA420C4D86D0095975F /* AttachmentManager.swift */, + 380F6EA620C4D8750095975F /* Views */, + 380F6EA720C4D87C0095975F /* Protocols */, + ); + path = AttachmentManager; + sourceTree = ""; + }; + 380F6E8320C4BBB50095975F /* AutocompleteManager */ = { + isa = PBXGroup; + children = ( + 380F6E7E20C4BB950095975F /* AutocompleteManager.swift */, + 380F6E9720C4BEA90095975F /* Views */, + 380F6E9220C4BE7E0095975F /* Models */, + 380F6E9120C4BE780095975F /* Protocols */, + 380F6E9020C4BE720095975F /* Extensions */, + ); + path = AutocompleteManager; + sourceTree = ""; + }; + 380F6E9020C4BE720095975F /* Extensions */ = { + isa = PBXGroup; + children = ( + 380F6E8820C4BE5C0095975F /* String+Extensions.swift */, + 380F6E8920C4BE5C0095975F /* UITextView+Extensions.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 380F6E9120C4BE780095975F /* Protocols */ = { + isa = PBXGroup; + children = ( + 380F6E8D20C4BE6E0095975F /* AutocompleteManagerDataSource.swift */, + 380F6E8C20C4BE6E0095975F /* AutocompleteManagerDelegate.swift */, + ); + path = Protocols; + sourceTree = ""; + }; + 380F6E9220C4BE7E0095975F /* Models */ = { + isa = PBXGroup; + children = ( + 380F6E8420C4BD480095975F /* AutocompleteCompletion.swift */, + 380F6E8620C4BD720095975F /* AutocompleteSession.swift */, + ); + path = Models; + sourceTree = ""; + }; + 380F6E9720C4BEA90095975F /* Views */ = { + isa = PBXGroup; + children = ( + 380F6E9520C4BEA50095975F /* AutocompleteCell.swift */, + 380F6E9320C4BE980095975F /* AutocompleteTableView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 380F6EA620C4D8750095975F /* Views */ = { + isa = PBXGroup; + children = ( + 380F6E9A20C4D84A0095975F /* AttachmentCell.swift */, + 380F6E9B20C4D84A0095975F /* ImageAttachmentCell.swift */, + 380F6E9E20C4D8530095975F /* AttachmentCollectionView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 380F6EA720C4D87C0095975F /* Protocols */ = { + isa = PBXGroup; + children = ( + 380F6EA020C4D85A0095975F /* AttachmentManagerDelegate.swift */, + 380F6EA120C4D85A0095975F /* AttachmentManagerDataSource.swift */, + ); + path = Protocols; + sourceTree = ""; + }; 386FB17220C496C1006A93BA = { isa = PBXGroup; children = ( 380F6E7020C4A6FE0095975F /* README.md */, 380F6E7120C4A6FE0095975F /* CHANGELOG.md */, 380F6E6F20C4A6FE0095975F /* MessageInputBar.podspec */, + 380F6E7D20C4BB730095975F /* Plugins */, 386FB17E20C496C1006A93BA /* Sources */, 386FB18920C496C1006A93BA /* Tests */, 386FB17D20C496C1006A93BA /* Products */, @@ -274,15 +397,32 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 380F6E8020C4BB950095975F /* AutocompleteManager.swift in Sources */, + 380F6EA320C4D85A0095975F /* AttachmentManagerDataSource.swift in Sources */, + 380F6EA220C4D85A0095975F /* AttachmentManagerDelegate.swift in Sources */, + 380F6E7C20C4BB0E0095975F /* InputPlugin.swift in Sources */, 386FB19E20C499B3006A93BA /* InputTextView.swift in Sources */, 386FB19F20C499B3006A93BA /* MessageInputBarDelegate.swift in Sources */, 380F6E7520C4A9A80095975F /* InputItem.swift in Sources */, + 380F6E8720C4BD720095975F /* AutocompleteSession.swift in Sources */, + 380F6EA520C4D86D0095975F /* AttachmentManager.swift in Sources */, + 380F6E8F20C4BE6F0095975F /* AutocompleteManagerDataSource.swift in Sources */, 386FB1A020C499B3006A93BA /* MessageInputBar.swift in Sources */, 386FB1A120C499B3006A93BA /* SeparatorLine.swift in Sources */, + 380F6E9620C4BEA60095975F /* AutocompleteCell.swift in Sources */, 386FB19D20C499B3006A93BA /* InputStackView.swift in Sources */, + 380F6E8A20C4BE5C0095975F /* String+Extensions.swift in Sources */, + 380F6E9D20C4D84A0095975F /* ImageAttachmentCell.swift in Sources */, + 380F6E9C20C4D84A0095975F /* AttachmentCell.swift in Sources */, 386FB1A320C49A9B006A93BA /* NSConstraintLayoutSet.swift in Sources */, + 380F6E8520C4BD480095975F /* AutocompleteCompletion.swift in Sources */, + 380F6E9920C4BF0E0095975F /* NSMutableAttributedString+Extensions.swift in Sources */, + 380F6E8E20C4BE6F0095975F /* AutocompleteManagerDelegate.swift in Sources */, 386FB1A520C49B39006A93BA /* UIView+Extensions.swift in Sources */, + 380F6E9420C4BE980095975F /* AutocompleteTableView.swift in Sources */, + 380F6E9F20C4D8530095975F /* AttachmentCollectionView.swift in Sources */, 386FB19C20C499B3006A93BA /* InputBarButtonItem.swift in Sources */, + 380F6E8B20C4BE5C0095975F /* UITextView+Extensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Plugins/AttachmentManager/AttachmentManager.swift b/Plugins/AttachmentManager/AttachmentManager.swift new file mode 100755 index 0000000..63c2a0a --- /dev/null +++ b/Plugins/AttachmentManager/AttachmentManager.swift @@ -0,0 +1,222 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +open class AttachmentManager: NSObject, InputPlugin { + + public enum Attachment { + case image(UIImage) + case url(URL) + case data(Data) + } + + // MARK: - Properties [Public] + + /// A protocol that can recieve notifications from the `AttachmentManager` + open weak var delegate: AttachmentManagerDelegate? + + /// A protocol to passes data to the `AttachmentManager` + open weak var dataSource: AttachmentManagerDataSource? + + open lazy var attachmentView: AttachmentCollectionView = { [weak self] in + let attachmentView = AttachmentCollectionView() + attachmentView.dataSource = self + attachmentView.delegate = self + return attachmentView + }() + + /// The attachments that the managers holds + private(set) public var attachments = [Attachment]() { didSet { reloadData() } } + + /// A flag you can use to determine if you want the manager to be always visible + open var isPersistent = false { didSet { attachmentView.reloadData() } } + + /// A flag to determine if the AddAttachmentCell is visible + open var showAddAttachmentCell = true { didSet { attachmentView.reloadData() } } + + // MARK: - Initialization + + public override init() { + super.init() + } + + // MARK: - InputManager + + open func reloadData() { + attachmentView.reloadData() + delegate?.attachmentManager(self, didReloadTo: attachments) + delegate?.attachmentManager(self, shouldBecomeVisible: attachments.count > 0 || isPersistent) + } + + /// Invalidates the `AttachmentManagers` session by removing all attachments + open func invalidate() { + attachments = [] + } + + /// Appends the object to the attachments + /// + /// - Parameter object: The object to append + open func handleInput(of object: AnyObject) -> Bool { + let attachment: Attachment + if let image = object as? UIImage { + attachment = .image(image) + } else if let url = object as? URL { + attachment = .url(url) + } else if let data = object as? Data { + attachment = .data(data) + } else { + return false + } + insertAttachment(attachment, at: attachments.count) + return true + } + + // MARK: - API [Public] + + /// Performs an animated insertion of an attachment at an index + /// + /// - Parameter index: The index to insert the attachment at + open func insertAttachment(_ attachment: Attachment, at index: Int) { + + attachmentView.performBatchUpdates({ + self.attachments.insert(attachment, at: index) + self.attachmentView.insertItems(at: [IndexPath(row: index, section: 0)]) + }, completion: { success in + self.attachmentView.reloadData() + self.delegate?.attachmentManager(self, didInsert: attachment, at: index) + self.delegate?.attachmentManager(self, shouldBecomeVisible: self.attachments.count > 0 || self.isPersistent) + }) + } + + /// Performs an animated removal of an attachment at an index + /// + /// - Parameter index: The index to remove the attachment at + open func removeAttachment(at index: Int) { + + let attachment = attachments[index] + attachmentView.performBatchUpdates({ + self.attachments.remove(at: index) + self.attachmentView.deleteItems(at: [IndexPath(row: index, section: 0)]) + }, completion: { success in + self.attachmentView.reloadData() + self.delegate?.attachmentManager(self, didRemove: attachment, at: index) + self.delegate?.attachmentManager(self, shouldBecomeVisible: self.attachments.count > 0 || self.isPersistent) + }) + } + +} + +extension AttachmentManager: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + + // MARK: - UICollectionViewDelegate + + final public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + if indexPath.row == attachments.count { + delegate?.attachmentManager(self, didSelectAddAttachmentAt: indexPath.row) + delegate?.attachmentManager(self, shouldBecomeVisible: attachments.count > 0 || isPersistent) + } + } + + // MARK: - UICollectionViewDataSource + + final public func numberOfItems(inSection section: Int) -> Int { + return 1 + } + + final public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return attachments.count + (showAddAttachmentCell ? 1 : 0) + } + + final public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + + if indexPath.row == attachments.count && showAddAttachmentCell { + return addAttachmentCell(in: collectionView, at: indexPath) + } + + let attachment = attachments[indexPath.row] + + if let cell = dataSource?.attachmentManager(self, cellFor: attachment, at: indexPath.row) { + return cell + } else { + + // Only images are supported by default + switch attachment { + case .image(let image): + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ImageAttachmentCell.reuseIdentifier, for: indexPath) as? ImageAttachmentCell else { + fatalError() + } + cell.attachment = attachment + cell.indexPath = indexPath + cell.manager = self + cell.imageView.image = image + return cell + default: + return collectionView.dequeueReusableCell(withReuseIdentifier: AttachmentCell.reuseIdentifier, for: indexPath) as! AttachmentCell + } + + } + } + + // MARK: - UICollectionViewDelegateFlowLayout + + final public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + + var height = attachmentView.intrinsicContentHeight + if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout { + height -= (layout.sectionInset.bottom + layout.sectionInset.top + collectionView.contentInset.top + collectionView.contentInset.bottom) + } + return CGSize(width: height, height: height) + } + + private func addAttachmentCell(in collectionView: UICollectionView, at indexPath: IndexPath) -> AttachmentCell { + + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AttachmentCell.reuseIdentifier, for: indexPath) as? AttachmentCell else { + fatalError() + } + cell.deleteButton.isHidden = true + // Draw a plus + let frame = CGRect(origin: CGPoint(x: cell.bounds.origin.x, + y: cell.bounds.origin.y), + size: CGSize(width: cell.bounds.width - cell.padding.left - cell.padding.right, + height: cell.bounds.height - cell.padding.top - cell.padding.bottom)) + let strokeWidth: CGFloat = 3 + let length: CGFloat = frame.width / 2 + let vLayer = CAShapeLayer() + vLayer.path = UIBezierPath(roundedRect: CGRect(x: frame.midX - (strokeWidth / 2), + y: frame.midY - (length / 2), + width: strokeWidth, + height: length), cornerRadius: 5).cgPath + vLayer.fillColor = UIColor.lightGray.cgColor + let hLayer = CAShapeLayer() + hLayer.path = UIBezierPath(roundedRect: CGRect(x: frame.midX - (length / 2), + y: frame.midY - (strokeWidth / 2), + width: length, + height: strokeWidth), cornerRadius: 5).cgPath + hLayer.fillColor = UIColor.lightGray.cgColor + cell.containerView.layer.addSublayer(vLayer) + cell.containerView.layer.addSublayer(hLayer) + return cell + } +} diff --git a/Plugins/AttachmentManager/Protocols/AttachmentManagerDataSource.swift b/Plugins/AttachmentManager/Protocols/AttachmentManagerDataSource.swift new file mode 100755 index 0000000..34990c3 --- /dev/null +++ b/Plugins/AttachmentManager/Protocols/AttachmentManagerDataSource.swift @@ -0,0 +1,38 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import Foundation + +/// AttachmentManagerDataSource is a protocol to passes data to the AttachmentManager +public protocol AttachmentManagerDataSource: class { + + /// The AttachmentCell for the attachment that is to be inserted into the AttachmentView + /// + /// - Parameters: + /// - manager: The AttachmentManager + /// - attachment: The object + /// - index: The index in the AttachmentView + /// - Returns: An AttachmentCell + func attachmentManager(_ manager: AttachmentManager, cellFor attachment: AttachmentManager.Attachment, at index: Int) -> AttachmentCell +} diff --git a/Plugins/AttachmentManager/Protocols/AttachmentManagerDelegate.swift b/Plugins/AttachmentManager/Protocols/AttachmentManagerDelegate.swift new file mode 100755 index 0000000..330aeda --- /dev/null +++ b/Plugins/AttachmentManager/Protocols/AttachmentManagerDelegate.swift @@ -0,0 +1,78 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +/// AttachmentManagerDelegate is a protocol that can recieve notifications from the AttachmentManager +public protocol AttachmentManagerDelegate: class { + + /// Can be used to determine if the AttachmentManager should be inserted into an InputStackView + /// + /// - Parameters: + /// - manager: The AttachmentManager + /// - shouldBecomeVisible: If the AttachmentManager should be presented or dismissed + func attachmentManager(_ manager: AttachmentManager, shouldBecomeVisible: Bool) + + + /// Notifys when an attachment has been inserted into the AttachmentManager + /// + /// - Parameters: + /// - manager: The AttachmentManager + /// - attachment: The attachment that was inserted + /// - index: The index of the attachment in the AttachmentManager's attachments array + func attachmentManager(_ manager: AttachmentManager, didInsert attachment: AttachmentManager.Attachment, at index: Int) + + /// Notifys when an attachment has been removed from the AttachmentManager + /// + /// - Parameters: + /// - manager: The AttachmentManager + /// - attachment: The attachment that was removed + /// - index: The index of the attachment in the AttachmentManager's attachments array + func attachmentManager(_ manager: AttachmentManager, didRemove attachment: AttachmentManager.Attachment, at index: Int) + + /// Notifys when the AttachmentManager was reloaded + /// + /// - Parameters: + /// - manager: The AttachmentManager + /// - attachments: The AttachmentManager's attachments array + func attachmentManager(_ manager: AttachmentManager, didReloadTo attachments: [AttachmentManager.Attachment]) + + /// Notifys when the AddAttachmentCell was selected + /// + /// - Parameters: + /// - manager: The AttachmentManager + /// - attachments: The index of the AddAttachmentCell + func attachmentManager(_ manager: AttachmentManager, didSelectAddAttachmentAt index: Int) +} + +public extension AttachmentManagerDelegate { + + func attachmentManager(_ manager: AttachmentManager, didInsert attachment: AttachmentManager.Attachment, at index: Int) {} + + func attachmentManager(_ manager: AttachmentManager, didRemove attachment: AttachmentManager.Attachment, at index: Int) {} + + func attachmentManager(_ manager: AttachmentManager, didReloadTo attachments: [AttachmentManager.Attachment]) {} + + func attachmentManager(_ manager: AttachmentManager, didSelectAddAttachmentAt index: Int) {} +} diff --git a/Plugins/AttachmentManager/Views/AttachmentCell.swift b/Plugins/AttachmentManager/Views/AttachmentCell.swift new file mode 100755 index 0000000..0335569 --- /dev/null +++ b/Plugins/AttachmentManager/Views/AttachmentCell.swift @@ -0,0 +1,129 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +open class AttachmentCell: UICollectionViewCell { + + // MARK: - Properties + + open class var reuseIdentifier: String { + return "AttachmentCell" + } + + open let containerView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .groupTableViewBackground + view.layer.cornerRadius = 8 + view.clipsToBounds = true + return view + }() + + open var padding: UIEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5) { + didSet { + updateContainerPadding() + } + } + + open lazy var deleteButton: UIButton = { [weak self] in + let button = UIButton() + button.setAttributedTitle(NSMutableAttributedString().bold("X", fontSize: 15, textColor: .white), for: .normal) + button.setAttributedTitle(NSMutableAttributedString().bold("X", fontSize: 15, textColor: UIColor.white.withAlphaComponent(0.5)), for: .highlighted) + button.layer.cornerRadius = 10 + button.clipsToBounds = true + button.backgroundColor = UIColor(red: 0, green: 122/255, blue: 1, alpha: 1) + button.addTarget(self, action: #selector(deleteAttachment), for: .touchUpInside) + return button + }() + + open var attachment: AttachmentManager.Attachment? + + open var indexPath: IndexPath? + + open weak var manager: AttachmentManager? + + private var containerViewLayoutSet: NSLayoutConstraintSet? + + // MARK: - Initialization + + public override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + open override func prepareForReuse() { + super.prepareForReuse() + indexPath = nil + manager = nil + attachment = nil + } + + // MARK: - Setup + + private func setup() { + + setupSubviews() + setupConstraints() + } + + private func setupSubviews() { + + contentView.addSubview(containerView) + contentView.addSubview(deleteButton) + } + + private func setupConstraints() { + + containerViewLayoutSet = NSLayoutConstraintSet( + top: containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding.top), + bottom: containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -padding.bottom), + left: containerView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: padding.left), + right: containerView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -padding.right) + ).activate() + deleteButton.addConstraints(contentView.topAnchor, right: contentView.rightAnchor, widthConstant: 20, heightConstant: 20) + } + + private func updateContainerPadding() { + + containerViewLayoutSet?.top?.constant = padding.top + containerViewLayoutSet?.bottom?.constant = -padding.bottom + containerViewLayoutSet?.left?.constant = padding.left + containerViewLayoutSet?.right?.constant = -padding.right + } + + // MARK: - User Actions + + @objc + func deleteAttachment() { + + guard let index = indexPath?.row else { return } + manager?.removeAttachment(at: index) + } +} diff --git a/Plugins/AttachmentManager/Views/AttachmentCollectionView.swift b/Plugins/AttachmentManager/Views/AttachmentCollectionView.swift new file mode 100755 index 0000000..7445c1b --- /dev/null +++ b/Plugins/AttachmentManager/Views/AttachmentCollectionView.swift @@ -0,0 +1,76 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +open class AttachmentCollectionView: UICollectionView { + + // MARK: - Properties + + open var intrinsicContentHeight: CGFloat = 100 { + didSet { + invalidateIntrinsicContentSize() + } + } + + open override var intrinsicContentSize: CGSize { + return CGSize(width: 0, height: intrinsicContentHeight) + } + + // MARK: - Initialization + + public init() { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.minimumLineSpacing = 0 + layout.sectionInset.top = 5 + layout.sectionInset.bottom = 5 + layout.headerReferenceSize = CGSize(width: 12, height: 0) + layout.footerReferenceSize = CGSize(width: 12, height: 0) + super.init(frame: .zero, collectionViewLayout: layout) + setup() + } + + public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { + super.init(frame: frame, collectionViewLayout: layout) + setup() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + // MARK: - Setup + + private func setup() { + + backgroundColor = .white + alwaysBounceHorizontal = true + showsHorizontalScrollIndicator = true + setContentHuggingPriority(UILayoutPriority.defaultHigh, for: .vertical) + register(AttachmentCell.self, forCellWithReuseIdentifier: AttachmentCell.reuseIdentifier) + register(ImageAttachmentCell.self, forCellWithReuseIdentifier: ImageAttachmentCell.reuseIdentifier) + } +} diff --git a/Plugins/AttachmentManager/Views/ImageAttachmentCell.swift b/Plugins/AttachmentManager/Views/ImageAttachmentCell.swift new file mode 100755 index 0000000..4824c7b --- /dev/null +++ b/Plugins/AttachmentManager/Views/ImageAttachmentCell.swift @@ -0,0 +1,64 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +open class ImageAttachmentCell: AttachmentCell { + + // MARK: - Properties + + open override class var reuseIdentifier: String { + return "ImageAttachmentCell" + } + + open let imageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + return imageView + }() + + // MARK: - Initialization + + public override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + open override func prepareForReuse() { + super.prepareForReuse() + imageView.image = nil + } + + // MARK: - Setup + + private func setup() { + containerView.addSubview(imageView) + imageView.fillSuperview() + } +} diff --git a/Plugins/AutocompleteManager/AutocompleteManager.swift b/Plugins/AutocompleteManager/AutocompleteManager.swift new file mode 100755 index 0000000..a951557 --- /dev/null +++ b/Plugins/AutocompleteManager/AutocompleteManager.swift @@ -0,0 +1,342 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +open class AutocompleteManager: NSObject, InputPlugin { + + // MARK: - Properties [Public] + + /// A protocol that passes data to the `AutocompleteManager` + open weak var dataSource: AutocompleteManagerDataSource? + + /// A protocol that more precisely defines `AutocompleteManager` logic + open weak var delegate: AutocompleteManagerDelegate? + + /// A reference to the `InputTextView` that the `AutocompleteManager` is using + private(set) public weak var inputTextView: InputTextView? + + /// An ongoing session reference that holds the prefix, range and text to complete with + private(set) public var currentSession: AutocompleteSession? { didSet { layoutIfNeeded() } } + + /// The `AutocompleteTableView` that renders available autocompletes for the `currentSession` + open lazy var tableView: AutocompleteTableView = { [weak self] in + let tableView = AutocompleteTableView() + tableView.register(AutocompleteCell.self, forCellReuseIdentifier: AutocompleteCell.reuseIdentifier) + tableView.separatorStyle = .none + tableView.backgroundColor = .white + tableView.rowHeight = 44 + tableView.delegate = self + tableView.dataSource = self + return tableView + }() + + /// If the autocomplete matches should be made by casting the strings to lowercase. + /// Default value is `FALSE` + open var isCaseSensitive = false + + /// Adds an additional space after the autocompleted text when true. + /// Default value is `TRUE` + open var appendSpaceOnCompletion = true + + /// Keeps the prefix typed when text is autocompleted. + /// Default value is `TRUE` + open var keepPrefixOnCompletion = true + + /// The default text attributes + open var defaultTextAttributes: [NSAttributedStringKey: Any] = + [.font: UIFont.preferredFont(forTextStyle: .body), .foregroundColor: UIColor.black] + + // MARK: - Properties [Private] + + /// The prefices that the manager will recognize + public private(set) var autocompletePrefixes = Set() + + /// The text attributes applied to highlighted substrings for each prefix + public private(set) var autocompleteTextAttributes = [String: [NSAttributedStringKey: Any]]() + + /// A key used for referencing which substrings were autocompletes + private let NSAttributedAutocompleteKey = NSAttributedStringKey.init("com.messagekit.MessageInputBar.autocompletekey") + + /// The NSAttributedStringKey.paragraphStyle value applied to attributed strings + private let paragraphStyle: NSMutableParagraphStyle = { + let style = NSMutableParagraphStyle() + style.paragraphSpacingBefore = 2 + style.lineHeightMultiple = 1 + return style + }() + + /// A reference to `defaultTextAttributes` that adds the NSAttributedAutocompleteKey + private var typingTextAttributes: [NSAttributedStringKey: Any] { + var attributes = defaultTextAttributes + attributes[NSAttributedAutocompleteKey] = false + attributes[.paragraphStyle] = paragraphStyle + return attributes + } + + /// The current autocomplete text options filtered by the text after the prefix + private var currentAutocompleteOptions: [AutocompleteCompletion] { + + guard let session = currentSession, let completions = dataSource?.autocompleteManager(self, autocompleteSourceFor: session.prefix) else { return [] } + guard !session.filter.isEmpty else { return completions } + guard isCaseSensitive else { return completions.filter { $0.text.lowercased().contains(session.filter.lowercased()) } } + return completions.filter { $0.text.contains(session.filter) } + } + + // MARK: - Initialization + + public init(for textView: InputTextView) { + super.init() + self.inputTextView = textView + self.inputTextView?.delegate = self + } + + // MARK: - InputPlugin + + /// Reloads the InputPlugin's session + open func reloadData() { + + guard let result = inputTextView?.find(prefixes: autocompletePrefixes) else { + invalidate() + return + } + let wordWithoutPrefix = (result.word as NSString).substring(from: result.prefix.utf16.count) + guard let session = AutocompleteSession(prefix: result.prefix, range: result.range, filter: wordWithoutPrefix) else { return } + registerCurrentSession(to: session) + } + + /// Invalidates the InputPlugin's session + open func invalidate() { + unregisterCurrentSession() + } + + /// Passes an object into the InputPlugin's session to handle + /// + /// - Parameter object: A string to append + open func handleInput(of object: AnyObject) -> Bool { + guard let newText = object as? String, let textView = inputTextView else { return false } + let attributedString = NSMutableAttributedString(attributedString: textView.attributedText) + let newAttributedString = NSAttributedString(string: newText, attributes: typingTextAttributes) + attributedString.append(newAttributedString) + textView.attributedText = attributedString + reloadData() + return true + } + + // MARK: - API [Public] + + open func register(prefix: String, with attributedTextAttributes: [NSAttributedStringKey:Any]? = nil) { + autocompletePrefixes.insert(prefix) + autocompleteTextAttributes[prefix] = attributedTextAttributes + autocompleteTextAttributes[prefix]?[.paragraphStyle] = paragraphStyle + } + + open func unregister(prefix: String) { + autocompletePrefixes.remove(prefix) + autocompleteTextAttributes[prefix] = nil + } + + /// Replaces the current prefix and filter text with the supplied text + /// + /// - Parameters: + /// - text: The replacement text + open func autocomplete(with session: AutocompleteSession) { + + guard let textView = inputTextView else { return } + guard delegate?.autocompleteManager(self, shouldComplete: session.prefix, with: session.filter) != false else { return } + + // Create a range that overlaps the prefix + let prefixLength = session.prefix.utf16.count + let insertionRange = NSRange( + location: session.range.location + (keepPrefixOnCompletion ? prefixLength : 0), + length: session.filter.utf16.count + (!keepPrefixOnCompletion ? prefixLength : 0) + ) + + // Transform range + guard let range = Range(insertionRange, in: textView.text) else { return } + let nsrange = NSRange(range, in: textView.text) + + // Replace the attributedText with a modified version + let autocomplete = session.completion?.text ?? "" + insertAutocomplete(autocomplete, at: session, for: nsrange) + + // Move Cursor to the end of the inserted text + let selectedLocation = insertionRange.location + autocomplete.utf16.count + (appendSpaceOnCompletion ? 1 : 0) + textView.selectedRange = NSRange( + location: selectedLocation, + length: 0 + ) + + // End the session + unregisterCurrentSession() + } + + // MARK: - API [Private] + + /// Resets the `InputTextView`'s typingAttributes to `defaultTextAttributes` + private func preserveTypingAttributes() { + + var typingAttributes = [String: Any]() + typingTextAttributes.forEach { typingAttributes[$0.key.rawValue] = $0.value } + inputTextView?.typingAttributes = typingAttributes + } + + + /// Inserts an autocomplete for a given selection + /// + /// - Parameters: + /// - autocomplete: The 'String' to autocomplete to + /// - sesstion: The 'AutocompleteSession' + /// - range: The 'NSRange' to insert over + private func insertAutocomplete(_ autocomplete: String, at session: AutocompleteSession, for range: NSRange) { + + guard let textView = inputTextView else { return } + + // Apply the autocomplete attributes + var attrs = autocompleteTextAttributes[session.prefix] ?? defaultTextAttributes + attrs[NSAttributedAutocompleteKey] = true + let newString = (keepPrefixOnCompletion ? session.prefix : "") + autocomplete + let newAttributedString = NSAttributedString(string: newString, attributes: attrs) + + // Modify the NSRange to include the prefix length + let rangeModifier = keepPrefixOnCompletion ? session.prefix.count : 0 + let highlightedRange = NSRange(location: range.location - rangeModifier, length: range.length + rangeModifier) + + // Replace the attributedText with a modified version including the autocompete + let newAttributedText = textView.attributedText.replacingCharacters(in: highlightedRange, with: newAttributedString) + if appendSpaceOnCompletion { + newAttributedText.append(NSAttributedString(string: " ", attributes: typingTextAttributes)) + } + textView.attributedText = newAttributedText + } + + /// Initializes a session with a new `AutocompleteSession` object + /// + /// - Parameters: + /// - session: The session to register + private func registerCurrentSession(to session: AutocompleteSession) { + + guard delegate?.autocompleteManager(self, shouldRegister: session.prefix, at: session.range) != false else { return } + currentSession = session + delegate?.autocompleteManager(self, shouldBecomeVisible: true) + } + + /// Invalidates the `currentSession` session if it existed + private func unregisterCurrentSession() { + + guard let session = currentSession else { return } + guard delegate?.autocompleteManager(self, shouldUnregister: session.prefix) != false else { return } + currentSession = nil + delegate?.autocompleteManager(self, shouldBecomeVisible: false) + } + + /// Calls the required methods to relayout the `AutocompleteTableView` in it's superview + private func layoutIfNeeded() { + + tableView.reloadData() + + // Resize the table to be fit properly in an `InputStackView` + tableView.invalidateIntrinsicContentSize() + + // Layout the table's superview + tableView.superview?.layoutIfNeeded() + } + +} + +extension AutocompleteManager: UITextViewDelegate { + + // MARK: - UITextViewDelegate + + public func textViewDidChange(_ textView: UITextView) { + reloadData() + } + + public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + + // Ensure that the text to be inserted is not using previous attributes + preserveTypingAttributes() + + // range.length > 0: Backspace/removing text + // range.lowerBound < textView.selectedRange.lowerBound: Ignore trying to delete + // the substring if the user is already doing so + if range.length > 0, range.lowerBound < textView.selectedRange.lowerBound { + + // Backspace/removing text + let attribute = textView.attributedText + .attributes(at: range.lowerBound, longestEffectiveRange: nil, in: range) + .filter { return $0.key == NSAttributedAutocompleteKey } + + if (attribute[NSAttributedAutocompleteKey] as? Bool ?? false) == true { + + // Remove the autocompleted substring + let lowerRange = NSRange(location: 0, length: range.location + 1) + textView.attributedText.enumerateAttribute(NSAttributedAutocompleteKey, in: lowerRange, options: .reverse, using: { (_, range, stop) in + + // Only delete the first found range + defer { stop.pointee = true } + + let emptyString = NSAttributedString(string: "", attributes: typingTextAttributes) + textView.attributedText = textView.attributedText.replacingCharacters(in: range, with: emptyString) + textView.selectedRange = NSRange(location: range.location, length: 0) + }) + unregisterCurrentSession() + return false + } + } + return true + } + +} + +extension AutocompleteManager: UITableViewDelegate, UITableViewDataSource { + + // MARK: - UITableViewDataSource + + final public func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + final public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return currentAutocompleteOptions.count + } + + final public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + guard var session = currentSession else { return UITableViewCell() } + session.completion = currentAutocompleteOptions[indexPath.row] + let cell = dataSource?.autocompleteManager(self, tableView: tableView, cellForRowAt: indexPath, for: session) ?? UITableViewCell() + return cell + } + + // MARK: - UITableViewDelegate + + final public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + + guard var session = currentSession else { return } + session.completion = currentAutocompleteOptions[indexPath.row] + autocomplete(with: session) + } + +} diff --git a/Plugins/AutocompleteManager/Extensions/String+Extensions.swift b/Plugins/AutocompleteManager/Extensions/String+Extensions.swift new file mode 100755 index 0000000..cb734c0 --- /dev/null +++ b/Plugins/AutocompleteManager/Extensions/String+Extensions.swift @@ -0,0 +1,63 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import Foundation + +extension String { + + func wordParts(_ range: Range) -> (left: String.SubSequence, right: String.SubSequence)? { + let whitespace = NSCharacterSet.whitespacesAndNewlines + let leftView = self[.. (word: String, range: Range)? { + guard !isEmpty, + let range = Range(nsrange, in: self), + let parts = self.wordParts(range) + else { return nil } + + // if the left-next character is whitespace, the "right word part" is the full word + // short circuit with the right word part + its range + if let characterBeforeRange = index(range.lowerBound, offsetBy: -1, limitedBy: startIndex), + let character = self[characterBeforeRange].unicodeScalars.first, + NSCharacterSet.whitespaces.contains(character) { + let right = parts.right + return (String(right), right.startIndex ..< right.endIndex) + } + + let joinedWord = String(parts.left + parts.right) + guard !joinedWord.isEmpty else { return nil } + + return (joinedWord, parts.left.startIndex ..< parts.right.endIndex) + } + +} diff --git a/Plugins/AutocompleteManager/Extensions/UITextView+Extensions.swift b/Plugins/AutocompleteManager/Extensions/UITextView+Extensions.swift new file mode 100755 index 0000000..64b32f6 --- /dev/null +++ b/Plugins/AutocompleteManager/Extensions/UITextView+Extensions.swift @@ -0,0 +1,63 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +extension UITextView { + + func find(prefixes: Set) -> (prefix: String, word: String, range: NSRange)? { + guard prefixes.count > 0, + let result = wordAtCaret, + !result.word.isEmpty + else { return nil } + for prefix in prefixes { + if result.word.hasPrefix(prefix) { + return (prefix, result.word, result.range) + } + } + return nil + } + + var wordAtCaret: (word: String, range: NSRange)? { + guard let caretRange = self.caretRange, + let result = text.word(at: caretRange) + else { return nil } + + let location = result.range.lowerBound.encodedOffset + let range = NSRange(location: location, length: result.range.upperBound.encodedOffset - location) + + return (result.word, range) + } + + var caretRange: NSRange? { + guard let selectedRange = self.selectedTextRange else { return nil } + return NSRange( + location: offset(from: beginningOfDocument, to: selectedRange.start), + length: offset(from: selectedRange.start, to: selectedRange.end) + ) + } + +} + + diff --git a/Plugins/AutocompleteManager/Models/AutocompleteCompletion.swift b/Plugins/AutocompleteManager/Models/AutocompleteCompletion.swift new file mode 100644 index 0000000..4cd12c5 --- /dev/null +++ b/Plugins/AutocompleteManager/Models/AutocompleteCompletion.swift @@ -0,0 +1,43 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import Foundation + +public struct AutocompleteCompletion { + + // The string used for sorting and to autocomplete a prefix + public var text: String + + // An optional string to display instead of `text`, for example emojis + public var displayText: String? + + public init(_ text: String) { + self.text = text + } + + public init(_ text: String, displayText: String) { + self.text = text + self.displayText = displayText + } +} diff --git a/Plugins/AutocompleteManager/Models/AutocompleteSession.swift b/Plugins/AutocompleteManager/Models/AutocompleteSession.swift new file mode 100644 index 0000000..363d5db --- /dev/null +++ b/Plugins/AutocompleteManager/Models/AutocompleteSession.swift @@ -0,0 +1,41 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import Foundation + +/// A structure containing data on the `AutocompleteManager`'s session +public struct AutocompleteSession { + + public let prefix: String + public var range: NSRange + public var filter: String + public var completion: AutocompleteCompletion? + + public init?(prefix: String?, range: NSRange?, filter: String?) { + guard let pfx = prefix, let rng = range, let flt = filter else { return nil } + self.prefix = pfx + self.range = rng + self.filter = flt + } +} diff --git a/Plugins/AutocompleteManager/Protocols/AutocompleteManagerDataSource.swift b/Plugins/AutocompleteManager/Protocols/AutocompleteManagerDataSource.swift new file mode 100755 index 0000000..e79ab1b --- /dev/null +++ b/Plugins/AutocompleteManager/Protocols/AutocompleteManagerDataSource.swift @@ -0,0 +1,64 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +/// AutocompleteManagerDataSource is a protocol that passes data to the AutocompleteManager +public protocol AutocompleteManagerDataSource: class { + + /// The autocomplete options for the registered prefix. + /// + /// - Parameters: + /// - manager: The AutocompleteManager + /// - prefix: The registered prefix + /// - Returns: An array of `AutocompleteCompletion` options for the given prefix + func autocompleteManager(_ manager: AutocompleteManager, autocompleteSourceFor prefix: String) -> [AutocompleteCompletion] + + /// The cell to populate the `AutocompleteTableView` with + /// + /// - Parameters: + /// - manager: The `AttachmentManager` that sources the UITableViewDataSource + /// - tableView: The `AttachmentManager`'s `AutocompleteTableView` + /// - indexPath: The `IndexPath` of the cell + /// - session: The current `Session` of the `AutocompleteManager` + /// - Returns: A UITableViewCell to populate the `AutocompleteTableView` + func autocompleteManager(_ manager: AutocompleteManager, tableView: UITableView, cellForRowAt indexPath: IndexPath, for session: AutocompleteSession) -> UITableViewCell +} + +public extension AutocompleteManagerDataSource { + + 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("AutocompleteCell is not registered") + } + + cell.textLabel?.attributedText = cell.attributedText(matching: session) + cell.backgroundColor = .white + cell.separatorLine.isHidden = tableView.numberOfRows(inSection: indexPath.section) - 1 == indexPath.row + return cell + + } + +} diff --git a/Plugins/AutocompleteManager/Protocols/AutocompleteManagerDelegate.swift b/Plugins/AutocompleteManager/Protocols/AutocompleteManagerDelegate.swift new file mode 100755 index 0000000..2058c00 --- /dev/null +++ b/Plugins/AutocompleteManager/Protocols/AutocompleteManagerDelegate.swift @@ -0,0 +1,79 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +/// AutocompleteManagerDelegate is a protocol that more precisely define AutocompleteManager logic +public protocol AutocompleteManagerDelegate: class { + + /// Can be used to determine if the AutocompleteManager should be inserted into an InputStackView + /// + /// - Parameters: + /// - manager: The AutocompleteManager + /// - shouldBecomeVisible: If the AutocompleteManager should be presented or dismissed + func autocompleteManager(_ manager: AutocompleteManager, shouldBecomeVisible: Bool) + + /// Determines if a prefix character should be registered to initialize the auto-complete selection table + /// + /// - Parameters: + /// - manager: The AutocompleteManager + /// - prefix: The prefix `Character` could be registered + /// - range: The `NSRange` of the prefix in the UITextView managed by the AutocompleteManager + /// - Returns: If the prefix should be registered. Default is TRUE + func autocompleteManager(_ manager: AutocompleteManager, shouldRegister prefix: String, at range: NSRange) -> Bool + + /// Determines if a prefix character should be unregistered to de-initialize the auto-complete selection table + /// + /// - Parameters: + /// - manager: The AutocompleteManager + /// - prefix: The prefix character could be unregistered + /// - range: The range of the prefix in the UITextView managed by the AutocompleteManager + /// - Returns: If the prefix should be unregistered. Default is TRUE + func autocompleteManager(_ manager: AutocompleteManager, shouldUnregister prefix: String) -> Bool + + /// Determines if a prefix character can should be autocompleted + /// + /// - Parameters: + /// - manager: The AutocompleteManager + /// - prefix: The prefix character that is currently registered + /// - text: The text to autocomplete with + /// - Returns: If the prefix can be autocompleted. Default is TRUE + func autocompleteManager(_ manager: AutocompleteManager, shouldComplete prefix: String, with text: String) -> Bool +} + +public extension AutocompleteManagerDelegate { + + func autocompleteManager(_ manager: AutocompleteManager, shouldRegister prefix: String, at range: NSRange) -> Bool { + return true + } + + func autocompleteManager(_ manager: AutocompleteManager, shouldUnregister prefix: String) -> Bool { + return true + } + + func autocompleteManager(_ manager: AutocompleteManager, shouldComplete prefix: String, with text: String) -> Bool { + return true + } +} + diff --git a/Plugins/AutocompleteManager/Views/AutocompleteCell.swift b/Plugins/AutocompleteManager/Views/AutocompleteCell.swift new file mode 100755 index 0000000..f3fec25 --- /dev/null +++ b/Plugins/AutocompleteManager/Views/AutocompleteCell.swift @@ -0,0 +1,102 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +open class AutocompleteCell: UITableViewCell { + + // MARK: - Properties + + open class var reuseIdentifier: String { + return "AutocompleteCell" + } + + /// A boarder line anchored to the top of the view + open let separatorLine = SeparatorLine() + + open var imageViewEdgeInsets: UIEdgeInsets = .zero + + // MARK: - Initialization + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) + setup() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + open override func prepareForReuse() { + super.prepareForReuse() + textLabel?.text = nil + detailTextLabel?.text = nil + imageView?.image = nil + imageViewEdgeInsets = .zero + separatorLine.backgroundColor = .lightGray + separatorLine.isHidden = false + } + + // MARK: - Setup + + private func setup() { + + setupSubviews() + setupConstraints() + } + + open func setupSubviews() { + + addSubview(separatorLine) + } + + open func setupConstraints() { + + separatorLine.addConstraints(left: leftAnchor, bottom: bottomAnchor, right: rightAnchor, heightConstant: separatorLine.height) + } + + open override func layoutSubviews() { + super.layoutSubviews() + guard let imageViewFrame = imageView?.frame else { return } + let imageViewOrigin = CGPoint(x: imageViewFrame.origin.x + imageViewEdgeInsets.left, y: imageViewFrame.origin.y + imageViewEdgeInsets.top) + let imageViewSize = CGSize(width: imageViewFrame.size.width - imageViewEdgeInsets.left - imageViewEdgeInsets.right, height: imageViewFrame.size.height - imageViewEdgeInsets.top - imageViewEdgeInsets.bottom) + imageView?.frame = CGRect(origin: imageViewOrigin, size: imageViewSize) + } + + // MARK: - API [Public] + + open func attributedText(matching session: AutocompleteSession) -> NSMutableAttributedString { + + let completionText = (session.completion?.displayText ?? session.completion?.text) ?? "" + + // Bolds the text that currently matches the filter + let matchingRange = (completionText as NSString).range(of: session.filter, options: .caseInsensitive) + let attributedString = NSMutableAttributedString().normal(completionText) + attributedString.addAttributes([.font: UIFont.boldSystemFont(ofSize: UIFont.preferredFont(forTextStyle: .body).pointSize)], range: matchingRange) + let stringWithPrefix = NSMutableAttributedString().normal(String(session.prefix)) + stringWithPrefix.append(attributedString) + return stringWithPrefix + } +} diff --git a/Plugins/AutocompleteManager/Views/AutocompleteTableView.swift b/Plugins/AutocompleteManager/Views/AutocompleteTableView.swift new file mode 100755 index 0000000..c1f853a --- /dev/null +++ b/Plugins/AutocompleteManager/Views/AutocompleteTableView.swift @@ -0,0 +1,42 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +open class AutocompleteTableView: UITableView { + + /// The max visible rows visible in the autocomplete table before the user has to scroll throught them + open var maxVisibleRows = 3 { + didSet { + invalidateIntrinsicContentSize() + } + } + + open override var intrinsicContentSize: CGSize { + + let rows = numberOfRows(inSection: 0) < maxVisibleRows ? numberOfRows(inSection: 0) : maxVisibleRows + return CGSize(width: super.intrinsicContentSize.width, height: (CGFloat(rows) * rowHeight)) + } + +} diff --git a/Sources/Extensions/NSMutableAttributedString+Extensions.swift b/Sources/Extensions/NSMutableAttributedString+Extensions.swift new file mode 100755 index 0000000..306a917 --- /dev/null +++ b/Sources/Extensions/NSMutableAttributedString+Extensions.swift @@ -0,0 +1,73 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +extension NSMutableAttributedString { + + @discardableResult + internal func bold(_ text: String, fontSize: CGFloat = UIFont.preferredFont(forTextStyle: .body).pointSize, textColor: UIColor = .black) -> NSMutableAttributedString { + let attrs: [NSAttributedStringKey:AnyObject] = [ + NSAttributedStringKey.font : UIFont.boldSystemFont(ofSize: fontSize), + NSAttributedStringKey.foregroundColor : textColor + ] + let boldString = NSMutableAttributedString(string: text, attributes: attrs) + self.append(boldString) + return self + } + + @discardableResult + internal func normal(_ text: String, fontSize: CGFloat = UIFont.preferredFont(forTextStyle: .body).pointSize, textColor: UIColor = .black) -> NSMutableAttributedString { + let attrs:[NSAttributedStringKey:AnyObject] = [ + NSAttributedStringKey.font : UIFont.systemFont(ofSize: fontSize), + NSAttributedStringKey.foregroundColor : textColor + ] + let normal = NSMutableAttributedString(string: text, attributes: attrs) + self.append(normal) + return self + } + +} + +extension NSAttributedString { + + func replacingCharacters(in range: NSRange, with attributedString: NSAttributedString) -> NSMutableAttributedString { + let ns = NSMutableAttributedString(attributedString: self) + ns.replaceCharacters(in: range, with: attributedString) + return ns + } + + static func += (lhs: inout NSAttributedString, rhs: NSAttributedString) { + let ns = NSMutableAttributedString(attributedString: lhs) + ns.append(rhs) + lhs = ns + } + + static func + (lhs: NSAttributedString, rhs: NSAttributedString) -> NSAttributedString { + let ns = NSMutableAttributedString(attributedString: lhs) + ns.append(rhs) + return NSAttributedString(attributedString: ns) + } + +} diff --git a/Sources/MessageInputBar.swift b/Sources/MessageInputBar.swift index c39cef0..dde7e92 100644 --- a/Sources/MessageInputBar.swift +++ b/Sources/MessageInputBar.swift @@ -267,6 +267,9 @@ open class MessageInputBar: UIView { return [leftStackViewItems, rightStackViewItems, bottomStackViewItems, topStackViewItems, nonStackViewItems].flatMap { $0 } } + /// The plugins injected into the MessageInputBar + open var plugins: [InputPlugin] = [] + // MARK: - Auto-Layout Management private var textViewLayoutSet: NSLayoutConstraintSet? @@ -745,6 +748,11 @@ open class MessageInputBar: UIView { items.forEach { $0.keyboardEditingEndsAction() } } + /// Invalidates each of the plugins injected into the MessageInputBar + open func invalidatePlugins() { + plugins.forEach { $0.invalidate() } + } + // MARK: - User Actions /// Calls the delegates `didPressSendButtonWith` method diff --git a/Sources/Protocols/InputPlugin.swift b/Sources/Protocols/InputPlugin.swift new file mode 100755 index 0000000..643df85 --- /dev/null +++ b/Sources/Protocols/InputPlugin.swift @@ -0,0 +1,40 @@ +/* + MIT License + + Copyright (c) 2017-2018 MessageKit + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import UIKit + +/// `InputPlugin` is a protocol that makes integrating plugins to the `MessageInputBar` easy. +public protocol InputPlugin: class { + + /// Should reload the state if the `InputPlugin` + func reloadData() + + /// Should remove any content that the `InputPlugin` is managing + func invalidate() + + /// Should handle the input of data types that an `InputPlugin` manages + /// + /// - Parameter object: The object to input + func handleInput(of object: AnyObject) -> Bool +} diff --git a/Sources/Views/InputTextView.swift b/Sources/Views/InputTextView.swift index c4cbee6..124b20f 100644 --- a/Sources/Views/InputTextView.swift +++ b/Sources/Views/InputTextView.swift @@ -234,8 +234,12 @@ open class InputTextView: UITextView { if isImagePasteEnabled { pasteImageInTextContainer(with: image) } else { - // TODO: - Uncomment when InputManager's have been added -// messageInputBar?.inputManagers.forEach { $0.handleInput(of: image) } + // Try to pass image paste to one of the plugins, exit on first successful handling + for plugin in messageInputBar?.plugins ?? [] { + if plugin.handleInput(of: image) { + return + } + } } }