Skip to content

Commit 9c29a58

Browse files
committed
v2.0
1 parent b24e38d commit 9c29a58

File tree

8 files changed

+164
-93
lines changed

8 files changed

+164
-93
lines changed

Example/AccessoryKit/ViewController.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ class ViewController: UIViewController {
1818
KeyboardAccessoryButton(type: .tab, position: .trailing),
1919
],
2020
[
21-
KeyboardAccessoryButton(type: .undo, position: .leading) { [weak self] in
21+
KeyboardAccessoryButton(identifier: "undo", type: .undo, position: .leading) { [weak self] in
2222
self?.undo()
2323
},
24-
KeyboardAccessoryButton(type: .redo, position: .leading) { [weak self] in
24+
KeyboardAccessoryButton(identifier: "redo", type: .redo, position: .leading) { [weak self] in
2525
self?.redo()
2626
},
2727
],
@@ -70,8 +70,8 @@ class ViewController: UIViewController {
7070
}
7171

7272
private func updateAccessoryViewButtonEnabled() {
73-
// accessoryView.setEnabled(textView.undoManager?.canUndo ?? false, at: 1)
74-
// accessoryView.setEnabled(textView.undoManager?.canRedo ?? false, at: 2)
73+
accessoryManager.setEnabled(textView.undoManager?.canUndo ?? false, for: "undo")
74+
accessoryManager.setEnabled(textView.undoManager?.canRedo ?? false, for: "redo")
7575
}
7676

7777
private func createInsertMenu() -> UIMenu {

README.md

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,34 @@ The main features are:
1616

1717
* Responsively uses `UITextInputAssistantItem` on iPad and `UITextInputView` on iPhone.
1818
* Scrollable input accessory view with blurry background and customizable buttons.
19+
* Grouping buttons into a visually related button group.
1920
* Supports Auto Layout and Safe Area.
2021
* Supports dark mode.
2122
* Provides built-in pre-defined buttons with SF Symbol.
2223
* Supports presenting `UIMenu` on input accessory view.
24+
* Control state of identified buttons independently.
2325

2426
## Usage
2527

2628
### Requirements
2729

2830
* iOS 14.0+
29-
* Swift 5.5+
31+
* Swift 5.7+
3032

3133
### Installation
3234

35+
#### Swift Package Manager
36+
37+
AccessoryKit is available as a Swift Package. Add this repo to your project through Xcode GUI or `Package.swift`.
38+
39+
```swift
40+
dependencies: [
41+
.package(url: "https://github.com/xnth97/AccessoryKit.git", .upToNextMajor(from: "2.0.0"))
42+
]
43+
```
44+
45+
#### CocoaPods
46+
3347
To install AccessoryKit, simply add the following line to your Podfile:
3448

3549
```ruby
@@ -43,24 +57,30 @@ To run the example project, clone the repo, and run `pod install` from the Examp
4357
### API
4458

4559
```swift
46-
// Create view model array of key buttons
47-
let keyButtons: [KeyboardAccessoryButton] = [
48-
// Create button with built-in type and tap handler block that will be placed on
49-
// the leading side of keyboard on iPad
50-
KeyboardAccessoryButton(type: .undo, position: .leading) { [weak self] in
51-
self?.undo()
52-
},
53-
// Create button with UIImage that will be collapsed in an overflow menu on iPad
54-
KeyboardAccessoryButton(image: UIImage(named: "img"), position: .overflow),
55-
// Create button with title
56-
KeyboardAccessoryButton(title: "Button",
57-
// Create button with UIMenu
58-
KeyboardAccessoryButton(type: .link, menu: createInsertMenu()),
60+
// Create view model array of key button groups
61+
let keyButtonGroups: [KeyboardAccessoryButtonGroup] = [
62+
// Group is just an array of `KeyboardAccessoryButton`. Group elements will be visually
63+
// grouped and close to each other.
64+
[
65+
// Create button with built-in type and tap handler block that will be placed on
66+
// the leading side of keyboard on iPad
67+
KeyboardAccessoryButton(type: .undo, position: .leading) { [weak self] in
68+
self?.undo()
69+
},
70+
// Create button with UIImage that will be collapsed in an overflow menu on iPad
71+
KeyboardAccessoryButton(image: UIImage(named: "img"), position: .overflow),
72+
],
73+
[
74+
// Create button with title
75+
KeyboardAccessoryButton(title: "Button"),
76+
// Create button with UIMenu
77+
KeyboardAccessoryButton(type: .link, menu: createInsertMenu()),
78+
],
5979
]
6080

6181
// Initialize and retain `KeyboardAccessoryManager`
6282
self.accessoryManager = KeyboardAccessoryView(
63-
keyButtons: keyButtons,
83+
keyButtonGroups: keyButtonGroups,
6484
showDismissKeyboardKey: true,
6585
delegate: self)
6686

Screenshots/1.png

100755100644
-47.5 KB
Loading

Sources/AccessoryKit/KeyboardAccessoryButton.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ public struct KeyboardAccessoryButton {
8282

8383
// MARK: - Properties
8484

85+
/// The identifier of this button.
86+
public let identifier: String?
87+
8588
/// The image that is shown on the button.
8689
public let image: UIImage?
8790

@@ -108,14 +111,16 @@ public struct KeyboardAccessoryButton {
108111

109112
/// Initialize the view model of key button inside `KeyboardAccessoryView`.
110113
/// - Parameters:
114+
/// - identifier: The identifier of this button.
111115
/// - title: The title that is shown on the button.
112116
/// - font: The font of title label of button.
113117
/// - image: The image that is shown on the button.
114118
/// - tintColor: The tint color of button. Only available on `UITextInputView`.
115119
/// - position: The position of keyboard accessory button. Only available on iPad.
116120
/// - menu: The menu that will be shown once button is tapped. Only available for iOS 14+.
117121
/// - tapHandler: The tap handler that will be invoked when tapping the button.
118-
public init(title: String? = nil,
122+
public init(identifier: String? = nil,
123+
title: String? = nil,
119124
font: UIFont? = nil,
120125
image: UIImage? = nil,
121126
tintColor: UIColor = Self.defaultTintColor,
@@ -125,6 +130,7 @@ public struct KeyboardAccessoryButton {
125130
if title == nil && image == nil {
126131
fatalError("[AccessoryKit] Error: Must provide a title or an image for button.")
127132
}
133+
self.identifier = identifier
128134
self.title = title
129135
self.font = font
130136
self.image = image
@@ -136,12 +142,14 @@ public struct KeyboardAccessoryButton {
136142

137143
/// Initialize the view model of key button with a given button type.
138144
/// - Parameters:
145+
/// - identifier: The identifier of this button.
139146
/// - type: Pre-defined button type.
140147
/// - tintColor: The tint color of button.
141148
/// - position: The position of keyboard accessory button. Only available on iPad.
142149
/// - menu: The menu that will be shown once button is tapped
143150
/// - tapHandler: The tap handler that will be invoked when tapping the button.
144-
public init(type: ButtonType,
151+
public init(identifier: String? = nil,
152+
type: ButtonType,
145153
tintColor: UIColor = Self.defaultTintColor,
146154
position: KeyboardAccessoryButtonPosition = .overflow,
147155
menu: UIMenu? = nil,
@@ -151,6 +159,7 @@ public struct KeyboardAccessoryButton {
151159
fatalError("[AccessoryKit] Error: Do not have corresponding image for button type \(type)")
152160
}
153161
self.init(
162+
identifier: identifier,
154163
title: Self.titleMap[type],
155164
image: image,
156165
tintColor: tintColor,

Sources/AccessoryKit/KeyboardAccessoryButtonView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import UIKit
1010
/// Internal subview class that is represented by view model `KeyboardAccessoryButton`.
1111
class KeyboardAccessoryButtonView: UIView {
1212

13+
let viewModel: KeyboardAccessoryButton
1314
private let button = UIButton(type: .custom)
14-
private let viewModel: KeyboardAccessoryButton
1515
private let viewSize: CGSize
1616

1717
init(viewModel: KeyboardAccessoryButton,

Sources/AccessoryKit/KeyboardAccessoryGroupView.swift

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@
77

88
import UIKit
99

10+
/// View for grouping multiple keyboard accessory buttons.
1011
class KeyboardAccessoryGroupView: UIView {
1112

1213
private static let spacing: CGFloat = 2.0
1314

14-
private let viewModels: KeyboardAccessoryButtonGroup
1515
private let viewSize: CGSize
1616
private let stackView = UIStackView()
17-
private let buttonViews: [KeyboardAccessoryButtonView]
17+
18+
let buttonViews: [KeyboardAccessoryButtonView]
1819

1920
init(viewModels: KeyboardAccessoryButtonGroup,
2021
keyWidth: CGFloat,
2122
height: CGFloat,
2223
cornerRadius: CGFloat) {
23-
self.viewModels = viewModels
2424
self.viewSize = Self.calculateSize(
2525
viewModels: viewModels,
2626
keyWidth: keyWidth,
@@ -35,20 +35,18 @@ class KeyboardAccessoryGroupView: UIView {
3535
ignoreCornerRadius: true)
3636
}
3737
super.init(frame: CGRect(origin: .zero, size: viewSize))
38-
setupViews(
39-
keyWidth: keyWidth,
40-
keyHeight: height,
41-
cornerRadius: cornerRadius)
38+
setupViews()
39+
40+
clipsToBounds = true
41+
layer.cornerRadius = cornerRadius
4242
}
4343

4444
@available(*, unavailable)
4545
required init?(coder: NSCoder) {
4646
fatalError("init(coder:) has not been implemented")
4747
}
4848

49-
private func setupViews(keyWidth: CGFloat,
50-
keyHeight: CGFloat,
51-
cornerRadius: CGFloat) {
49+
private func setupViews() {
5250
addSubview(stackView)
5351

5452
stackView.axis = .horizontal
@@ -65,9 +63,6 @@ class KeyboardAccessoryGroupView: UIView {
6563
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
6664
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
6765
])
68-
69-
clipsToBounds = true
70-
layer.cornerRadius = cornerRadius
7166
}
7267

7368
// MARK: - Overrides

Sources/AccessoryKit/KeyboardAccessoryManager.swift

Lines changed: 80 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ public class KeyboardAccessoryManager {
2222
private let keyHeight: CGFloat
2323
private let keyCornerRadius: CGFloat
2424
private let showDismissKeyboardKey: Bool
25+
2526
private weak var delegate: KeyboardAccessoryViewDelegate?
27+
private var identifiedActionItems: [String: Any] = [:]
2628

2729
// MARK: - Initializer
2830

@@ -61,7 +63,7 @@ public class KeyboardAccessoryManager {
6163
if Self.isIPad {
6264
configure(inputAssistantItem: textView.inputAssistantItem)
6365
} else {
64-
textView.inputAccessoryView = makeInputView()
66+
textView.inputAccessoryView = inputAccessoryView
6567
}
6668
}
6769

@@ -73,56 +75,70 @@ public class KeyboardAccessoryManager {
7375
if Self.isIPad {
7476
configure(inputAssistantItem: textField.inputAssistantItem)
7577
} else {
76-
textField.inputAccessoryView = makeInputView()
78+
textField.inputAccessoryView = inputAccessoryView
7779
}
7880
}
7981

8082
/// Creates an instance of toolbar view that can be assigned to the text view's `inputAccessoryView`.
8183
/// - Returns: The keyboard accessory view instance.
82-
public func makeInputView() -> KeyboardAccessoryView {
83-
return KeyboardAccessoryView(
84-
keyWidth: keyWidth,
85-
keyHeight: keyHeight,
86-
keyCornerRadius: keyCornerRadius,
87-
keyMargin: keyMargin,
88-
keyButtonGroups: keyButtonGroups,
89-
showDismissKeyboardKey: showDismissKeyboardKey,
90-
delegate: delegate)
91-
}
84+
public private(set) lazy var inputAccessoryView = KeyboardAccessoryView(
85+
keyWidth: keyWidth,
86+
keyHeight: keyHeight,
87+
keyCornerRadius: keyCornerRadius,
88+
keyMargin: keyMargin,
89+
keyButtonGroups: keyButtonGroups,
90+
showDismissKeyboardKey: showDismissKeyboardKey,
91+
delegate: delegate)
9292

9393
/// Configures the `UITextInputAssistantItem` with given accessory manager.
9494
/// - Parameter inputAssistantItem: The `UITextInputAssistantItem` to be configured.
9595
public func configure(inputAssistantItem: UITextInputAssistantItem) {
9696
var leadingButtons: [UIBarButtonItem] = []
9797
var trailingButtons: [UIBarButtonItem] = []
98-
var overflowMenuActions: [UIAction] = []
99-
100-
for button in keyButtonGroups.flatMap({ $0 }) {
101-
if button.position == .overflow {
102-
guard let title = button.title else {
103-
fatalError("[AccessoryKit] Overflow button must have a title")
104-
}
105-
let action = UIAction(
106-
title: title,
107-
image: button.image,
108-
handler: { handler in
109-
button.tapHandler?()
110-
})
111-
overflowMenuActions.append(action)
112-
} else {
113-
let buttonItem = UIBarButtonItem(
114-
title: button.title,
115-
image: button.image,
116-
primaryAction: UIAction(handler: { handler in
117-
button.tapHandler?()
118-
}),
119-
menu: button.menu)
120-
if button.position == .leading {
121-
leadingButtons.append(buttonItem)
98+
var overflowMenuActions: [UIMenuElement] = []
99+
100+
for buttonGroup in keyButtonGroups {
101+
var groupOverflowActions: [UIAction] = []
102+
103+
for button in buttonGroup {
104+
if button.position == .overflow {
105+
guard let title = button.title else {
106+
fatalError("[AccessoryKit] Overflow button must have a title")
107+
}
108+
let action = UIAction(
109+
title: title,
110+
image: button.image,
111+
handler: { handler in
112+
button.tapHandler?()
113+
})
114+
groupOverflowActions.append(action)
115+
116+
if let identifier = button.identifier {
117+
identifiedActionItems[identifier] = action
118+
}
122119
} else {
123-
trailingButtons.append(buttonItem)
120+
let buttonItem = UIBarButtonItem(
121+
title: button.title,
122+
image: button.image,
123+
primaryAction: UIAction(handler: { handler in
124+
button.tapHandler?()
125+
}),
126+
menu: button.menu)
127+
if button.position == .leading {
128+
leadingButtons.append(buttonItem)
129+
} else {
130+
trailingButtons.append(buttonItem)
131+
}
132+
if let identifier = button.identifier {
133+
identifiedActionItems[identifier] = buttonItem
134+
}
124135
}
125136
}
137+
138+
if !groupOverflowActions.isEmpty {
139+
let groupedAction = UIMenu(title: "", options: .displayInline, children: groupOverflowActions)
140+
overflowMenuActions.append(groupedAction)
141+
}
126142
}
127143

128144
if !overflowMenuActions.isEmpty {
@@ -158,6 +174,33 @@ public class KeyboardAccessoryManager {
158174
}
159175
}
160176

177+
// MARK: - API
178+
179+
/// Set `isEnabled` value on the key with a given identifier.
180+
/// - Parameters:
181+
/// - enabled: Boolean value indicating whether the key is enabled.
182+
/// - identifier: Identifier of menu item.
183+
public func setEnabled(_ enabled: Bool, for identifier: String) {
184+
if Self.isIPad {
185+
if let item = identifiedActionItems[identifier] {
186+
switch item {
187+
case is UIAction:
188+
if !enabled {
189+
(item as? UIAction)?.attributes = .disabled
190+
} else {
191+
(item as? UIAction)?.attributes = []
192+
}
193+
case is UIBarButtonItem:
194+
(item as? UIBarButtonItem)?.isEnabled = enabled
195+
default:
196+
break
197+
}
198+
}
199+
} else {
200+
inputAccessoryView.setEnabled(enabled, for: identifier)
201+
}
202+
}
203+
161204
// MARK: - Private
162205

163206
@objc

0 commit comments

Comments
 (0)