diff --git a/.gitignore b/.gitignore index d534044..875ebac 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,6 @@ fastlane/report.xml fastlane/Preview.html fastlane/screenshots fastlane/test_output + +# macos +.DS_Store diff --git a/KeyboardLayoutGuide/.swiftlint.yml b/KeyboardLayoutGuide/.swiftlint.yml new file mode 100644 index 0000000..e6795f0 --- /dev/null +++ b/KeyboardLayoutGuide/.swiftlint.yml @@ -0,0 +1,115 @@ +excluded: # paths to ignore during linting. Takes precedence over `included`. + - Carthage + - Pods + +disabled_rules: # rule identifiers to exclude from running + - block_based_kvo # Swift implementatoin has bugs which lead to a crash - https://bugs.swift.org/browse/SR-5116, https://bugs.swift.org/browse/SR-5117 + - trailing_comma # There should be a com after the last item in the list + +opt_in_rules: # some rules are only opt-in + - attributes + - anyobject_protocol + - discouraged_optional_boolean + - closure_body_length + - closure_end_indentation + - closure_spacing + - collection_alignment + - contains_over_first_not_nil + - convenience_type + - discouraged_object_literal + - discouraged_optional_collection + - empty_count + - empty_string + - empty_xctest_method + - explicit_init + - extension_access_modifier + - fallthrough + - fatal_error_message + - first_where +# - file_header + - file_name + - force_unwrapping + - function_default_parameter_at_end + - identical_operands + - implicit_return + - implicitly_unwrapped_optional + - joined_default_parameter + - last_where + - legacy_random + - let_var_whitespace + - literal_expression_end_indentation + - lower_acl_than_parent + - missing_docs + - modifier_order + - multiline_arguments + - multiline_arguments_brackets + - multiline_literal_brackets + - multiline_parameters + - multiline_parameters_brackets + - nimble_operator + - number_separator + - operator_usage_whitespace + - override_in_extension + - overridden_super_call + - pattern_matching_keywords + - prefixed_toplevel_constant + - private_action + - private_outlet + - prohibited_interface_builder + - prohibited_super_call + - quick_discouraged_call + - redundant_nil_coalescing + - redundant_type_annotation + - required_enum_case + - single_test_class + - sorted_first_last + - sorted_imports + - static_operator + - strict_fileprivate + - switch_case_on_newline + - toggle_bool + - trailing_closure + - unavailable_function + - unneeded_parentheses_in_closure_argument + - untyped_error_in_catch + - unused_import + - unused_private_declaration + - vertical_parameter_alignment_on_call + - vertical_whitespace_closing_braces + - vertical_whitespace_opening_braces + - xct_specific_matcher + - yoda_condition + +line_length: 200 + +identifier_name: + severity: warning + min_length: 3 + max_length: 40 + excluded: + - id + - ID + - rx + validates_start_with_lowercase: true + +function_body_length: 50 + +cyclomatic_complexity: + warning: 8 + error: 10 + +force_unwrapping: + severity: error + +redundant_type_annotation: + severity: error + +custom_rules: + empty_lines_after_type_declarations: + included: ".*.swift" + name: "Empty lines after type declarations" + regex: '(struct|class|enum|protocol|extension) ([\w]+(:\s*[\w\s,]+)* )\{\n\n' + message: "There should be no empty lines after type declarations" + severity: error + +warning_threshold: 1 diff --git a/KeyboardLayoutGuide/KeyboardLayoutGuide.xcodeproj/project.pbxproj b/KeyboardLayoutGuide/KeyboardLayoutGuide.xcodeproj/project.pbxproj index ccba455..0e02195 100644 --- a/KeyboardLayoutGuide/KeyboardLayoutGuide.xcodeproj/project.pbxproj +++ b/KeyboardLayoutGuide/KeyboardLayoutGuide.xcodeproj/project.pbxproj @@ -111,6 +111,7 @@ 9902DE071FBB24A5009E0D48 /* Frameworks */, 9902DE081FBB24A5009E0D48 /* Headers */, 9902DE091FBB24A5009E0D48 /* Resources */, + 10B2D35122645E990028548F /* SwiftLint */, ); buildRules = ( ); @@ -197,6 +198,27 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 10B2D35122645E990028548F /* SwiftLint */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = SwiftLint; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\n/usr/local/bin/swiftlint autocorrect --quiet\n/usr/local/bin/swiftlint lint --reporter \"xcode\" --quiet \nelse\necho \"error: SwiftLint not installed, 'brew install swiftlint' or download from https://github.com/realm/SwiftLint\"\nexit -1\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 9902DE061FBB24A5009E0D48 /* Sources */ = { isa = PBXSourcesBuildPhase; diff --git a/KeyboardLayoutGuide/KeyboardLayoutGuide/Keyboard+LayoutGuide.swift b/KeyboardLayoutGuide/KeyboardLayoutGuide/Keyboard+LayoutGuide.swift index e8aee65..beb2aaa 100644 --- a/KeyboardLayoutGuide/KeyboardLayoutGuide/Keyboard+LayoutGuide.swift +++ b/KeyboardLayoutGuide/KeyboardLayoutGuide/Keyboard+LayoutGuide.swift @@ -13,54 +13,51 @@ private class Keyboard { var currentHeight: CGFloat = 0 } -public extension UIView { - - private struct AssociatedKeys { +extension UIView { + private enum AssociatedKeys { static var keyboardLayoutGuide = "keyboardLayoutGuide" } - + /// A layout guide representing the inset for the keyboard. /// Use this layout guide’s top anchor to create constraints pinning to the top of the keyboard. - var keyboardLayoutGuide: KeyboardLayoutGuide { - get { - if let obj = objc_getAssociatedObject(self, &AssociatedKeys.keyboardLayoutGuide) as? KeyboardLayoutGuide { - return obj - } - let new = KeyboardLayoutGuide() - addLayoutGuide(new) - new.setUp() - objc_setAssociatedObject(self, &AssociatedKeys.keyboardLayoutGuide, new as Any, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - return new + public var keyboardLayoutGuide: KeyboardLayoutGuide { + if let obj = objc_getAssociatedObject(self, &AssociatedKeys.keyboardLayoutGuide) as? KeyboardLayoutGuide { + return obj } + let new = KeyboardLayoutGuide() + addLayoutGuide(new) + new.setUp() + objc_setAssociatedObject(self, &AssociatedKeys.keyboardLayoutGuide, new as Any, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + return new } } open class KeyboardLayoutGuide: UILayoutGuide { - + @available(*, unavailable) public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - public override init() { + + public init(notificationCenter: NotificationCenter = NotificationCenter.default) { super.init() - // Observe keyboardWillChangeFrame notifications - let nc = NotificationCenter.default - nc.addObserver(self, - selector: #selector(keyboardWillChangeFrame(_:)), - name: UIResponder.keyboardWillChangeFrameNotification, - object: nil) + notificationCenter.addObserver( + self, + selector: #selector(keyboardWillChangeFrame(_:)), + name: UIResponder.keyboardWillChangeFrameNotification, + object: nil + ) } - + internal func setUp() { - guard let view = owningView else { - return - } - NSLayoutConstraint.activate([ - heightAnchor.constraint(equalToConstant: Keyboard.shared.currentHeight), - leftAnchor.constraint(equalTo: view.leftAnchor), - rightAnchor.constraint(equalTo: view.rightAnchor), - ]) + guard let view = owningView else { return } + NSLayoutConstraint.activate( + [ + heightAnchor.constraint(equalToConstant: Keyboard.shared.currentHeight), + leftAnchor.constraint(equalTo: view.leftAnchor), + rightAnchor.constraint(equalTo: view.rightAnchor), + ] + ) let viewBottomAnchor: NSLayoutYAxisAnchor if #available(iOS 11.0, *) { viewBottomAnchor = view.safeAreaLayoutGuide.bottomAnchor @@ -69,21 +66,24 @@ open class KeyboardLayoutGuide: UILayoutGuide { } bottomAnchor.constraint(equalTo: viewBottomAnchor).isActive = true } - + @objc private func keyboardWillChangeFrame(_ note: Notification) { if var height = note.keyboardHeight { - if #available(iOS 11.0, *), height > 0 { - height -= (owningView?.safeAreaInsets.bottom)! + if #available(iOS 11.0, *), height > 0, let bottom = owningView?.safeAreaInsets.bottom { + height -= bottom } heightConstraint?.constant = height animate(note) Keyboard.shared.currentHeight = height } } - + private func animate(_ note: Notification) { - if isVisible(view: self.owningView!) { + if + let owningView = self.owningView, + isVisible(view: owningView) + { self.owningView?.layoutIfNeeded() } else { UIView.performWithoutAnimation { @@ -91,35 +91,27 @@ open class KeyboardLayoutGuide: UILayoutGuide { } } } - - deinit { - NotificationCenter.default.removeObserver(self) - } } // MARK: - Helpers extension UILayoutGuide { internal var heightConstraint: NSLayoutConstraint? { - guard let target = owningView else { return nil } - for c in target.constraints { - if let fi = c.firstItem as? UILayoutGuide, fi == self && c.firstAttribute == .height { - return c - } + return owningView?.constraints.first { + $0 == self && $0.firstAttribute == .height } - return nil } } extension Notification { var keyboardHeight: CGFloat? { - guard let v = userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { + guard let keyboardFrame = userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return nil } // Weirdly enough UIKeyboardFrameEndUserInfoKey doesn't have the same behaviour // in ios 10 or iOS 11 so we can't rely on v.cgRectValue.width let screenHeight = UIApplication.shared.keyWindow?.bounds.height ?? UIScreen.main.bounds.height - return screenHeight - v.cgRectValue.minY + return screenHeight - keyboardFrame.cgRectValue.minY } } @@ -136,4 +128,3 @@ func isVisible(view: UIView) -> Bool { } return isVisible(view: view, inView: view.superview) } - diff --git a/KeyboardLayoutGuide/KeyboardLayoutGuideTests/KeyboardLayoutGuideTests.swift b/KeyboardLayoutGuide/KeyboardLayoutGuideTests/KeyboardLayoutGuideTests.swift index fb44251..2e3ac34 100644 --- a/KeyboardLayoutGuide/KeyboardLayoutGuideTests/KeyboardLayoutGuideTests.swift +++ b/KeyboardLayoutGuide/KeyboardLayoutGuideTests/KeyboardLayoutGuideTests.swift @@ -6,31 +6,17 @@ // Copyright © 2017 freshos. All rights reserved. // -import XCTest @testable import KeyboardLayoutGuide +import XCTest class KeyboardLayoutGuideTests: 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. - } - } - }