diff --git a/Example/AccessoryKit/ViewController.swift b/Example/AccessoryKit/ViewController.swift index 22e03b8..79b9079 100755 --- a/Example/AccessoryKit/ViewController.swift +++ b/Example/AccessoryKit/ViewController.swift @@ -18,10 +18,10 @@ class ViewController: UIViewController { KeyboardAccessoryButton(type: .tab, position: .trailing), ], [ - KeyboardAccessoryButton(type: .undo, position: .leading) { [weak self] in + KeyboardAccessoryButton(identifier: "undo", type: .undo, position: .leading) { [weak self] in self?.undo() }, - KeyboardAccessoryButton(type: .redo, position: .leading) { [weak self] in + KeyboardAccessoryButton(identifier: "redo", type: .redo, position: .leading) { [weak self] in self?.redo() }, ], @@ -70,8 +70,8 @@ class ViewController: UIViewController { } private func updateAccessoryViewButtonEnabled() { -// accessoryView.setEnabled(textView.undoManager?.canUndo ?? false, at: 1) -// accessoryView.setEnabled(textView.undoManager?.canRedo ?? false, at: 2) + accessoryManager.setEnabled(textView.undoManager?.canUndo ?? false, for: "undo") + accessoryManager.setEnabled(textView.undoManager?.canRedo ?? false, for: "redo") } private func createInsertMenu() -> UIMenu { diff --git a/README.md b/README.md index c4f613d..46f9acf 100755 --- a/README.md +++ b/README.md @@ -16,20 +16,34 @@ The main features are: * Responsively uses `UITextInputAssistantItem` on iPad and `UITextInputView` on iPhone. * Scrollable input accessory view with blurry background and customizable buttons. +* Grouping buttons into a visually related button group. * Supports Auto Layout and Safe Area. * Supports dark mode. * Provides built-in pre-defined buttons with SF Symbol. * Supports presenting `UIMenu` on input accessory view. +* Control state of identified buttons independently. ## Usage ### Requirements * iOS 14.0+ -* Swift 5.5+ +* Swift 5.7+ ### Installation +#### Swift Package Manager + +AccessoryKit is available as a Swift Package. Add this repo to your project through Xcode GUI or `Package.swift`. + +```swift +dependencies: [ + .package(url: "https://github.com/xnth97/AccessoryKit.git", .upToNextMajor(from: "2.0.0")) +] +``` + +#### CocoaPods + To install AccessoryKit, simply add the following line to your Podfile: ```ruby @@ -43,24 +57,30 @@ To run the example project, clone the repo, and run `pod install` from the Examp ### API ```swift -// Create view model array of key buttons -let keyButtons: [KeyboardAccessoryButton] = [ - // Create button with built-in type and tap handler block that will be placed on - // the leading side of keyboard on iPad - KeyboardAccessoryButton(type: .undo, position: .leading) { [weak self] in - self?.undo() - }, - // Create button with UIImage that will be collapsed in an overflow menu on iPad - KeyboardAccessoryButton(image: UIImage(named: "img"), position: .overflow), - // Create button with title - KeyboardAccessoryButton(title: "Button", - // Create button with UIMenu - KeyboardAccessoryButton(type: .link, menu: createInsertMenu()), +// Create view model array of key button groups +let keyButtonGroups: [KeyboardAccessoryButtonGroup] = [ + // Group is just an array of `KeyboardAccessoryButton`. Group elements will be visually + // grouped and close to each other. + [ + // Create button with built-in type and tap handler block that will be placed on + // the leading side of keyboard on iPad + KeyboardAccessoryButton(type: .undo, position: .leading) { [weak self] in + self?.undo() + }, + // Create button with UIImage that will be collapsed in an overflow menu on iPad + KeyboardAccessoryButton(image: UIImage(named: "img"), position: .overflow), + ], + [ + // Create button with title + KeyboardAccessoryButton(title: "Button"), + // Create button with UIMenu + KeyboardAccessoryButton(type: .link, menu: createInsertMenu()), + ], ] // Initialize and retain `KeyboardAccessoryManager` self.accessoryManager = KeyboardAccessoryView( - keyButtons: keyButtons, + keyButtonGroups: keyButtonGroups, showDismissKeyboardKey: true, delegate: self) diff --git a/Screenshots/1.png b/Screenshots/1.png old mode 100755 new mode 100644 index 09a9b80..97c2963 Binary files a/Screenshots/1.png and b/Screenshots/1.png differ diff --git a/Sources/AccessoryKit/KeyboardAccessoryButton.swift b/Sources/AccessoryKit/KeyboardAccessoryButton.swift index 32e6f70..5144bde 100755 --- a/Sources/AccessoryKit/KeyboardAccessoryButton.swift +++ b/Sources/AccessoryKit/KeyboardAccessoryButton.swift @@ -82,6 +82,9 @@ public struct KeyboardAccessoryButton { // MARK: - Properties + /// The identifier of this button. + public let identifier: String? + /// The image that is shown on the button. public let image: UIImage? @@ -108,6 +111,7 @@ public struct KeyboardAccessoryButton { /// Initialize the view model of key button inside `KeyboardAccessoryView`. /// - Parameters: + /// - identifier: The identifier of this button. /// - title: The title that is shown on the button. /// - font: The font of title label of button. /// - image: The image that is shown on the button. @@ -115,7 +119,8 @@ public struct KeyboardAccessoryButton { /// - position: The position of keyboard accessory button. Only available on iPad. /// - menu: The menu that will be shown once button is tapped. Only available for iOS 14+. /// - tapHandler: The tap handler that will be invoked when tapping the button. - public init(title: String? = nil, + public init(identifier: String? = nil, + title: String? = nil, font: UIFont? = nil, image: UIImage? = nil, tintColor: UIColor = Self.defaultTintColor, @@ -125,6 +130,7 @@ public struct KeyboardAccessoryButton { if title == nil && image == nil { fatalError("[AccessoryKit] Error: Must provide a title or an image for button.") } + self.identifier = identifier self.title = title self.font = font self.image = image @@ -136,12 +142,14 @@ public struct KeyboardAccessoryButton { /// Initialize the view model of key button with a given button type. /// - Parameters: + /// - identifier: The identifier of this button. /// - type: Pre-defined button type. /// - tintColor: The tint color of button. /// - position: The position of keyboard accessory button. Only available on iPad. /// - menu: The menu that will be shown once button is tapped /// - tapHandler: The tap handler that will be invoked when tapping the button. - public init(type: ButtonType, + public init(identifier: String? = nil, + type: ButtonType, tintColor: UIColor = Self.defaultTintColor, position: KeyboardAccessoryButtonPosition = .overflow, menu: UIMenu? = nil, @@ -151,6 +159,7 @@ public struct KeyboardAccessoryButton { fatalError("[AccessoryKit] Error: Do not have corresponding image for button type \(type)") } self.init( + identifier: identifier, title: Self.titleMap[type], image: image, tintColor: tintColor, diff --git a/Sources/AccessoryKit/KeyboardAccessoryButtonView.swift b/Sources/AccessoryKit/KeyboardAccessoryButtonView.swift index f0ef3d6..af7d691 100755 --- a/Sources/AccessoryKit/KeyboardAccessoryButtonView.swift +++ b/Sources/AccessoryKit/KeyboardAccessoryButtonView.swift @@ -10,8 +10,8 @@ import UIKit /// Internal subview class that is represented by view model `KeyboardAccessoryButton`. class KeyboardAccessoryButtonView: UIView { + let viewModel: KeyboardAccessoryButton private let button = UIButton(type: .custom) - private let viewModel: KeyboardAccessoryButton private let viewSize: CGSize init(viewModel: KeyboardAccessoryButton, diff --git a/Sources/AccessoryKit/KeyboardAccessoryGroupView.swift b/Sources/AccessoryKit/KeyboardAccessoryGroupView.swift index 23544b1..7f5eb80 100644 --- a/Sources/AccessoryKit/KeyboardAccessoryGroupView.swift +++ b/Sources/AccessoryKit/KeyboardAccessoryGroupView.swift @@ -7,20 +7,20 @@ import UIKit +/// View for grouping multiple keyboard accessory buttons. class KeyboardAccessoryGroupView: UIView { private static let spacing: CGFloat = 2.0 - private let viewModels: KeyboardAccessoryButtonGroup private let viewSize: CGSize private let stackView = UIStackView() - private let buttonViews: [KeyboardAccessoryButtonView] + + let buttonViews: [KeyboardAccessoryButtonView] init(viewModels: KeyboardAccessoryButtonGroup, keyWidth: CGFloat, height: CGFloat, cornerRadius: CGFloat) { - self.viewModels = viewModels self.viewSize = Self.calculateSize( viewModels: viewModels, keyWidth: keyWidth, @@ -35,10 +35,10 @@ class KeyboardAccessoryGroupView: UIView { ignoreCornerRadius: true) } super.init(frame: CGRect(origin: .zero, size: viewSize)) - setupViews( - keyWidth: keyWidth, - keyHeight: height, - cornerRadius: cornerRadius) + setupViews() + + clipsToBounds = true + layer.cornerRadius = cornerRadius } @available(*, unavailable) @@ -46,9 +46,7 @@ class KeyboardAccessoryGroupView: UIView { fatalError("init(coder:) has not been implemented") } - private func setupViews(keyWidth: CGFloat, - keyHeight: CGFloat, - cornerRadius: CGFloat) { + private func setupViews() { addSubview(stackView) stackView.axis = .horizontal @@ -65,9 +63,6 @@ class KeyboardAccessoryGroupView: UIView { stackView.leadingAnchor.constraint(equalTo: leadingAnchor), stackView.trailingAnchor.constraint(equalTo: trailingAnchor), ]) - - clipsToBounds = true - layer.cornerRadius = cornerRadius } // MARK: - Overrides diff --git a/Sources/AccessoryKit/KeyboardAccessoryManager.swift b/Sources/AccessoryKit/KeyboardAccessoryManager.swift index 626965f..4b07578 100644 --- a/Sources/AccessoryKit/KeyboardAccessoryManager.swift +++ b/Sources/AccessoryKit/KeyboardAccessoryManager.swift @@ -22,7 +22,9 @@ public class KeyboardAccessoryManager { private let keyHeight: CGFloat private let keyCornerRadius: CGFloat private let showDismissKeyboardKey: Bool + private weak var delegate: KeyboardAccessoryViewDelegate? + private var identifiedActionItems: [String: Any] = [:] // MARK: - Initializer @@ -61,7 +63,7 @@ public class KeyboardAccessoryManager { if Self.isIPad { configure(inputAssistantItem: textView.inputAssistantItem) } else { - textView.inputAccessoryView = makeInputView() + textView.inputAccessoryView = inputAccessoryView } } @@ -73,56 +75,70 @@ public class KeyboardAccessoryManager { if Self.isIPad { configure(inputAssistantItem: textField.inputAssistantItem) } else { - textField.inputAccessoryView = makeInputView() + textField.inputAccessoryView = inputAccessoryView } } /// Creates an instance of toolbar view that can be assigned to the text view's `inputAccessoryView`. /// - Returns: The keyboard accessory view instance. - public func makeInputView() -> KeyboardAccessoryView { - return KeyboardAccessoryView( - keyWidth: keyWidth, - keyHeight: keyHeight, - keyCornerRadius: keyCornerRadius, - keyMargin: keyMargin, - keyButtonGroups: keyButtonGroups, - showDismissKeyboardKey: showDismissKeyboardKey, - delegate: delegate) - } + public private(set) lazy var inputAccessoryView = KeyboardAccessoryView( + keyWidth: keyWidth, + keyHeight: keyHeight, + keyCornerRadius: keyCornerRadius, + keyMargin: keyMargin, + keyButtonGroups: keyButtonGroups, + showDismissKeyboardKey: showDismissKeyboardKey, + delegate: delegate) /// Configures the `UITextInputAssistantItem` with given accessory manager. /// - Parameter inputAssistantItem: The `UITextInputAssistantItem` to be configured. public func configure(inputAssistantItem: UITextInputAssistantItem) { var leadingButtons: [UIBarButtonItem] = [] var trailingButtons: [UIBarButtonItem] = [] - var overflowMenuActions: [UIAction] = [] - - for button in keyButtonGroups.flatMap({ $0 }) { - if button.position == .overflow { - guard let title = button.title else { - fatalError("[AccessoryKit] Overflow button must have a title") - } - let action = UIAction( - title: title, - image: button.image, - handler: { handler in - button.tapHandler?() - }) - overflowMenuActions.append(action) - } else { - let buttonItem = UIBarButtonItem( - title: button.title, - image: button.image, - primaryAction: UIAction(handler: { handler in - button.tapHandler?() - }), - menu: button.menu) - if button.position == .leading { - leadingButtons.append(buttonItem) + var overflowMenuActions: [UIMenuElement] = [] + + for buttonGroup in keyButtonGroups { + var groupOverflowActions: [UIAction] = [] + + for button in buttonGroup { + if button.position == .overflow { + guard let title = button.title else { + fatalError("[AccessoryKit] Overflow button must have a title") + } + let action = UIAction( + title: title, + image: button.image, + handler: { handler in + button.tapHandler?() + }) + groupOverflowActions.append(action) + + if let identifier = button.identifier { + identifiedActionItems[identifier] = action + } } else { - trailingButtons.append(buttonItem) + let buttonItem = UIBarButtonItem( + title: button.title, + image: button.image, + primaryAction: UIAction(handler: { handler in + button.tapHandler?() + }), + menu: button.menu) + if button.position == .leading { + leadingButtons.append(buttonItem) + } else { + trailingButtons.append(buttonItem) + } + if let identifier = button.identifier { + identifiedActionItems[identifier] = buttonItem + } } } + + if !groupOverflowActions.isEmpty { + let groupedAction = UIMenu(title: "", options: .displayInline, children: groupOverflowActions) + overflowMenuActions.append(groupedAction) + } } if !overflowMenuActions.isEmpty { @@ -158,6 +174,33 @@ public class KeyboardAccessoryManager { } } + // MARK: - API + + /// Set `isEnabled` value on the key with a given identifier. + /// - Parameters: + /// - enabled: Boolean value indicating whether the key is enabled. + /// - identifier: Identifier of menu item. + public func setEnabled(_ enabled: Bool, for identifier: String) { + if Self.isIPad { + if let item = identifiedActionItems[identifier] { + switch item { + case is UIAction: + if !enabled { + (item as? UIAction)?.attributes = .disabled + } else { + (item as? UIAction)?.attributes = [] + } + case is UIBarButtonItem: + (item as? UIBarButtonItem)?.isEnabled = enabled + default: + break + } + } + } else { + inputAccessoryView.setEnabled(enabled, for: identifier) + } + } + // MARK: - Private @objc diff --git a/Sources/AccessoryKit/KeyboardAccessoryView.swift b/Sources/AccessoryKit/KeyboardAccessoryView.swift index ff00fcc..fa547b2 100755 --- a/Sources/AccessoryKit/KeyboardAccessoryView.swift +++ b/Sources/AccessoryKit/KeyboardAccessoryView.swift @@ -39,6 +39,8 @@ public class KeyboardAccessoryView: UIInputView { private let keyButtonGroupViews: [KeyboardAccessoryGroupView] private let showDismissKeyboardKey: Bool + private var identifiedButtonViews: [String: KeyboardAccessoryButtonView] = [:] + public var accessoryViewHeight: CGFloat { return 2 * keyMargin + keyHeight } @@ -88,6 +90,14 @@ public class KeyboardAccessoryView: UIInputView { setupViews() autoresizingMask = .flexibleHeight + + for groupView in keyButtonGroupViews { + for buttonView in groupView.buttonViews { + if let identifier = buttonView.viewModel.identifier { + identifiedButtonViews[identifier] = buttonView + } + } + } } @available(*, unavailable) @@ -176,27 +186,21 @@ public class KeyboardAccessoryView: UIInputView { // MARK: - APIs -// /// Set `isEnabled` value on the key of a given index. -// /// - Parameters: -// /// - enabled: Boolean value indicating whether the key is enabled. -// /// - index: Index of key in `KeyboardAccessoryView`. -// public func setEnabled(_ enabled: Bool, at index: Int) { -// guard index >= 0 && index < keyButtonViews.count else { -// return -// } -// keyButtonViews[index].isEnabled = enabled -// } -// -// /// Set `tintColor` value on the key of a given index. -// /// - Parameters: -// /// - tintColor: Tint color to be set. -// /// - index: Index of key in `KeyboardAccessoryView`. -// public func setTintColor(_ tintColor: UIColor, at index: Int) { -// guard index >= 0 && index < keyButtonViews.count else { -// return -// } -// keyButtonViews[index].tintColor = tintColor -// } + /// Set `isEnabled` value on the key with a given identifier. + /// - Parameters: + /// - enabled: Boolean value indicating whether the key is enabled. + /// - identifier: Identifier of key in `KeyboardAccessoryView`. + public func setEnabled(_ enabled: Bool, for identifier: String) { + identifiedButtonViews[identifier]?.isEnabled = enabled + } + + /// Set `tintColor` value on the key with a given identifier. + /// - Parameters: + /// - tintColor: Tint color to be set. + /// - identifier: Identifier of key in `KeyboardAccessoryView`. + public func setTintColor(_ tintColor: UIColor, for identifier: String) { + identifiedButtonViews[identifier]?.tintColor = tintColor + } /// Set `tintColor` for the whole accessory view. public override var tintColor: UIColor! {