diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..2455008 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,4 @@ +included: + - MessageViewController + - MessageViewControllerTests + - Examples diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index e6e09f9..7f1b849 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -101,6 +101,7 @@ 29B98214201E738F0002DA39 /* Resources */, 8D226DA12211905BC93F2571 /* [CP] Embed Pods Frameworks */, EE524A12456D75F342514EA8 /* [CP] Copy Pods Resources */, + 5560190E20259DAC00E8A9E5 /* SwiftLint */, ); buildRules = ( ); @@ -159,6 +160,20 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 5560190E20259DAC00E8A9E5 /* SwiftLint */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = SwiftLint; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\n swiftlint --strict\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; + }; 8D226DA12211905BC93F2571 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/Examples/Examples/AppDelegate.swift b/Examples/Examples/AppDelegate.swift index 6ebbe38..caba85b 100644 --- a/Examples/Examples/AppDelegate.swift +++ b/Examples/Examples/AppDelegate.swift @@ -13,34 +13,42 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? + ) -> Bool { // Override point for customization after application launch. return true } func applicationWillResignActive(_ application: UIApplication) { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + // Sent when the application is about to move from active to inactive state. This can occur for certain types of + // temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the + // application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games + // should use this method to pause the game. } func applicationDidEnterBackground(_ application: UIApplication) { - // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + // Use this method to release shared resources, save user data, invalidate timers, and store enough application + // state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: + // when the user quits. } func applicationWillEnterForeground(_ application: UIApplication) { - // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + // Called as part of the transition from the background to the active state; here you can undo many of the + // changes made on entering the background. } func applicationDidBecomeActive(_ application: UIApplication) { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the + // application was previously in the background, optionally refresh the user interface. } func applicationWillTerminate(_ application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + // Called when the application is about to terminate. Save data if appropriate. See also + // applicationDidEnterBackground:. } - } - diff --git a/Examples/Examples/ViewController.swift b/Examples/Examples/ViewController.swift index 7110027..a2fec85 100644 --- a/Examples/Examples/ViewController.swift +++ b/Examples/Examples/ViewController.swift @@ -9,8 +9,9 @@ import UIKit import MessageViewController -class ViewController: MessageViewController, UITableViewDataSource, UITableViewDelegate, MessageAutocompleteControllerDelegate { +class ViewController: MessageViewController { + // swiftlint:disable:next line_length var data = "Lorem ipsum dolor sit amet|consectetur adipiscing elit|sed do eiusmod|tempor incididunt|ut labore et dolore|magna aliqua| Ut enim ad minim|veniam, quis nostrud|exercitation ullamco|laboris nisi ut aliquip|ex ea commodo consequat|Duis aute|irure dolor in reprehenderit|in voluptate|velit esse cillum|dolore eu|fugiat nulla pariatur|Excepteur sint occaecat|cupidatat non proident|sunt in culpa|qui officia|deserunt|mollit anim id est laborum" .components(separatedBy: "|") let users = ["rnystrom", "BasThomas", "jessesquires", "Sherlouk"] @@ -53,8 +54,11 @@ class ViewController: MessageViewController, UITableViewDataSource, UITableViewD animated: true ) } +} + +// MARK: UITableViewDataSource - // MARK: UITableViewDataSource +extension ViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return tableView === self.tableView @@ -72,7 +76,11 @@ class ViewController: MessageViewController, UITableViewDataSource, UITableViewD return cell } - // MARK: UITableViewDelegate +} + +// MARK: UITableViewDelegate + +extension ViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) @@ -81,7 +89,11 @@ class ViewController: MessageViewController, UITableViewDataSource, UITableViewD } } - // MARK: MessageAutocompleteControllerDelegate +} + +// MARK: MessageAutocompleteControllerDelegate + +extension ViewController: MessageAutocompleteControllerDelegate { func didFind(controller: MessageAutocompleteController, prefix: String, word: String) { autocompleteUsers = users.filter { word.isEmpty || $0.lowercased().contains(word.lowercased()) } @@ -89,4 +101,3 @@ class ViewController: MessageViewController, UITableViewDataSource, UITableViewD } } - diff --git a/MessageViewController.xcodeproj/project.pbxproj b/MessageViewController.xcodeproj/project.pbxproj index 31174c0..c2e2b76 100644 --- a/MessageViewController.xcodeproj/project.pbxproj +++ b/MessageViewController.xcodeproj/project.pbxproj @@ -145,6 +145,7 @@ 290482071FED90070053978C /* Frameworks */, 290482081FED90070053978C /* Headers */, 290482091FED90070053978C /* Resources */, + 5560190F20259E6800E8A9E5 /* SwiftLint */, ); buildRules = ( ); @@ -229,6 +230,23 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 5560190F20259E6800E8A9E5 /* SwiftLint */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = SwiftLint; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\n swiftlint --strict\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 290482061FED90070053978C /* Sources */ = { isa = PBXSourcesBuildPhase; diff --git a/MessageViewController/MessageView.swift b/MessageViewController/MessageView.swift index c41fbce..2a700b2 100644 --- a/MessageViewController/MessageView.swift +++ b/MessageViewController/MessageView.swift @@ -14,7 +14,6 @@ public final class MessageView: UIView, MessageTextViewListener { internal weak var delegate: MessageViewDelegate? internal let button = UIButton() - internal let UITextViewContentSizeKeyPath = #keyPath(UITextView.contentSize) internal let topBorderLayer = CALayer() internal var contentView: UIView? internal var buttonAction: Selector? @@ -32,7 +31,9 @@ public final class MessageView: UIView, MessageTextViewListener { textView.contentInset = .zero textView.textContainerInset = .zero textView.backgroundColor = .clear - textView.addObserver(self, forKeyPath: UITextViewContentSizeKeyPath, options: [.new], context: nil) + textViewContentSizeObservation = textView.observe(\.contentSize, options: [.new]) { [weak self] (_, _) in + self?.textViewContentSizeDidChange() + } textView.font = .systemFont(ofSize: UIFont.systemFontSize) textView.add(listener: self) @@ -58,10 +59,6 @@ public final class MessageView: UIView, MessageTextViewListener { fatalError("init(coder:) has not been implemented") } - deinit { - textView.removeObserver(self, forKeyPath: UITextViewContentSizeKeyPath) - } - // MARK: Public API public var font: UIFont? { @@ -188,12 +185,6 @@ public final class MessageView: UIView, MessageTextViewListener { ) } - public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - if keyPath == UITextViewContentSizeKeyPath { - textViewContentSizeDidChange() - } - } - public override func resignFirstResponder() -> Bool { return textView.resignFirstResponder() } @@ -231,6 +222,8 @@ public final class MessageView: UIView, MessageTextViewListener { textView.alwaysBounceVertical = textView.contentSize.height > maxHeight } + private var textViewContentSizeObservation: NSKeyValueObservation? + // MARK: MessageTextViewListener public func didChange(textView: MessageTextView) { diff --git a/MessageViewController/MessageViewController.swift b/MessageViewController/MessageViewController.swift index 1089118..a4c0f71 100644 --- a/MessageViewController/MessageViewController.swift +++ b/MessageViewController/MessageViewController.swift @@ -89,11 +89,36 @@ open class MessageViewController: UIViewController, MessageAutocompleteControlle messageAutocompleteController.layoutDelegate = self let notificationCenter = NotificationCenter.default - notificationCenter.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil) - notificationCenter.addObserver(self, selector: #selector(keyboardDidShow(notification:)), name: NSNotification.Name.UIKeyboardDidShow, object: nil) - notificationCenter.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil) - notificationCenter.addObserver(self, selector: #selector(keyboardDidHide(notification:)), name: NSNotification.Name.UIKeyboardDidHide, object: nil) - notificationCenter.addObserver(self, selector: #selector(appWillResignActive(notification:)), name: NSNotification.Name.UIApplicationWillResignActive, object: nil) + notificationCenter.addObserver( + self, + selector: #selector(keyboardWillShow(notification:)), + name: NSNotification.Name.UIKeyboardWillShow, + object: nil + ) + notificationCenter.addObserver( + self, + selector: #selector(keyboardDidShow(notification:)), + name: NSNotification.Name.UIKeyboardDidShow, + object: nil + ) + notificationCenter.addObserver( + self, + selector: #selector(keyboardWillHide(notification:)), + name: NSNotification.Name.UIKeyboardWillHide, + object: nil + ) + notificationCenter.addObserver( + self, + selector: #selector(keyboardDidHide(notification:)), + name: NSNotification.Name.UIKeyboardDidHide, + object: nil + ) + notificationCenter.addObserver( + self, + selector: #selector(appWillResignActive(notification:)), + name: NSNotification.Name.UIApplicationWillResignActive, + object: nil + ) } internal var safeAreaAdditionalHeight: CGFloat { @@ -220,7 +245,7 @@ open class MessageViewController: UIViewController, MessageAutocompleteControlle guard gesture.state == .changed else { return } let location = gesture.location(in: view) if messageView.frame.contains(location) { - let _ = messageView.resignFirstResponder() + _ = messageView.resignFirstResponder() } } diff --git a/MessageViewController/UITextView+Prefixes.swift b/MessageViewController/UITextView+Prefixes.swift index 815c5ef..7a42b29 100644 --- a/MessageViewController/UITextView+Prefixes.swift +++ b/MessageViewController/UITextView+Prefixes.swift @@ -10,6 +10,7 @@ import UIKit internal extension UITextView { + // swiftlint:disable:next large_tuple func find(prefixes: Set) -> (prefix: String, word: String, range: NSRange)? { guard prefixes.count > 0, let result = wordAtCaret, @@ -43,4 +44,3 @@ internal extension UITextView { } } - diff --git a/MessageViewController/UIView+iOS11.swift b/MessageViewController/UIView+iOS11.swift index 83932f8..a87a991 100644 --- a/MessageViewController/UIView+iOS11.swift +++ b/MessageViewController/UIView+iOS11.swift @@ -8,7 +8,7 @@ import UIKit internal extension UIView { - + // swiftlint:disable:next identifier_name var util_safeAreaInsets: UIEdgeInsets { if #available(iOS 11.0, *) { return safeAreaInsets @@ -20,7 +20,7 @@ internal extension UIView { } internal extension UIScrollView { - + // swiftlint:disable:next identifier_name var util_adjustedContentInset: UIEdgeInsets { if #available(iOS 11.0, *) { return adjustedContentInset diff --git a/MessageViewControllerTests/MessageViewControllerTests.swift b/MessageViewControllerTests/MessageViewControllerTests.swift index 9544733..1760f8f 100644 --- a/MessageViewControllerTests/MessageViewControllerTests.swift +++ b/MessageViewControllerTests/MessageViewControllerTests.swift @@ -9,27 +9,27 @@ import XCTest class MessageViewControllerTests: XCTestCase { - + override func setUp() { super.setUp() // Put setup code here. This method is called before the invocation of each test method in the class. } - + override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } - + func testExample() { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } - + func testPerformanceExample() { // This is an example of a performance test case. self.measure { // Put the code you want to measure the time of here. } } - + } diff --git a/MessageViewControllerTests/String+WordAtRangeTests.swift b/MessageViewControllerTests/String+WordAtRangeTests.swift index ad7ed19..a1cf73b 100644 --- a/MessageViewControllerTests/String+WordAtRangeTests.swift +++ b/MessageViewControllerTests/String+WordAtRangeTests.swift @@ -9,25 +9,42 @@ import XCTest import MessageViewController +// swiftlint:disable:next type_name class String_WordAtRangeTests: XCTestCase { - + func test_wordParts_whenSingleCharacter() { // 16 characters let text = "foo bar\nbaz bang" - let begin = text.wordParts(text.index(text.startIndex, offsetBy: 0) ..< text.index(text.startIndex, offsetBy: 0)) + let begin = text.wordParts( + text.index(text.startIndex, offsetBy: 0) + ..< + text.index(text.startIndex, offsetBy: 0) + ) XCTAssertEqual(begin?.left, "") XCTAssertEqual(begin?.right, "foo") - let middle = text.wordParts(text.index(text.startIndex, offsetBy: 5) ..< text.index(text.startIndex, offsetBy: 5)) + let middle = text.wordParts( + text.index(text.startIndex, offsetBy: 5) + ..< + text.index(text.startIndex, offsetBy: 5) + ) XCTAssertEqual(middle?.left, "b") XCTAssertEqual(middle?.right, "ar") - let newline = text.wordParts(text.index(text.startIndex, offsetBy: 10) ..< text.index(text.startIndex, offsetBy: 10)) + let newline = text.wordParts( + text.index(text.startIndex, offsetBy: 10) + ..< + text.index(text.startIndex, offsetBy: 10) + ) XCTAssertEqual(newline?.left, "ba") XCTAssertEqual(newline?.right, "z") - let end = text.wordParts(text.index(text.startIndex, offsetBy: 16) ..< text.index(text.startIndex, offsetBy: 16)) + let end = text.wordParts( + text.index(text.startIndex, offsetBy: 16) + ..< + text.index(text.startIndex, offsetBy: 16) + ) XCTAssertEqual(end?.left, "bang") XCTAssertEqual(end?.right, "") } @@ -36,19 +53,35 @@ class String_WordAtRangeTests: XCTestCase { // 16 characters let text = "foo bar\nbaz bang" - let begin = text.wordParts(text.index(text.startIndex, offsetBy: 0) ..< text.index(text.startIndex, offsetBy: 1)) + let begin = text.wordParts( + text.index(text.startIndex, offsetBy: 0) + ..< + text.index(text.startIndex, offsetBy: 1) + ) XCTAssertEqual(begin?.left, "f") XCTAssertEqual(begin?.right, "oo") - let middle = text.wordParts(text.index(text.startIndex, offsetBy: 0) ..< text.index(text.startIndex, offsetBy: 5)) + let middle = text.wordParts( + text.index(text.startIndex, offsetBy: 0) + ..< + text.index(text.startIndex, offsetBy: 5) + ) XCTAssertEqual(middle?.left, "b") XCTAssertEqual(middle?.right, "ar") - let newline = text.wordParts(text.index(text.startIndex, offsetBy: 6) ..< text.index(text.startIndex, offsetBy: 10)) + let newline = text.wordParts( + text.index(text.startIndex, offsetBy: 6) + ..< + text.index(text.startIndex, offsetBy: 10) + ) XCTAssertEqual(newline?.left, "ba") XCTAssertEqual(newline?.right, "z") - let end = text.wordParts(text.index(text.startIndex, offsetBy: 15) ..< text.index(text.startIndex, offsetBy: 16)) + let end = text.wordParts( + text.index(text.startIndex, offsetBy: 15) + ..< + text.index(text.startIndex, offsetBy: 16) + ) XCTAssertEqual(end?.left, "bang") XCTAssertEqual(end?.right, "") } @@ -87,5 +120,5 @@ class String_WordAtRangeTests: XCTestCase { XCTAssertEqual(result?.word, "bar") XCTAssertEqual(text[result!.range], "bar") } - + }