Skip to content

Commit

Permalink
v2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
xnth97 committed Sep 11, 2023
1 parent b24e38d commit 9c29a58
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 93 deletions.
8 changes: 4 additions & 4 deletions Example/AccessoryKit/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
},
],
Expand Down Expand Up @@ -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 {
Expand Down
50 changes: 35 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand Down
Binary file modified Screenshots/1.png
100755 → 100644
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 11 additions & 2 deletions Sources/AccessoryKit/KeyboardAccessoryButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand All @@ -108,14 +111,16 @@ 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.
/// - tintColor: The tint color of button. Only available on `UITextInputView`.
/// - 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,
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion Sources/AccessoryKit/KeyboardAccessoryButtonView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
21 changes: 8 additions & 13 deletions Sources/AccessoryKit/KeyboardAccessoryGroupView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -35,20 +35,18 @@ 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)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private func setupViews(keyWidth: CGFloat,
keyHeight: CGFloat,
cornerRadius: CGFloat) {
private func setupViews() {
addSubview(stackView)

stackView.axis = .horizontal
Expand All @@ -65,9 +63,6 @@ class KeyboardAccessoryGroupView: UIView {
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
])

clipsToBounds = true
layer.cornerRadius = cornerRadius
}

// MARK: - Overrides
Expand Down
117 changes: 80 additions & 37 deletions Sources/AccessoryKit/KeyboardAccessoryManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -61,7 +63,7 @@ public class KeyboardAccessoryManager {
if Self.isIPad {
configure(inputAssistantItem: textView.inputAssistantItem)
} else {
textView.inputAccessoryView = makeInputView()
textView.inputAccessoryView = inputAccessoryView
}
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 9c29a58

Please sign in to comment.