Skip to content

Commit

Permalink
[773] Keyboard performance improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
Clstroud committed May 22, 2024
1 parent 5874de1 commit d08d49b
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 143 deletions.
4 changes: 0 additions & 4 deletions Vocable.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,6 @@
A98E8A2523F1DBF200BF1F22 /* SettingsFooterCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = A98E8A2423F1DBF200BF1F22 /* SettingsFooterCollectionViewCell.xib */; };
A99AF23123FC8BF700BE1184 /* TextTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99AF23023FC8BF700BE1184 /* TextTransaction.swift */; };
A99AF23323FDA8B600BE1184 /* SuggestionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99AF23223FDA8B600BE1184 /* SuggestionCollectionViewCell.swift */; };
A99AF23523FDA8DC00BE1184 /* SuggestionCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = A99AF23423FDA8DC00BE1184 /* SuggestionCollectionViewCell.xib */; };
A99B5324282C0D5D004FEFDA /* AnalyticsReportable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99B5323282C0D5D004FEFDA /* AnalyticsReportable.swift */; };
A99B5326282C0E9D004FEFDA /* TimeInterval+AnalyticsReportable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99B5325282C0E9D004FEFDA /* TimeInterval+AnalyticsReportable.swift */; };
A9B32C8C240EB6250084E151 /* KeyboardModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B32C8B240EB6250084E151 /* KeyboardModel.swift */; };
Expand Down Expand Up @@ -513,7 +512,6 @@
A98E8A2423F1DBF200BF1F22 /* SettingsFooterCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SettingsFooterCollectionViewCell.xib; sourceTree = "<group>"; };
A99AF23023FC8BF700BE1184 /* TextTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextTransaction.swift; sourceTree = "<group>"; };
A99AF23223FDA8B600BE1184 /* SuggestionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionCollectionViewCell.swift; sourceTree = "<group>"; };
A99AF23423FDA8DC00BE1184 /* SuggestionCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SuggestionCollectionViewCell.xib; sourceTree = "<group>"; };
A99B5323282C0D5D004FEFDA /* AnalyticsReportable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsReportable.swift; sourceTree = "<group>"; };
A99B5325282C0E9D004FEFDA /* TimeInterval+AnalyticsReportable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+AnalyticsReportable.swift"; sourceTree = "<group>"; };
A9B32C8B240EB6250084E151 /* KeyboardModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1227,7 +1225,6 @@
A94A6CCB2412D33F00625275 /* Models */,
6B14DC722419590E0050C287 /* OutputTextView.swift */,
A99AF23223FDA8B600BE1184 /* SuggestionCollectionViewCell.swift */,
A99AF23423FDA8DC00BE1184 /* SuggestionCollectionViewCell.xib */,
);
path = Keyboard;
sourceTree = "<group>";
Expand Down Expand Up @@ -1379,7 +1376,6 @@
A9CF2AF5242D4EF1005633A7 /* SensitivityCollectionViewCell.xib in Resources */,
71DBE5EB240DE4D000BA8D01 /* WarningView.xib in Resources */,
B24F02352BEBEF4E003C6BF9 /* Localizable.xcstrings in Resources */,
A99AF23523FDA8DC00BE1184 /* SuggestionCollectionViewCell.xib in Resources */,
A96D61D724229F7400577144 /* SettingsCollectionViewCell.xib in Resources */,
71B856442416CEB600CB163B /* ToastView.xib in Resources */,
A98E8A2523F1DBF200BF1F22 /* SettingsFooterCollectionViewCell.xib in Resources */,
Expand Down
2 changes: 1 addition & 1 deletion Vocable/Common/Views/GazeableButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class GazeableButton: UIButton {
UIView.animate(
withDuration: 0.2,
delay: 0,
options: [.beginFromCurrentState, .curveEaseOut],
options: [.beginFromCurrentState, .curveEaseOut, .allowUserInteraction],
animations: actions,
completion: nil
)
Expand Down
12 changes: 7 additions & 5 deletions Vocable/Common/Views/VocableCollectionViewCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,13 @@ class VocableCollectionViewCell: UICollectionViewCell {
}

if UIView.inheritedAnimationDuration == 0 {
UIView.animate(withDuration: 0.2,
delay: 0,
options: [.beginFromCurrentState, .curveEaseOut],
animations: actions,
completion: nil)
UIView.animate(
withDuration: 0.2,
delay: 0,
options: [.beginFromCurrentState, .curveEaseOut, .allowUserInteraction],
animations: actions,
completion: nil
)
} else {
actions()
}
Expand Down
164 changes: 97 additions & 67 deletions Vocable/Features/Keyboard/KeyboardViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,17 @@ import Combine

class KeyboardViewController: UICollectionViewController {

private var dataSource: UICollectionViewDiffableDataSource<Section, ItemWrapper>!

private typealias DataSource = UICollectionViewDiffableDataSource<Section, ItemWrapper>
private typealias Snapshot = NSDiffableDataSourceSnapshot<Section, ItemWrapper>

private typealias SuggestionCellRegistration = UICollectionView.CellRegistration<SuggestionCollectionViewCell, TextSuggestion>
private typealias KeyCellRegistration = UICollectionView.CellRegistration<KeyboardKeyCollectionViewCell, String>
private typealias FunctionKeyCellRegistration = UICollectionView.CellRegistration<KeyboardKeyCollectionViewCell, KeyboardFunctionKey>
private typealias SpeakKeyCellRegistration = UICollectionView.CellRegistration<SpeakFunctionKeyboardKeyCollectionViewCell, KeyboardFunctionKey>

private var speechSynthesizer: VocableSpeechSynthesizer!
private var dataSource: DataSource!

private var disposables = Set<AnyCancellable>()

private var _textTransaction = TextTransaction(text: "") {
Expand All @@ -31,17 +39,25 @@ class KeyboardViewController: UICollectionViewController {

private var suggestions: [TextSuggestion] = [] {
didSet {
updateSnapshot()
var snapshot = dataSource.snapshot()
let suggestionItems = snapshot.itemIdentifiers(inSection: .suggestions)
snapshot.reconfigureItems(suggestionItems)
dataSource.apply(snapshot, animatingDifferences: true)
}
}

@PublishedValue
var attributedText: NSAttributedString?


private var suggestionCellRegistration: SuggestionCellRegistration!
private var keyCellRegistration: KeyCellRegistration!
private var functionKeyCellRegistration: FunctionKeyCellRegistration!
private var speakCellRegistration: SpeakKeyCellRegistration!

private enum ItemWrapper: Hashable {
case key(String)
case keyboardFunctionButton(KeyboardFunctionButton)
case suggestionText(TextSuggestion)
case functionKey(KeyboardFunctionKey)
case suggestionCell(Int)
}

private enum Section: Int, CaseIterable {
Expand All @@ -62,12 +78,19 @@ class KeyboardViewController: UICollectionViewController {

speechSynthesizer = VocableSpeechSynthesizer()

$attributedText.receive(on: DispatchQueue.main).sink { [weak self] (newAttributedText) in
guard let self = self, let newAttributedText = newAttributedText,
newAttributedText.string != self._textTransaction.text else { return }
self._textTransaction = TextTransaction(text: newAttributedText.string, intent: .lastCharacter)
}.store(in: &disposables)

$attributedText
.receive(on: DispatchQueue.main)
.sink { [weak self] (newAttributedText) in
guard
let self, let newAttributedText,
newAttributedText.string != self._textTransaction.text
else {
return
}
self._textTransaction = TextTransaction(text: newAttributedText.string, intent: .lastCharacter)
}
.store(in: &disposables)

setupCollectionView()
configureDataSource()
}
Expand All @@ -80,51 +103,67 @@ class KeyboardViewController: UICollectionViewController {
private func setupCollectionView() {
collectionView.delaysContentTouches = false
collectionView.isScrollEnabled = false

collectionView.register(
KeyboardKeyCollectionViewCell.self,
forCellWithReuseIdentifier: KeyboardKeyCollectionViewCell.reuseIdentifier
)
collectionView.register(
SpeakFunctionKeyboardKeyCollectionViewCell.self,
forCellWithReuseIdentifier: SpeakFunctionKeyboardKeyCollectionViewCell.reuseIdentifier
)
collectionView.register(UINib(nibName: "SuggestionCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: SuggestionCollectionViewCell.reuseIdentifier)

let layout = createLayout()
collectionView.collectionViewLayout = layout
collectionView.backgroundColor = UIColor.collectionViewBackgroundColor
collectionView.allowsMultipleSelection = true

collectionView.register(PresetPageControlReusableView.self, forSupplementaryViewOfKind: "footerPageIndicator", withReuseIdentifier: "PresetPageControlView")
collectionView.accessibilityID = .shared.keyboard.collectionView
}

private func configureDataSource() {
dataSource = UICollectionViewDiffableDataSource<Section, ItemWrapper>(collectionView: collectionView, cellProvider: { (collectionView: UICollectionView, indexPath: IndexPath, identifier: ItemWrapper) -> UICollectionViewCell? in

switch identifier {
case .suggestionText(let predictiveText):
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SuggestionCollectionViewCell.reuseIdentifier, for: indexPath) as! SuggestionCollectionViewCell
cell.setup(title: predictiveText.text)
return cell
let suggestionRegistration = SuggestionCellRegistration { cell, _, itemIdentifier in
cell.setup(title: itemIdentifier.text)
}
let keyCellRegistration = KeyCellRegistration { cell, _, char in
cell.setup(title: char)
cell.accessibilityID = .shared.keyboard.key(char)
}
let functionKeyRegistration = FunctionKeyCellRegistration { cell, _, itemIdentifier in
cell.setup(with: itemIdentifier.image)
cell.accessibilityID = .shared.keyboard.key(itemIdentifier.accessibilityID)
}
let speakKeyRegistration = SpeakKeyCellRegistration { cell, _, itemIdentifier in
cell.setup(with: itemIdentifier.image)
cell.accessibilityID = .shared.keyboard.key(itemIdentifier.accessibilityID)
}

dataSource = DataSource(collectionView: collectionView) { [weak self] (collectionView: UICollectionView, indexPath: IndexPath, identifier: ItemWrapper) -> UICollectionViewCell? in
guard let self else { return nil }
return switch identifier {
case .suggestionCell(let index):
collectionView.dequeueConfiguredReusableCell(
using: suggestionRegistration,
for: indexPath,
item: suggestions[safe: index] ?? .init(text: "")
)
case .key(let char):
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: KeyboardKeyCollectionViewCell.reuseIdentifier, for: indexPath) as! KeyboardKeyCollectionViewCell
cell.setup(title: char)
cell.accessibilityID = .shared.keyboard.key(char)
return cell
case .keyboardFunctionButton(let functionType):
collectionView.dequeueConfiguredReusableCell(
using: keyCellRegistration,
for: indexPath,
item: char
)
case .functionKey(let functionType):
if functionType == .speak {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SpeakFunctionKeyboardKeyCollectionViewCell.reuseIdentifier, for: indexPath) as! SpeakFunctionKeyboardKeyCollectionViewCell
cell.setup(with: functionType.image)
return cell
collectionView.dequeueConfiguredReusableCell(
using: speakKeyRegistration,
for: indexPath,
item: functionType
)
} else {
collectionView.dequeueConfiguredReusableCell(
using: functionKeyRegistration,
for: indexPath,
item: functionType
)
}
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: KeyboardKeyCollectionViewCell.reuseIdentifier, for: indexPath) as! KeyboardKeyCollectionViewCell
cell.setup(with: functionType.image)
return cell
}
})

}
self.keyCellRegistration = keyCellRegistration
self.functionKeyCellRegistration = functionKeyRegistration
self.speakCellRegistration = speakKeyRegistration
self.suggestionCellRegistration = suggestionRegistration
updateSnapshot()
}

Expand All @@ -149,31 +188,20 @@ class KeyboardViewController: UICollectionViewController {

// Snapshot construction
snapshot.appendSections([.suggestions])

if suggestions.isEmpty {
snapshot.appendItems([.suggestionText(TextSuggestion(text: "")),
.suggestionText(TextSuggestion(text: "")),
.suggestionText(TextSuggestion(text: "")),
.suggestionText(TextSuggestion(text: ""))])
} else {
snapshot.appendItems([.suggestionText(TextSuggestion(text: (suggestions[safe: 0]?.text ?? ""))),
.suggestionText(TextSuggestion(text: (suggestions[safe: 1]?.text ?? ""))),
.suggestionText(TextSuggestion(text: (suggestions[safe: 2]?.text ?? ""))),
.suggestionText(TextSuggestion(text: (suggestions[safe: 3]?.text ?? "")))])
}
snapshot.appendItems((0...3).map { ItemWrapper.suggestionCell($0)})

snapshot.appendSections([.keyboard])
if traitCollection.horizontalSizeClass == .compact && traitCollection.verticalSizeClass == .regular && !AppConfig.isCompactQWERTYKeyboardEnabled {
if sizeClass == .hCompact_vRegular && !AppConfig.isCompactQWERTYKeyboardEnabled {
snapshot.appendItems(KeyboardLocale.current.compactPortraitKeyMapping.map { ItemWrapper.key("\($0)") })
} else {
snapshot.appendItems(KeyboardLocale.current.landscapeKeyMapping.map { ItemWrapper.key("\($0)") })
}

snapshot.appendItems([.keyboardFunctionButton(.clear), .keyboardFunctionButton(.space), .keyboardFunctionButton(.backspace), .keyboardFunctionButton(.speak)])
snapshot.appendItems([.functionKey(.clear), .functionKey(.space), .functionKey(.backspace), .functionKey(.speak)])

dataSource.apply(snapshot, animatingDifferences: animated)
}

// MARK: - Collection View Delegate
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let selectedItem = dataSource.itemIdentifier(for: indexPath) else { return }
Expand All @@ -184,7 +212,7 @@ class KeyboardViewController: UICollectionViewController {
}

switch selectedItem {
case .keyboardFunctionButton(let functionType):
case .functionKey(let functionType):
switch functionType {
case .space:
setTextTransaction(textTransaction.appendingCharacter(with: " "))
Expand All @@ -206,8 +234,10 @@ class KeyboardViewController: UICollectionViewController {
}
case .key(let char):
setTextTransaction(textTransaction.appendingCharacter(with: char))
case .suggestionText(let suggestion):
setTextTransaction(textTransaction.insertingSuggestion(with: suggestion.text))
case .suggestionCell(let index):
if let suggestion = suggestions[safe: index] {
setTextTransaction(textTransaction.insertingSuggestion(with: suggestion.text))
}
}

if collectionView.indexPathForGazedItem != indexPath {
Expand All @@ -219,21 +249,21 @@ class KeyboardViewController: UICollectionViewController {
guard let item = dataSource.itemIdentifier(for: indexPath) else { return false }

switch item {
case .keyboardFunctionButton, .key:
case .functionKey, .key:
return true
case .suggestionText(let suggestion):
return !suggestion.text.isEmpty
case .suggestionCell(let index):
return suggestions.indices.contains(index)
}
}

override func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
guard let item = dataSource.itemIdentifier(for: indexPath) else { return false }

switch item {
case .keyboardFunctionButton, .key:
case .functionKey, .key:
return true
case .suggestionText(let suggestion):
return !suggestion.text.isEmpty
case .suggestionCell(let index):
return suggestions.indices.contains(index)
}
}

Expand Down
11 changes: 10 additions & 1 deletion Vocable/Features/Keyboard/Models/KeyboardModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ struct KeyboardLocale {
}
}

enum KeyboardFunctionButton {
enum KeyboardFunctionKey {
case clear
case backspace
case space
Expand All @@ -74,4 +74,13 @@ enum KeyboardFunctionButton {
return UIImage(systemName: "person.wave.2.fill")!
}
}

var accessibilityID: String {
return switch self {
case .clear: "clear"
case .backspace: "backspace"
case .space: "space"
case .speak: "speak"
}
}
}
Loading

0 comments on commit d08d49b

Please sign in to comment.