├── Vocable
├── Supporting Files
│ ├── de.lproj
│ │ └── LaunchScreen.strings
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── Colors
│ │ │ ├── Contents.json
│ │ │ ├── ErrorRed.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── Background.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── GrayDivider.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── Selection.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── TextHighlight.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── BorderHighlight.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── CategoryBackground.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── DefaultFontColor.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── AlertBackground.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── DefaultCellBackground.colorset
│ │ │ │ └── Contents.json
│ │ │ └── selectedCellBackground.colorset
│ │ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── icon-ios-20@2x.png
│ │ │ ├── icon-ios-20@3x.png
│ │ │ ├── icon-ios-29@2x.png
│ │ │ ├── icon-ios-29@3x.png
│ │ │ ├── icon-ios-40@2x.png
│ │ │ ├── icon-ios-40@3x.png
│ │ │ ├── icon-ios-60@2x.png
│ │ │ ├── icon-ios-60@3x.png
│ │ │ ├── icon-ios-76@2x.png
│ │ │ ├── icon-ios-1024@1x.png
│ │ │ ├── icon-ios-20@2x-1.png
│ │ │ ├── icon-ios-20@2x-2.png
│ │ │ ├── icon-ios-29@2x-1.png
│ │ │ ├── icon-ios-40@2x-1.png
│ │ │ └── icon-ios-83.5@2x.png
│ │ ├── logo.imageset
│ │ │ └── Contents.json
│ │ └── CircleAlert.imageset
│ │ │ └── Contents.json
│ ├── mul.lproj
│ │ └── LaunchScreen.xcstrings
│ ├── AXIdentifier
│ │ ├── AccessibilityID.swift
│ │ ├── Extensions
│ │ │ ├── XCUIElementQuery+AccessibilityID.swift
│ │ │ └── UIAccessibilityIdentification+AccessibilityID.swift
│ │ ├── AccessibilityID+Root+Categories.swift
│ │ ├── AccessibilityID+Settings+SelectionMode.swift
│ │ ├── AccessibilityID+Shared+Pagination.swift
│ │ ├── AccessibilityID+Settings+EditPhrases.swift
│ │ ├── AccessibilityID+Settings+ListeningMode.swift
│ │ ├── AccessibilityID+Root.swift
│ │ ├── AccessibilityID+Settings+VoiceConfiguration.swift.swift
│ │ ├── AccessibilityID+Settings+EditCategories.swift
│ │ ├── AccessibilityID+Settings+EditCategoryDetails.swift
│ │ ├── AccessibilityID+Shared.swift
│ │ ├── AccessibilityID+Shared+Alert.swift
│ │ ├── AccessibilityID+Settings+TimingSensitivity.swift
│ │ ├── AccessibilityID+Settings.swift
│ │ └── AccessibilityID+Shared+Keyboard.swift
│ └── Info.plist
├── SwiftUI
│ ├── Documentation.docc
│ │ ├── Documentation.md
│ │ └── Resources
│ │ │ ├── GazeButton-Roles.png
│ │ │ └── GazeButton-Styling.png
│ ├── UIKit Bridge
│ │ ├── ContentHuggingHostingController.swift
│ │ └── ContentHuggingPriority.swift
│ ├── Extensions
│ │ └── View+.swift
│ ├── GazeBlocking.swift
│ └── Gaze Button
│ │ ├── ButtonState.swift
│ │ ├── DefaultGazeButtonStyle.swift
│ │ └── GazeButtonStyleConfiguration.swift
├── Features
│ ├── Voice
│ │ ├── Paused.wav
│ │ ├── Listening.wav
│ │ ├── VocableChoicesModel.mlmodel
│ │ ├── SoundEffect.swift
│ │ ├── ListeningFeedbackSuccessView.swift
│ │ └── ListenModeDebugStorage.swift
│ ├── Settings
│ │ ├── ListeningMode
│ │ │ └── SettingsFooterTextSupplementaryView.swift
│ │ ├── SelectionMode
│ │ │ └── Views
│ │ │ │ ├── SettingsFooterCollectionViewCell.swift
│ │ │ │ └── SettingsCollectionViewCell.swift
│ │ ├── VoiceSettings
│ │ │ ├── VoiceProfileItem.swift
│ │ │ ├── PersonalVoicePermissionPromptController.swift
│ │ │ ├── VocableListContentConfiguration+VoiceProfileItem.swift
│ │ │ └── VoiceProfilePreviewDataSource+Filter.swift
│ │ └── TimingSensitivity
│ │ │ └── Model
│ │ │ └── CursorSensitivity.swift
│ ├── Keyboard
│ │ ├── Keypad
│ │ │ ├── Layout
│ │ │ │ ├── Components
│ │ │ │ │ ├── KeyboardLayoutConfiguration.swift
│ │ │ │ │ ├── KeyboardLayoutElement.swift
│ │ │ │ │ ├── KeyboardLayout.swift
│ │ │ │ │ ├── KeyboardLayoutMode.swift
│ │ │ │ │ ├── KeyboardKeyAction.swift
│ │ │ │ │ ├── KeyboardLayoutElement+Padding.swift
│ │ │ │ │ ├── KeyboardBody.swift
│ │ │ │ │ ├── KeyboardLayoutRelativeWidth
│ │ │ │ │ │ ├── KeyboardLayoutRelativeWidth.swift
│ │ │ │ │ │ ├── KeyboardLayoutRelativeWidthFixed.swift
│ │ │ │ │ │ ├── KeyboardLayoutRelativeWidthProportional.swift
│ │ │ │ │ │ └── KeyboardLayoutRelativeWidthCompound.swift
│ │ │ │ │ ├── KeyboardLayoutElement+Sizing.swift
│ │ │ │ │ ├── KeyboardLayoutPreviewView.swift
│ │ │ │ │ ├── KeyboardLayoutGroup.swift
│ │ │ │ │ ├── KeyboardLayoutElement+Environment.swift
│ │ │ │ │ ├── KeyboardLayoutRow.swift
│ │ │ │ │ ├── KeyboardLayoutKey.swift
│ │ │ │ │ ├── KeyboardLayoutDirectionalInsets.swift
│ │ │ │ │ ├── KeyboardLayoutContentBuilder.swift
│ │ │ │ │ └── KeyboardLayoutBuilder.swift
│ │ │ │ └── CompactKeyboardLayoutEN.swift
│ │ │ └── UI
│ │ │ │ ├── KeyboardViewDelegate.swift
│ │ │ │ ├── KeyboardKeyAction+Representation.swift
│ │ │ │ └── KeyboardKeyContainerView.swift
│ │ ├── KeyboardLocale.swift
│ │ └── SuggestionsUI
│ │ │ ├── KeyboardSuggestionButton.swift
│ │ │ ├── AnimationContainerView.swift
│ │ │ └── KeyboardSuggestionAnimationContainer.swift
│ └── TextEditor
│ │ └── UITraitCollection+isSpeaking.swift
├── HeadTracking
│ ├── GazeLib.scnassets
│ │ └── axes.scn
│ ├── UIHeadGazeRecognizer.swift
│ ├── Interpolation
│ │ ├── PulseController
│ │ │ ├── SingleControlDisplayable.swift
│ │ │ ├── FormatterExtensions.swift
│ │ │ ├── PulseExtension.swift
│ │ │ ├── PIDControlConstants.swift
│ │ │ ├── Queue.swift
│ │ │ └── TunningViewController.swift
│ │ └── InterpolableValues.swift
│ ├── UIHeadGazeEvent.swift
│ ├── UIHeadGazeTrackingWindow.swift
│ ├── UIHeadGazeCursorWindow.swift
│ ├── Extensions.swift
│ └── ToastWindow.swift
├── APIClient
│ └── Models
│ │ ├── Query.swift
│ │ ├── Exchange.swift
│ │ └── Reply.swift
├── Common
│ ├── AnalyticsReportable.swift
│ ├── Views
│ │ ├── WarningView.swift
│ │ ├── HighlightableContentCell.swift
│ │ ├── PageControlReusableView.swift
│ │ ├── GazeableAlertController
│ │ │ ├── de.lproj
│ │ │ │ └── GazeableAlertViewController.strings
│ │ │ ├── en.lproj
│ │ │ │ └── GazeableAlertViewController.strings
│ │ │ └── GazeableAlertPresentationController.swift
│ │ ├── GazeEatingView.swift
│ │ ├── ViewControllerWrapperView.swift
│ │ ├── TrackingContainerViewController.swift
│ │ └── ToastView.swift
│ ├── TimeInterval+AnalyticsReportable.swift
│ ├── LaunchEnvironment.swift
│ ├── NSLayoutConstraint+Priority.swift
│ ├── VocableListCell
│ │ ├── VocableListCell.swift
│ │ └── VocableListCellAccessory.swift
│ ├── LaunchArguments.swift
│ ├── PublishedValue.swift
│ ├── VocableNavigationController.swift
│ ├── CategoryReordering
│ │ └── CategoryReordering.swift
│ ├── CarouselGridLayout+Masks.swift
│ ├── NSAttributedString+Helpers.swift
│ ├── AppStorage+Init.swift
│ └── AddPhraseCollectionViewCell.swift
├── CoreData
│ ├── Phrases.xcdatamodeld
│ │ ├── .xccurrentversion
│ │ ├── Phrases.xcdatamodel
│ │ │ └── contents
│ │ ├── Phrases v3.xcdatamodel
│ │ │ └── contents
│ │ └── Phrases v2.xcdatamodel
│ │ │ └── contents
│ ├── NSPersistentContainer+Shared.swift
│ ├── ModelObjectExtensions.swift
│ └── NSManagedObject+Helpers.swift
├── Extensions
│ ├── CGSize+Helpers.swift
│ ├── BidirectionalCollection+.swift
│ ├── NSRange+.swift
│ ├── Array+Split.swift
│ ├── UIApplication+Helpers.swift
│ ├── NSDiffableDataSourceSnapshot+Map.swift
│ ├── NSPredicate+Operators.swift
│ ├── TextSuggestionController.swift
│ ├── UIFont+Helpers.swift
│ ├── Phrase+Helpers.swift
│ ├── UICollectionDiffableDatasource+Mutations.swift
│ ├── UICollectionView+Helpers.swift
│ └── TextExpression.swift
├── GoogleService-Info.plist
└── AppConfig
│ └── AppConfig.swift
├── Gemfile
├── CODEOWNERS
├── scripts
└── upload-symbols
├── marketing_assets
├── appstore_badge.png
└── vocable_vimeo_still.gif
├── crowdin.yml
├── Vocable.xcodeproj
└── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── pull_request_template.md
├── fastlane
├── Appfile
├── Matchfile
└── README.md
├── Tests
├── VocableUITests
│ ├── Common
│ │ ├── ResultBuilders.swift
│ │ ├── Arguments.swift
│ │ ├── XCUIApplication+LaunchConfiguration.swift
│ │ └── Environment.swift
│ ├── Utilities.swift
│ ├── Tests
│ │ ├── CustomCategoryBaseTest.swift
│ │ ├── PaginationBaseTest.swift
│ │ ├── PresetsOverrideTestCase.swift
│ │ ├── BaseTest.swift
│ │ ├── CategoryIdentifier.swift
│ │ ├── KeyboardScreenTests.swift
│ │ └── TimingAndSensitivityTests.swift
│ ├── Info.plist
│ ├── Screens
│ │ ├── TimingAndSensitivityScreen.swift
│ │ └── KeyboardScreen.swift
│ └── XCUIElement.swift
├── VocableTests
│ ├── Info.plist
│ └── CategoryOrderabilityTests.swift
├── UnitTests.xctestplan
└── UITests.xctestplan
├── .github
├── ISSUE_TEMPLATE
│ ├── development-task.md
│ ├── design-template.md
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ ├── actions
│ └── run_lane
│ │ └── action.yml
│ ├── on_release_push.yml
│ ├── crowdin_push.yml
│ ├── on_pull_request.yml
│ └── crowdin_pull.yml
├── ROADMAP.md
├── .swiftlint.yml
├── LICENSE
├── CONTRIBUTING.md
└── .gitignore
/Vocable/Supporting Files/de.lproj/LaunchScreen.strings:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/Vocable/SwiftUI/Documentation.docc/Documentation.md:
--------------------------------------------------------------------------------
1 | # ``Vocable``
2 |
3 | Placeholder
4 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem 'rb-readline'
4 | gem "fastlane", '2.226.0'
5 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @jrtb @Clstroud @pg8wood @jsmorgan42 @thomasshealy @stevefosterwta @carolinelaw
2 |
--------------------------------------------------------------------------------
/scripts/upload-symbols:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/scripts/upload-symbols
--------------------------------------------------------------------------------
/Vocable/Features/Voice/Paused.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/Vocable/Features/Voice/Paused.wav
--------------------------------------------------------------------------------
/marketing_assets/appstore_badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/marketing_assets/appstore_badge.png
--------------------------------------------------------------------------------
/Vocable/Features/Voice/Listening.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/Vocable/Features/Voice/Listening.wav
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/marketing_assets/vocable_vimeo_still.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/marketing_assets/vocable_vimeo_still.gif
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/Colors/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/mul.lproj/LaunchScreen.xcstrings:
--------------------------------------------------------------------------------
1 | {
2 | "sourceLanguage" : "en",
3 | "strings" : {
4 |
5 | },
6 | "version" : "1.0"
7 | }
--------------------------------------------------------------------------------
/Vocable/HeadTracking/GazeLib.scnassets/axes.scn:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/Vocable/HeadTracking/GazeLib.scnassets/axes.scn
--------------------------------------------------------------------------------
/Vocable/Features/Voice/VocableChoicesModel.mlmodel:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/Vocable/Features/Voice/VocableChoicesModel.mlmodel
--------------------------------------------------------------------------------
/Vocable/SwiftUI/Documentation.docc/Resources/GazeButton-Roles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/Vocable/SwiftUI/Documentation.docc/Resources/GazeButton-Roles.png
--------------------------------------------------------------------------------
/Vocable/SwiftUI/Documentation.docc/Resources/GazeButton-Styling.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/Vocable/SwiftUI/Documentation.docc/Resources/GazeButton-Styling.png
--------------------------------------------------------------------------------
/crowdin.yml:
--------------------------------------------------------------------------------
1 | files:
2 | - source: /Vocable/**/*.xcstrings
3 | ignore:
4 | - CrowdinExport
5 | translation: /CrowdinExport/%osx_locale%/%file_name%.xliff
6 | bundles:
7 | - 6
8 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-20@2x.png
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-20@3x.png
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-29@2x.png
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-29@3x.png
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-40@2x.png
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-40@3x.png
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-60@2x.png
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-60@3x.png
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-76@2x.png
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-1024@1x.png
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-20@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-20@2x-1.png
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-20@2x-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-20@2x-2.png
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-29@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-29@2x-1.png
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-40@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-40@2x-1.png
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/willowtreeapps/vocable-ios/HEAD/Vocable/Supporting Files/Assets.xcassets/AppIcon.appiconset/icon-ios-83.5@2x.png
--------------------------------------------------------------------------------
/Vocable.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/pull_request_template.md:
--------------------------------------------------------------------------------
1 | https://github.com/willowtreeapps/vocable-ios/issues/###
2 |
3 | closes ###
4 |
5 | *Add the ticket number to above*
6 |
7 | # Description of Work
8 | Description
9 |
10 | ## Notes to Test (Optional)
11 | Notes
12 |
13 | ---
14 |
--------------------------------------------------------------------------------
/fastlane/Appfile:
--------------------------------------------------------------------------------
1 | # app_identifier("[[APP_IDENTIFIER]]") # The bundle identifier of your app
2 | # apple_id("[[APPLE_ID]]") # Your Apple email address
3 |
4 |
5 | # For more information about the Appfile, see:
6 | # https://docs.fastlane.tools/advanced/#appfile
7 |
--------------------------------------------------------------------------------
/fastlane/Matchfile:
--------------------------------------------------------------------------------
1 | git_url("git@github.com:willowtreeapps/eyespeak-match.git")
2 |
3 | storage_mode("git")
4 |
5 | type("development") # The default type, can be: appstore, adhoc, enterprise or development
6 |
7 | app_identifier(["com.willowtreeapps.eyespeakaac"])
8 | team_id("753J68XC6W") # WillowRoot Apps, LLC
--------------------------------------------------------------------------------
/Vocable/APIClient/Models/Query.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Query.swift
3 | // Vocable
4 | //
5 | // Created by Andrew Carter on 8/17/23.
6 | // Copyright © 2023 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct Query: Codable {
12 | let prompt: String
13 | let history: [Exchange]
14 | }
15 |
--------------------------------------------------------------------------------
/Vocable/APIClient/Models/Exchange.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Exchange.swift
3 | // Vocable
4 | //
5 | // Created by Andrew Carter on 8/17/23.
6 | // Copyright © 2023 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct Exchange: Codable {
12 | let prompt: String
13 | let response: String
14 | }
15 |
--------------------------------------------------------------------------------
/Vocable.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Vocable/APIClient/Models/Reply.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QueryResult.swift
3 | // Vocable
4 | //
5 | // Created by Andrew Carter on 8/17/23.
6 | // Copyright © 2023 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct Reply: Decodable {
12 | let responses: [String]
13 | let history: [Exchange]
14 | }
15 |
--------------------------------------------------------------------------------
/Vocable/Common/AnalyticsReportable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnalyticsReportable.swift
3 | // Vocable
4 | //
5 | // Created by Jesse Morgan on 5/11/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol AnalyticsReportable {
12 | var analyticsDescription: String { get }
13 | }
14 |
--------------------------------------------------------------------------------
/Vocable/CoreData/Phrases.xcdatamodeld/.xccurrentversion:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | _XCCurrentVersionName
6 | Phrases v4.xcdatamodel
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Vocable/Extensions/CGSize+Helpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGSize+Helpers.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 4/16/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension CGSize {
12 | var area: CGFloat {
13 | return width * height
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Vocable/Common/Views/WarningView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WarningView.swift
3 | // Vocable
4 | //
5 | // Created by Martin Pittenauer on 20.03.20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @IBDesignable
12 | class WarningView: UIView {
13 |
14 | @IBOutlet weak var label: UILabel?
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/logo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "logo.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | },
12 | "properties" : {
13 | "preserves-vector-representation" : true
14 | }
15 | }
--------------------------------------------------------------------------------
/Tests/VocableUITests/Common/ResultBuilders.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResultBuilders.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 4/25/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | @resultBuilder
12 | enum ListBuilder {
13 | static func buildBlock(_ components: T...) -> [T] {
14 | components
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Vocable/Common/TimeInterval+AnalyticsReportable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimeInterval+AnalyticsReportable.swift
3 | // Vocable
4 | //
5 | // Created by Jesse Morgan on 5/11/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension TimeInterval: AnalyticsReportable {
12 | var analyticsDescription: String {
13 | "\(self)s"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/development-task.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Development Task
3 | about: Used for creating new issues for development
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Acceptance Criteria**:
11 |
12 | any scenarios or conditions that need to be satisfied
13 | If familiar with Gherkin format, follow that template.
14 |
15 | **Design**:
16 |
17 | Screenshots or Link to Figma
18 |
--------------------------------------------------------------------------------
/Vocable/Common/Views/HighlightableContentCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HighlightableContentCell.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 5/22/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | protocol HighlightableContentCell: UICollectionViewCell {
13 | @MainActor
14 | func setHighlightRange(_ range: NSRange?)
15 | }
16 |
--------------------------------------------------------------------------------
/Vocable/Common/Views/PageControlReusableView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PresetPageControl.swift
3 | // Vocable AAC
4 | //
5 | // Created by Jesse Morgan on 1/30/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | class PresetPageControlReusableView: UICollectionReusableView {
13 |
14 | @IBOutlet var pageControl: UIPageControl!
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/AXIdentifier/AccessibilityID.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccessibilityIdentifiers.swift
3 | // Vocable
4 | //
5 | // Created by Rhonda Oglesby on 4/29/22.
6 | //
7 |
8 | import Foundation
9 |
10 | public struct AccessibilityID: ExpressibleByStringLiteral, Hashable {
11 | let id: String
12 | public init(stringLiteral value: StringLiteralType) {
13 | self.id = value
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Vocable/Features/Settings/ListeningMode/SettingsFooterTextSupplementaryView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsFooterTextSupplementaryView.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 2/19/21.
6 | // Copyright © 2021 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class SettingsFooterTextSupplementaryView: UICollectionReusableView {
12 | @IBOutlet private(set) var textLabel: UILabel!
13 | }
14 |
--------------------------------------------------------------------------------
/Vocable/Extensions/BidirectionalCollection+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BidirectionalCollection+.swift
3 | // Vocable
4 | //
5 | // Created by Robert Moyer on 3/23/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | extension BidirectionalCollection {
10 | /// The range spanning all valid indices of the collection
11 | var rangeOfIndices: Range {
12 | startIndex ..< endIndex
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/Layout/Components/KeyboardLayoutConfiguration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardLayoutConfiguration.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 7/31/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct KeyboardLayoutConfiguration {
12 | let mode: KeyboardLayoutMode
13 | let modifierGrapheme: Character?
14 | let sizeClass: SizeClass
15 | }
16 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/UI/KeyboardViewDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardViewDelegate.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 7/31/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol KeyboardViewDelegate: AnyObject {
12 | func keyboardViewDidSelectSuggestion(_ suggestion: String)
13 | func keyboardViewDidSelectKey(_ value: KeyboardLayoutKey)
14 | }
15 |
--------------------------------------------------------------------------------
/Vocable/Common/Views/GazeableAlertController/de.lproj/GazeableAlertViewController.strings:
--------------------------------------------------------------------------------
1 |
2 | /* Class = "UILabel"; text = "Lorem ipsum dolor"; ObjectID = "Qxz-Cn-o5q"; */
3 | "Qxz-Cn-o5q.text" = "Lorem ipsum dolor";
4 |
5 | /* Class = "UIButton"; normalTitle = "Confirm"; ObjectID = "he4-Sq-X8i"; */
6 | "he4-Sq-X8i.normalTitle" = "Okay";
7 |
8 | /* Class = "UIButton"; normalTitle = "Cancel"; ObjectID = "rA5-3d-GQF"; */
9 | "rA5-3d-GQF.normalTitle" = "Abbrechen";
10 |
--------------------------------------------------------------------------------
/Vocable/Common/Views/GazeableAlertController/en.lproj/GazeableAlertViewController.strings:
--------------------------------------------------------------------------------
1 |
2 | /* Class = "UILabel"; text = "Lorem ipsum dolor"; ObjectID = "Qxz-Cn-o5q"; */
3 | "Qxz-Cn-o5q.text" = "Lorem ipsum dolor";
4 |
5 | /* Class = "UIButton"; normalTitle = "Confirm"; ObjectID = "he4-Sq-X8i"; */
6 | "he4-Sq-X8i.normalTitle" = "Okay";
7 |
8 | /* Class = "UIButton"; normalTitle = "Cancel"; ObjectID = "rA5-3d-GQF"; */
9 | "rA5-3d-GQF.normalTitle" = "Cancel";
10 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/CircleAlert.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "CircleAlert.pdf",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/Layout/Components/KeyboardLayoutElement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardLayoutElement.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 7/31/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol KeyboardLayoutElement {
12 | var children: [any KeyboardLayoutElement] { get set }
13 | func makeKeys(environment: KeyboardLayoutEnvironment) -> [KeyboardLayoutKey]
14 | }
15 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/Layout/Components/KeyboardLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardLayout.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 5/24/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | protocol KeyboardLayout {
13 |
14 | var identifier: String { get }
15 |
16 | @KeyboardBuilder
17 | func makeLayout(configuration: KeyboardLayoutConfiguration) -> KeyboardBody
18 | }
19 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/Colors/ErrorRed.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "108",
9 | "green" : "0",
10 | "red" : "173"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/design-template.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Design Template
3 | about: Used for new Design Tasks in the Vocable Project
4 | title: ''
5 | labels: Design Updates
6 | assignees: ''
7 |
8 | ---
9 |
10 | #### Design - Area of Focus
11 | This is a template. Description of what you are working on.
12 |
13 | - [x] Item 1 to work on
14 | - [ ] Item 2 to work on
15 | - [ ] Item 3 to work on
16 |
17 | *or*
18 | * Item 1 to work on
19 | * Item 2 to work on
20 | * Item 3 to work on
21 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/Layout/Components/KeyboardLayoutMode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardLayoutMode.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 7/31/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum KeyboardLayoutMode: Equatable {
12 | case alphabetical
13 | case modifierPicker
14 | case numerical
15 |
16 | var isAlphabetical: Bool {
17 | self == .alphabetical
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/Colors/Background.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | },
6 | "colors" : [
7 | {
8 | "idiom" : "universal",
9 | "color" : {
10 | "color-space" : "srgb",
11 | "components" : {
12 | "red" : "0.128",
13 | "alpha" : "1.000",
14 | "blue" : "0.358",
15 | "green" : "0.109"
16 | }
17 | }
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/Colors/GrayDivider.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | },
6 | "colors" : [
7 | {
8 | "idiom" : "universal",
9 | "color" : {
10 | "color-space" : "srgb",
11 | "components" : {
12 | "red" : "0.635",
13 | "alpha" : "1.000",
14 | "blue" : "0.690",
15 | "green" : "0.631"
16 | }
17 | }
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/Colors/Selection.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | },
6 | "colors" : [
7 | {
8 | "idiom" : "universal",
9 | "color" : {
10 | "color-space" : "srgb",
11 | "components" : {
12 | "red" : "0.214",
13 | "alpha" : "1.000",
14 | "blue" : "0.602",
15 | "green" : "0.980"
16 | }
17 | }
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/Colors/TextHighlight.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | },
6 | "colors" : [
7 | {
8 | "idiom" : "universal",
9 | "color" : {
10 | "color-space" : "srgb",
11 | "components" : {
12 | "red" : "0.298",
13 | "alpha" : "1.000",
14 | "blue" : "0.976",
15 | "green" : "0.852"
16 | }
17 | }
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/Colors/BorderHighlight.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | },
6 | "colors" : [
7 | {
8 | "idiom" : "universal",
9 | "color" : {
10 | "color-space" : "srgb",
11 | "components" : {
12 | "red" : "0.975",
13 | "alpha" : "1.000",
14 | "blue" : "0.185",
15 | "green" : "0.645"
16 | }
17 | }
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/Colors/CategoryBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | },
6 | "colors" : [
7 | {
8 | "idiom" : "universal",
9 | "color" : {
10 | "color-space" : "srgb",
11 | "components" : {
12 | "red" : "0.312",
13 | "alpha" : "1.000",
14 | "blue" : "0.573",
15 | "green" : "0.294"
16 | }
17 | }
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/Colors/DefaultFontColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | },
6 | "colors" : [
7 | {
8 | "idiom" : "universal",
9 | "color" : {
10 | "color-space" : "srgb",
11 | "components" : {
12 | "red" : "0.816",
13 | "alpha" : "1.000",
14 | "blue" : "0.913",
15 | "green" : "0.932"
16 | }
17 | }
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/Colors/AlertBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "0.800",
8 | "blue" : "0.949",
9 | "green" : "0.949",
10 | "red" : "0.949"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/Colors/DefaultCellBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | },
6 | "colors" : [
7 | {
8 | "idiom" : "universal",
9 | "color" : {
10 | "color-space" : "srgb",
11 | "components" : {
12 | "red" : "0.218",
13 | "alpha" : "1.000",
14 | "blue" : "0.627",
15 | "green" : "0.195"
16 | }
17 | }
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Assets.xcassets/Colors/selectedCellBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | },
6 | "colors" : [
7 | {
8 | "idiom" : "universal",
9 | "color" : {
10 | "color-space" : "srgb",
11 | "components" : {
12 | "red" : "0x73",
13 | "alpha" : "1.000",
14 | "blue" : "0xFF",
15 | "green" : "0x00"
16 | }
17 | }
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/Vocable/Extensions/NSRange+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StringProtocol+Range.swift
3 | // Vocable
4 | //
5 | // Created by Robert Moyer on 3/22/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension NSRange {
12 | init(of s: S) {
13 | self.init(s.rangeOfIndices, in: s)
14 | }
15 |
16 | static func entireRange(of s: S) -> NSRange {
17 | NSRange(s.rangeOfIndices, in: s)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Vocable/Common/LaunchEnvironment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LaunchEnvironment.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 4/22/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum LaunchEnvironment {
12 |
13 | public enum Key: String {
14 | case overridePresets
15 | case mixpanelToken
16 | }
17 |
18 | static func value(for key: Key) -> String? {
19 | ProcessInfo.processInfo.environment[key.rawValue]
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Vocable/Common/Views/GazeEatingView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GazeEatingView.swift
3 | // Vocable
4 | //
5 | // Created by Steve Foster on 3/24/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | final class GazeEatingView: UIView {
12 |
13 | override func gazeableHitTest(_ point: CGPoint, with event: UIHeadGazeEvent?) -> UIView? {
14 | // Hit test this view's subviews, otherwise swallow the gazeable hit test
15 | super.gazeableHitTest(point, with: event) ?? self
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/Vocable/SwiftUI/UIKit Bridge/ContentHuggingHostingController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentHuggingHostingController.swift
3 | // Vocable
4 | //
5 | // Created by Robert Moyer on 4/6/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import UIKit
11 |
12 | class ContentHuggingHostingController: UIHostingController {
13 | override func viewDidLayoutSubviews() {
14 | super.viewDidLayoutSubviews()
15 | preferredContentSize = view.intrinsicContentSize
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Vocable/SwiftUI/Extensions/View+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // View+.swift
3 | // Vocable
4 | //
5 | // Created by Robert Moyer on 6/6/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | extension View {
12 | @available(iOS, obsoleted: 15, message: "Please use the built-in overlay modifier")
13 | func overlay(
14 | alignment: Alignment = .center,
15 | @ViewBuilder _ content: () -> Content
16 | ) -> some View where Content: View {
17 | overlay(content())
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Vocable/Common/Views/ViewControllerWrapperView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewControllerWrapperView.swift
3 | // Vocable AAC
4 | //
5 | // Created by Chris Stroud on 2/4/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | final class ViewControllerWrapperView: UIView {
12 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
13 | guard let result = super.hitTest(point, with: event), result != self else {
14 | return nil
15 | }
16 | return result
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Vocable/Features/Settings/SelectionMode/Views/SettingsFooterCollectionViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsFooterCollectionViewCell.swift
3 | // Vocable AAC
4 | //
5 | // Created by Jesse Morgan on 2/10/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | final class SettingsFooterCollectionViewCell: UICollectionViewCell {
12 |
13 | @IBOutlet private weak var versionLabel: UILabel!
14 |
15 | func setup(versionLabel: String) {
16 | self.versionLabel.text = versionLabel
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/AXIdentifier/Extensions/XCUIElementQuery+AccessibilityID.swift:
--------------------------------------------------------------------------------
1 | //
2 | // XCUIElementQuery+AccessibilityID.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 5/11/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 |
12 | public extension XCUIElementQuery {
13 | subscript(_ id: AccessibilityID) -> XCUIElement {
14 | let predicate = NSPredicate(format: "identifier MATCHES %@", id.id)
15 | return self.element(matching: predicate)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.github/workflows/actions/run_lane/action.yml:
--------------------------------------------------------------------------------
1 | name: Run Lane
2 | description: Run a fastane lane and upload artifacts
3 | inputs:
4 | lane:
5 | description: Name of fastlane lane to run
6 | required: true
7 | match_password:
8 | description: Match password, ideally in secrets.MATCH_PASSWORD
9 | required: false
10 | runs:
11 | using: composite
12 | steps:
13 | - name: Run fastlane
14 | run: bundle exec fastlane ${{ inputs.lane }}
15 | shell: sh
16 | env:
17 | MATCH_PASSWORD: ${{ inputs.match_password }}
18 |
19 | # TODO: Artifacts
20 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/AXIdentifier/AccessibilityID+Root+Categories.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccessibilityID+Root+Categories.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 5/11/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension AccessibilityID.root {
12 | public struct categories {
13 | public static let previousButton: AccessibilityID = "root-categories-previous-button"
14 | public static let nextButton: AccessibilityID = "root-categories-next-button"
15 | private init() {}
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Tests/VocableUITests/Common/Arguments.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LaunchArguments+StringArray.swift
3 | // VocableUITests
4 | //
5 | // Created by Jesse Morgan on 4/22/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 |
12 | struct Arguments: XCTestAppConfigurable {
13 |
14 | private let keys: [LaunchArguments.Key]
15 |
16 | init(_ keys: LaunchArguments.Key...) {
17 | self.keys = keys
18 | }
19 |
20 | func configure(_ app: XCUIApplication) {
21 | app.launchArguments = keys.map(\.rawValue)
22 | }
23 | }
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Vocable/Common/NSLayoutConstraint+Priority.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSLayoutConstraint+Priority.swift
3 | // Vocable
4 | //
5 | // Created by Jesse Morgan on 3/17/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension NSLayoutConstraint {
12 |
13 | func withPriority(_ priority: UILayoutPriority) -> NSLayoutConstraint {
14 | self.priority = priority
15 | return self
16 | }
17 |
18 | func withPriority(_ rawPriority: Float) -> NSLayoutConstraint {
19 | self.priority = UILayoutPriority(rawValue: rawPriority)
20 | return self
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Vocable/HeadTracking/UIHeadGazeRecognizer.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIKit.UIGestureRecognizerSubclass
3 |
4 | class UIHeadGazeRecognizer: UIGestureRecognizer {
5 |
6 | func gazeBegan(_ gaze: UIHeadGaze, with event: UIHeadGazeEvent?) {
7 | // No-op
8 | }
9 |
10 | func gazeMoved(_ gaze: UIHeadGaze, with event: UIHeadGazeEvent?) {
11 | // No-op
12 | }
13 |
14 | func gazeEnded(_ gaze: UIHeadGaze, with event: UIHeadGazeEvent?) {
15 | // No-op
16 | }
17 |
18 | func gazeCancelled(_ gaze: UIHeadGaze, with event: UIHeadGazeEvent?) {
19 | // No-op
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/KeyboardLocale.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardModel.swift
3 | // Vocable AAC
4 | //
5 | // Created by Jesse Morgan on 3/3/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | enum KeyboardLocale: String {
13 | case en
14 | case de
15 | case it
16 |
17 | static var current: Self {
18 | let preferredLanguageCode = AppConfig.activePreferredLanguageCode
19 | let code = Locale(identifier: preferredLanguageCode).languageCode ?? AppConfig.defaultLanguageCode
20 | return Self(rawValue: code) ?? .en
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/AXIdentifier/AccessibilityID+Settings+SelectionMode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccessibilityID+Settings+SelectionMode.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 5/11/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension AccessibilityID.settings {
12 | public struct selectionMode {
13 | public static let headTrackingToggle: AccessibilityID = "selection-mode-head-tracking-toggle"
14 | public static let compactQwertyToggle: AccessibilityID = "selection-mode-compact-qwerty-toggle"
15 | private init() {}
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Vocable/Common/VocableListCell/VocableListCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VocableListCell.swift
3 | // Vocable
4 | //
5 | // Created by Jesse Morgan on 3/14/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 | import UIKit
9 |
10 | final class VocableListCell: UICollectionViewListCell {
11 |
12 | override func updateConfiguration(using state: UICellConfigurationState) {
13 | super.updateConfiguration(using: state)
14 |
15 | var background = UIBackgroundConfiguration.listSidebarCell()
16 | background.backgroundColor = .primaryBackgroundColor
17 | backgroundConfiguration = background
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/ROADMAP.md:
--------------------------------------------------------------------------------
1 | # Vocable Roadmap
2 |
3 | - [ ] Handset Support
4 | - [ ] Edit and delete customized presets
5 | - [ ] Custom Categories: create, edit, and delete
6 | - [ ] Custom category/phrase sorting and UI placement
7 | - [ ] History: recently-spoken phrases
8 | - [ ] Head tracking calibration
9 | - [ ] Audio feedback for navigation
10 | - [ ] Potential Voice integration
11 | - [ ] Alternative modalities / input actions (e.g. touch, joystick method, scanning)
12 | - [ ] Light / Dark Mode
13 | - [ ] Localization of app strings
14 | - [ ] Localization of keyboard(s)
15 | - [ ] Photo upload / pictoral buttons
16 | - [ ] Translate selected phrase ↔️ spoken phrase
--------------------------------------------------------------------------------
/Tests/VocableUITests/Common/XCUIApplication+LaunchConfiguration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // XCUIApplication+LaunchConfiguration.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 4/25/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 |
12 | protocol XCTestAppConfigurable {
13 | func configure(_ app: XCUIApplication)
14 | }
15 |
16 | extension XCUIApplication {
17 | func configure(@ListBuilder _ builder: () -> [XCTestAppConfigurable]) {
18 | builder().forEach { configurable in
19 | configurable.configure(self)
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Vocable/HeadTracking/Interpolation/PulseController/SingleControlDisplayable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SingleControlDisplayable.swift
3 | // Pulse
4 | //
5 | // Created by Dawid Cieslak on 15/04/2018.
6 | // Copyright © 2018 Dawid Cieslak. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// Data required to present Pulse's single configuration
12 | protocol SingleControlDisplayable {
13 |
14 | /// Name of factor being controlled
15 | var name: String { get }
16 |
17 | /// Minimum value that can be set
18 | var minimumValue: Float { get }
19 |
20 | /// Maximum value that can be set
21 | var maximumValue: Float { get }
22 | }
23 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/SuggestionsUI/KeyboardSuggestionButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardSuggestionButton.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 7/31/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension KeyboardSuggestionsView {
12 | final class SuggestionButton: GazeableButton {
13 | override func updateConfiguration() {
14 | fillColor = .categoryBackgroundColor
15 | font = .keyboardKey(satisfying: traitCollection)
16 | super.updateConfiguration()
17 | configuration?.titleLineBreakMode = .byTruncatingTail
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/AXIdentifier/AccessibilityID+Shared+Pagination.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccessibilityID+Shared+Pagination.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 5/11/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension AccessibilityID.shared {
12 | public struct pagination {
13 | public static let previousButton: AccessibilityID = "shared-pagination-previous-button"
14 | public static let nextButton: AccessibilityID = "shared-pagination-next-button"
15 | public static let pageLabel: AccessibilityID = "shared-pagination-page-label"
16 | private init() {}
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/AXIdentifier/AccessibilityID+Settings+EditPhrases.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccessibilityID+Settings+EditPhrases.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 5/11/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension AccessibilityID.settings {
12 | public struct editPhrases {
13 | public static let addPhraseButton: AccessibilityID = "edit-phrases-add-phrase-button"
14 | public static let editPhraseButton: AccessibilityID = "edit-phrase-button"
15 | public static let deletePhraseButton: AccessibilityID = "delete-phrase-button"
16 | private init() {}
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/AXIdentifier/AccessibilityID+Settings+ListeningMode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccessibilityID+Settings+ListeningMode.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 8/21/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension AccessibilityID.settings {
12 | public struct listeningMode {
13 | public static let listeningModeToggle: AccessibilityID = "listening_mode_toggle"
14 | public static let hotWordEnabledToggle: AccessibilityID = "hot_word_toggle"
15 | public static let smartAssistEnabledToggle: AccessibilityID = "use_gpt_toggle"
16 | private init() {}
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Tests/VocableUITests/Utilities.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Utilities.swift
3 | // VocableUITests
4 | //
5 | // Created by Canan Arikan on 6/23/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 |
12 | class Utilities {
13 |
14 | static func restartApp() {
15 | XCUIApplication().terminate()
16 | XCUIApplication().activate()
17 | }
18 |
19 | static func restartApp(withLaunchArguments launchArguments: Arguments) {
20 | let app = XCUIApplication()
21 | app.configure {
22 | launchArguments
23 | }
24 | app.terminate()
25 | app.activate()
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Tests/VocableUITests/Tests/CustomCategoryBaseTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomCategoryBaseTest.swift
3 | // VocableUITests
4 | //
5 | // Created by Canan Arikan and Rudy Salas on 4/5/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class CustomCategoryBaseTest: BaseTest {
12 |
13 | private(set) var customCategoryName: String = "Test"
14 | private(set) var nameSuffix: String = "add"
15 |
16 | override func setUpWithError() throws {
17 | try super.setUpWithError()
18 | try SettingsScreen.navigateToSettingsCategoryScreen()
19 | try CustomCategoriesScreen.createCustomCategory(categoryName: customCategoryName)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Vocable/HeadTracking/UIHeadGazeEvent.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIKit
3 |
4 | class UIHeadGazeEvent: UIEvent {
5 | public var allGazes: Set?
6 | override var allTouches: Set? {
7 | return allGazes
8 | }
9 |
10 | /**
11 | The time when the event occurred
12 | */
13 | private var _timestamp: TimeInterval
14 |
15 | /**
16 | Returns the time when the event occurred
17 | */
18 | public var timeStamp: TimeInterval {
19 | return _timestamp
20 | }
21 |
22 | init(allGazes: Set? = nil) {
23 | self.allGazes = allGazes
24 | self._timestamp = Date().timeIntervalSince1970
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/AXIdentifier/Extensions/UIAccessibilityIdentification+AccessibilityID.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIAccessibilityIdentification+AccessibilityID.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 5/11/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | extension UIAccessibilityIdentification {
13 | var accessibilityID: AccessibilityID? {
14 | get {
15 | accessibilityIdentifier.map { value in
16 | AccessibilityID(stringLiteral: value)
17 | }
18 | }
19 | set {
20 | self.accessibilityIdentifier = newValue?.id
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/Vocable/Extensions/Array+Split.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Array+Split.swift
3 | // Vocable AAC
4 | //
5 | // Created by Duncan Lewis on 11/6/18.
6 | // Copyright © 2018 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension Array {
12 | func chunked(into size: Int) -> [[Element]] {
13 | return stride(from: 0, to: count, by: size).map {
14 | Array(self[$0.. Element? {
23 | return indices.contains(index) ? self[index] : nil
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Vocable/Extensions/UIApplication+Helpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URL+Helpers.swift
3 | // Vocable
4 | //
5 | // Created by Jesse Morgan on 6/2/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIApplication {
12 |
13 | static func openSettingsURL() {
14 | if let url = URL(string: self.openSettingsURLString) {
15 | if shared.canOpenURL(url) {
16 | shared.open(url, options: [:], completionHandler: nil)
17 | }
18 | }
19 | }
20 |
21 | var connectedSceneWindows: [UIWindow] {
22 | connectedScenes
23 | .compactMap { $0 as? UIWindowScene }
24 | .flatMap(\.windows)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Vocable/Common/LaunchArguments.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LaunchArguments.swift
3 | // Vocable
4 | //
5 | // Created by Jesse Morgan on 4/22/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public struct LaunchArguments {
12 |
13 | public enum Key: String {
14 | case resetAppDataOnLaunch
15 | case enableListeningMode
16 | case disableAnimations
17 | }
18 |
19 | static func contains(_ key: Key) -> Bool {
20 | CommandLine
21 | .arguments
22 | .compactMap(LaunchArguments.Key.init)
23 | .contains(key)
24 | }
25 |
26 | let keys: [Key]
27 |
28 | private init() {
29 | keys = []
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/AXIdentifier/AccessibilityID+Root.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccessibilityID+Root.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 5/11/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension AccessibilityID {
12 | public struct root {
13 | public static let outputText: AccessibilityID = "root-output-text"
14 | public static let categoryBackButton: AccessibilityID = "root-category-back-button"
15 | public static let categoryForwardButton: AccessibilityID = "root-category-forward-button"
16 | public static let addPhraseButton: AccessibilityID = "root-add-phrase-button"
17 | private init() {}
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/AXIdentifier/AccessibilityID+Settings+VoiceConfiguration.swift.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccessibilityID+Settings+VoiceConfiguration.swift.swift
3 | // Vocable
4 | //
5 | // Created by Steve Foster on 4/23/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension AccessibilityID.settings {
12 | public struct voiceSettings {
13 | public static let playButton: AccessibilityID = "voice-configuration-play-voice-button"
14 | public static let audioPlaying: AccessibilityID = "voice-configuration-audio-playing-button"
15 | public static let previewVoiceCell: AccessibilityID = "voice-settings-preview-voice-cell"
16 | private init() {}
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 |
16 |
17 | **Expected behavior**
18 | A clear and concise description of what you expected to happen.
19 |
20 | **Actual behavior**
21 | A clear and concise description of what is currently happening.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Device Information**
27 |
28 | - Device: [e.g. iPhone 11]
29 | - OS: [e.g. iOS 13.4]
30 |
--------------------------------------------------------------------------------
/.github/workflows/on_release_push.yml:
--------------------------------------------------------------------------------
1 | name: On release push
2 | on:
3 | push:
4 | branches:
5 | - release.**
6 | jobs:
7 | on_release_push:
8 | runs-on: macos-14
9 | steps:
10 | - uses: actions/checkout@v4
11 |
12 | - name: Run Unit and UI tests
13 | uses: ./.github/workflows/actions/run_lane
14 | with:
15 | lane: test_unit_ui
16 | match_password: ${{ secrets.MATCH_PASSWORD }}
17 |
18 | - name: Commit translation file if changed
19 | uses: ./.github/workflows/actions/xliff_export
20 |
21 | - name: Build and deploy to TestFlight
22 | uses: ./.github/workflows/actions/run_lane
23 | with:
24 | lane: build_deploy_testflight
25 | match_password: ${{ secrets.MATCH_PASSWORD }}
26 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/Layout/Components/KeyboardKeyAction.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardKeyAction.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 8/2/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum KeyboardKeyAction: Hashable {
12 | case clear
13 | case backspace
14 | case space
15 | case speak
16 | case numberPad
17 | case alphabet
18 | case openModifierPicker
19 | case closeModifierPicker
20 | case beginModifier(Character)
21 | case endModifier(Character)
22 | case insertCharacter(Character)
23 |
24 | var isStandardKey: Bool {
25 | if case .insertCharacter = self {
26 | return true
27 | }
28 | return false
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Vocable/HeadTracking/UIHeadGazeTrackingWindow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIHeadGazeTrackingWindow.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 4/24/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | final class UIHeadGazeTrackingWindow: UIWindow {
12 |
13 | override init(frame: CGRect) {
14 | super.init(frame: frame)
15 | commonInit()
16 | }
17 |
18 | override init(windowScene: UIWindowScene) {
19 | super.init(windowScene: windowScene)
20 | commonInit()
21 | }
22 |
23 | required init?(coder: NSCoder) {
24 | super.init(coder: coder)
25 | commonInit()
26 | }
27 |
28 | private func commonInit() {
29 | self.rootViewController = UIHeadGazeViewController()
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Vocable/Features/TextEditor/UITraitCollection+isSpeaking.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UITraitCollection+isSpeaking.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 7/31/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | @available(iOS 17.0, *)
13 | private struct SpeakingStateTrait: UITraitDefinition {
14 | static var defaultValue: Bool = false
15 | }
16 |
17 | @available(iOS 17.0, *)
18 | extension UITraitCollection {
19 | var isSpeaking: Bool {
20 | self[SpeakingStateTrait.self]
21 | }
22 | }
23 |
24 | @available(iOS 17.0, *)
25 | extension UIMutableTraits {
26 | var isSpeaking: Bool {
27 | get { self[SpeakingStateTrait.self] }
28 | set { self[SpeakingStateTrait.self] = newValue }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Tests/VocableTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | English
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/AXIdentifier/AccessibilityID+Settings+EditCategories.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccessibilityID+Settings+EditCategories.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 5/11/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension AccessibilityID.settings {
12 | public struct editCategories {
13 | public static let addCategoryButton: AccessibilityID = "edit-categories-add-category-button"
14 | public static let categoryButton: AccessibilityID = "edit-categories-category-button"
15 | public static let moveUpButton: AccessibilityID = "edit-categories-move-up-button"
16 | public static let moveDownButton: AccessibilityID = "edit-categories-move-down-button"
17 | private init() {}
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Tests/VocableUITests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Vocable/Common/Views/TrackingContainerViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TrackingContainerViewController.swift
3 | // Vocable AAC
4 | //
5 | // Created by Chris Stroud on 2/3/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Combine
11 |
12 | final class TrackingContainerViewController: UIViewController {
13 |
14 | private var contentViewController: UIViewController!
15 | private var trackingViewController: UIHeadGazeViewController?
16 |
17 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
18 | if segue.identifier == "ContentViewControllerSegue" {
19 | self.contentViewController = segue.destination
20 | }
21 | }
22 |
23 | override var childForHomeIndicatorAutoHidden: UIViewController? {
24 | return contentViewController
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/AXIdentifier/AccessibilityID+Settings+EditCategoryDetails.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccessibilityID+Settings+EditCategoryDetails.swift
3 | // Vocable
4 | //
5 | // Created by Rudy Salas on 5/25/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension AccessibilityID.settings {
12 | public struct editCategoryDetails {
13 | public static let renameCategoryButton: AccessibilityID = "category-details-rename-category-button"
14 | public static let showCategoryToggle: AccessibilityID = "category-details-show-category-toggle"
15 | public static let editPhrasesButton: AccessibilityID = "category-details-edit-phrases-button"
16 | public static let removeCategoryButton: AccessibilityID = "category-details-remove-category-button"
17 | private init() {}
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Vocable/Features/Settings/VoiceSettings/VoiceProfileItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VoiceProfileItem.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 4/25/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AVFoundation
11 |
12 | struct VoiceProfileItem: Hashable {
13 | let voice: AVSpeechSynthesisVoice
14 | let isSelected: Bool
15 | let isPlaying: Bool
16 |
17 | func hash(into hasher: inout Hasher) {
18 | voice.identifier.hash(into: &hasher)
19 | isSelected.hash(into: &hasher)
20 | isPlaying.hash(into: &hasher)
21 | }
22 |
23 | static func == (_ lhs: VoiceProfileItem, _ rhs: VoiceProfileItem) -> Bool {
24 | lhs.isSelected == rhs.isSelected &&
25 | lhs.isPlaying == rhs.isPlaying &&
26 | lhs.voice.identifier == rhs.voice.identifier
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Vocable/HeadTracking/Interpolation/PulseController/FormatterExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FormatterExtensions.swift
3 | // Pulse
4 | //
5 | // Created by Dawid Cieslak on 15/04/2018.
6 | // Copyright © 2018 Dawid Cieslak. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension Formatter {
12 |
13 | /// Creates number formatter with exact fraction digits
14 | ///
15 | /// - Parameter fractionDigits: Number of fraction digits
16 | /// - Returns: Number formatter with decimal format and specified number of fraction digits
17 | static func decimalFormat(fractionDigits: Int) -> NumberFormatter {
18 | let formatter = NumberFormatter()
19 | formatter.minimumFractionDigits = fractionDigits
20 | formatter.maximumFractionDigits = fractionDigits
21 | formatter.numberStyle = .decimal
22 | return formatter
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Tests/UnitTests.xctestplan:
--------------------------------------------------------------------------------
1 | {
2 | "configurations" : [
3 | {
4 | "id" : "4FFAE1EC-9CEC-4992-88CE-08AC9C26EFCA",
5 | "name" : "Configuration 1",
6 | "options" : {
7 |
8 | }
9 | }
10 | ],
11 | "defaultOptions" : {
12 | "repeatInNewRunnerProcess" : true,
13 | "targetForVariableExpansion" : {
14 | "containerPath" : "container:Vocable.xcodeproj",
15 | "identifier" : "130C00B320D2AD2A007C3163",
16 | "name" : "Vocable"
17 | },
18 | "testExecutionOrdering" : "random",
19 | "testRepetitionMode" : "retryOnFailure",
20 | "testTimeoutsEnabled" : true
21 | },
22 | "testTargets" : [
23 | {
24 | "target" : {
25 | "containerPath" : "container:Vocable.xcodeproj",
26 | "identifier" : "54D3C41323E37CD40061EF47",
27 | "name" : "VocableTests"
28 | }
29 | }
30 | ],
31 | "version" : 1
32 | }
33 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/AXIdentifier/AccessibilityID+Shared.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccessibilityID+Shared.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 5/11/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension AccessibilityID {
12 | public struct shared {
13 | public static let keyboardButton: AccessibilityID = "shared-keyboard-button"
14 | public static let settingsButton: AccessibilityID = "shared-settings-button"
15 | public static let backButton: AccessibilityID = "shared-back-button"
16 | public static let dismissButton: AccessibilityID = "shared-dismiss-button"
17 | public static let titleLabel: AccessibilityID = "shared-title-label"
18 | public static let emptyStateAddPhraseButton: AccessibilityID = "empty-state-addPhrase-button"
19 | private init() {}
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.github/workflows/crowdin_push.yml:
--------------------------------------------------------------------------------
1 | name: Push Strings to Crowdin
2 |
3 | on:
4 | push:
5 | branches: [ develop ]
6 | workflow_dispatch:
7 | inputs:
8 | test:
9 | description: Just to force the manual trigger
10 | required: false
11 | default: 'trigger'
12 | jobs:
13 | synchronize-with-crowdin:
14 | runs-on: macos-14
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v4
18 |
19 | - name: Install Crowdin CLI
20 | run: brew install crowdin
21 | shell: sh
22 |
23 | - name: Upload Sources
24 | run: >
25 | crowdin upload sources
26 | shell: sh
27 | env:
28 | GITHUB_TOKEN: ${{ secrets.CROWDIN_GITHUB_TOKEN }}
29 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
30 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
--------------------------------------------------------------------------------
/Vocable/HeadTracking/Interpolation/PulseController/PulseExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PulseExtension.swift
3 | // Pulse
4 | //
5 | // Created by Dawid Cieslak on 14/04/2018.
6 | // Copyright © 2018 Dawid Cieslak. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension Pulse {
12 |
13 | public convenience init( measureClosure: @escaping (() -> CGFloat), outputClosure: @escaping ((_ output: CGFloat) -> Void)) {
14 | let configuration = Pulse.Configuration(minimumValueStep: 0.005, Kp: 1.0, Ki: 0.1, Kd: 0.1)
15 | self.init(configuration: configuration, measureClosure: measureClosure, outputClosure: outputClosure)
16 | }
17 |
18 | public func showTunningView(minimumValue: CGFloat, maximumValue: CGFloat) {
19 | TunningWindow.shared.tuningContainerViewController.addTuningViewController(for: self, minValue: minimumValue, maxValue: maximumValue)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Vocable/Common/PublishedValue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PublishedValue.swift
3 | // Vocable AAC
4 | //
5 | // Created by Jesse Morgan on 3/26/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Combine
11 |
12 | @propertyWrapper struct PublishedValue {
13 |
14 | typealias Publisher = AnyPublisher
15 | private let subject: CurrentValueSubject
16 |
17 | var wrappedValue: T {
18 | didSet {
19 | subject.send(wrappedValue)
20 | }
21 | }
22 |
23 | var projectedValue: PublishedValue.Publisher {
24 | mutating get {
25 | return subject.eraseToAnyPublisher()
26 | }
27 | }
28 |
29 | init(wrappedValue: T) {
30 | self.wrappedValue = wrappedValue
31 | self.subject = CurrentValueSubject(self.wrappedValue)
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/AXIdentifier/AccessibilityID+Shared+Alert.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccessibilityID+Shared+Alert.swift
3 | // Vocable
4 | //
5 | // Created by Canan Arikan on 5/27/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension AccessibilityID.shared {
12 | public struct alert {
13 | public static let continueButton: AccessibilityID = "alert-button-continue-editing"
14 | public static let discardButton: AccessibilityID = "alert-button-discard-changes"
15 | public static let deleteButton: AccessibilityID = "alert-button-delete"
16 | public static let cancelButton: AccessibilityID = "alert-button-cancel"
17 | public static let createDuplicateButton: AccessibilityID = "alert-button-create-duplicate"
18 | public static let messageLabel: AccessibilityID = "alert-message"
19 | private init() {}
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules:
2 | - trailing_whitespace
3 | - identifier_name
4 | - compiler_protocol_init
5 | - unused_optional_binding
6 | - force_cast
7 | - force_try
8 | - type_name
9 | - large_tuple
10 | opt_in_rules:
11 | - empty_count
12 | - empty_string
13 | included:
14 | - Vocable
15 | excluded:
16 | - Carthage
17 | - Pods
18 | - SwiftLint/Common/3rdPartyLib
19 | line_length:
20 | warning: 250
21 | error: 300
22 | ignores_function_declarations: true
23 | ignores_comments: true
24 | ignores_urls: true
25 | function_body_length:
26 | warning: 300
27 | error: 500
28 | function_parameter_count:
29 | warning: 6
30 | error: 8
31 | type_body_length:
32 | warning: 500
33 | error: 500
34 | file_length:
35 | warning: 1000
36 | error: 1500
37 | ignore_comment_only_lines: true
38 | cyclomatic_complexity:
39 | warning: 15
40 | error: 25
41 | nesting:
42 | type_level: 5
43 | reporter: "xcode"
44 |
--------------------------------------------------------------------------------
/Vocable/Extensions/NSDiffableDataSourceSnapshot+Map.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSDiffableDataSourceSnapshot+Map.swift
3 | // Vocable
4 | //
5 | // Created by Jesse Morgan on 4/12/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension NSDiffableDataSourceSnapshot {
12 | func mapItemIdentifier(_ transform: (ItemIdentifierType) -> NewIdentifierType) -> NSDiffableDataSourceSnapshot {
13 | var updatedSnapshot = NSDiffableDataSourceSnapshot()
14 |
15 | updatedSnapshot.appendSections(sectionIdentifiers)
16 |
17 | for sectionId in sectionIdentifiers {
18 | let items = itemIdentifiers(inSection: sectionId).map(transform)
19 | updatedSnapshot.appendItems(items, toSection: sectionId)
20 | }
21 |
22 | return updatedSnapshot
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Tests/VocableUITests/Common/Environment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Environment.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 4/25/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 |
12 | protocol LaunchEnvironmentEncodable {
13 | func launchEnvironmentValue() -> String
14 | }
15 |
16 | struct Environment: XCTestAppConfigurable {
17 |
18 | private let key: LaunchEnvironment.Key
19 | private let value: String
20 |
21 | init(_ key: LaunchEnvironment.Key, value: String) {
22 | self.key = key
23 | self.value = value
24 | }
25 |
26 | init(_ key: LaunchEnvironment.Key, value: () -> T) where T: LaunchEnvironmentEncodable {
27 | self.key = key
28 | self.value = value().launchEnvironmentValue()
29 | }
30 |
31 | func configure(_ app: XCUIApplication) {
32 | app.launchEnvironment[key.rawValue] = value
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/Layout/Components/KeyboardLayoutElement+Padding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardLayoutElement+Padding.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 7/31/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension KeyboardLayoutElement {
12 |
13 | func padding(
14 | leading: some KeyboardLayoutRelativeWidth
15 | ) -> some KeyboardLayoutElement {
16 | environment(\.padding.leading, leading)
17 | }
18 |
19 | func padding(
20 | trailing: some KeyboardLayoutRelativeWidth
21 | ) -> some KeyboardLayoutElement {
22 | environment(\.padding.trailing, trailing)
23 | }
24 |
25 | func padding(
26 | leading: some KeyboardLayoutRelativeWidth,
27 | trailing: some KeyboardLayoutRelativeWidth
28 | ) -> some KeyboardLayoutElement {
29 | environment(\.padding, .init(leading: leading, trailing: trailing))
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Vocable/CoreData/NSPersistentContainer+Shared.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSPersistentContainer+Shared.swift
3 | // Vocable AAC
4 | //
5 | // Created by Chris Stroud on 2/21/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import CoreData
10 |
11 | extension NSPersistentContainer {
12 | private struct Storage {
13 | static let container: NSPersistentContainer = {
14 | let container = NSPersistentContainer(name: "Phrases")
15 | container.loadPersistentStores { (_, error) in
16 | if let error = error {
17 | assertionFailure("CoreData: Unresolved error \(error.localizedDescription)")
18 | return
19 | }
20 | container.viewContext.automaticallyMergesChangesFromParent = true
21 | }
22 | return container
23 | }()
24 | }
25 |
26 | static var shared: NSPersistentContainer {
27 | return Storage.container
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Vocable/Common/Views/ToastView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhraseSavedView.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 3/18/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @IBDesignable
12 | class ToastView: UIView {
13 |
14 | @IBOutlet private var iconView: UIImageView!
15 | @IBOutlet private var titleLabel: UILabel!
16 |
17 | var text: String = "" {
18 | didSet {
19 | titleLabel?.text = text
20 | }
21 | }
22 |
23 | init() {
24 | super.init(frame: .zero)
25 | commonInit()
26 | }
27 |
28 | override init(frame: CGRect) {
29 | super.init(frame: frame)
30 | commonInit()
31 | }
32 |
33 | required init?(coder aDecoder: NSCoder) {
34 | super.init(coder: aDecoder)
35 | commonInit()
36 | }
37 |
38 | private func commonInit() {
39 | layer.cornerRadius = 30
40 | layer.masksToBounds = true
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Vocable/Features/Settings/SelectionMode/Views/SettingsCollectionViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsCollectionViewCell.swift
3 | // Vocable AAC
4 | //
5 | // Created by Jesse Morgan on 2/26/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Combine
11 |
12 | final class SettingsCollectionViewCell: VocableCollectionViewCell {
13 |
14 | @IBOutlet private weak var textLabel: UILabel!
15 | @IBOutlet private weak var imageView: UIImageView!
16 |
17 | override func updateContent() {
18 | super.updateContent()
19 |
20 | let disabledColor = isEnabled ? .defaultTextColor : UIColor.defaultTextColor.withAlphaComponent(0.6)
21 | textLabel?.textColor = disabledColor
22 | imageView?.tintColor = disabledColor
23 | }
24 |
25 | func setup(title: String, image: UIImage?) {
26 | guard let image = image else { return }
27 |
28 | textLabel.text = title
29 | imageView.image = image
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/Vocable/Features/Voice/SoundEffect.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Sounds.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 1/5/21.
6 | // Copyright © 2021 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AudioToolbox
11 |
12 | enum SoundEffect: String {
13 |
14 | private struct Storage {
15 | var data = [SoundEffect: Data]()
16 | static var shared = Storage()
17 | }
18 |
19 | case listening = "Listening"
20 | case paused = "Paused"
21 |
22 | var soundData: Data? {
23 |
24 | if let contents = Storage.shared.data[self] {
25 | return contents
26 | }
27 |
28 | guard let url = Bundle.main.url(forResource: rawValue, withExtension: "wav"),
29 | let data = try? Data(contentsOf: url) else {
30 | assertionFailure("File not found for name \"\(rawValue)\" of type .wav")
31 | return nil
32 | }
33 | Storage.shared.data[self] = data
34 | return data
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Vocable/HeadTracking/Interpolation/InterpolableValues.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InterpolableValues.swift
3 | // Vocable AAC
4 | //
5 | // Created by Chris Stroud on 1/31/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CoreGraphics
11 |
12 | // Extensions of types that we would like to interpolate
13 | // to make them conform to Interpolable
14 |
15 | extension CGPoint: Interpolable {
16 | var interpolableValues: [Double] {
17 | return [Double(x), Double(y)]
18 | }
19 | init(interpolableValues: [Double]) {
20 | self.init(x: CGFloat(interpolableValues[0]), y: CGFloat(interpolableValues[1]))
21 | }
22 | }
23 |
24 | // MARK: -
25 |
26 | extension SIMD2: Interpolable where Scalar == Float {
27 | var interpolableValues: [Double] {
28 | return [Double(self[0]), Double(self[1])]
29 | }
30 | init(interpolableValues: [Double]) {
31 | self.init(Float(interpolableValues[0]), Float(interpolableValues[1]))
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Vocable/Features/Settings/TimingSensitivity/Model/CursorSensitivity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Sensitivity.swift
3 | // Vocable AAC
4 | //
5 | // Created by Jesse Morgan on 3/30/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | enum CursorSensitivity: Int, Codable, AnalyticsReportable {
12 |
13 | case low
14 | case medium
15 | case high
16 |
17 | // The minimum/maximum values to scale how quickly the cursor moves around the screen
18 | var range: ClosedRange {
19 | switch self {
20 | case .low:
21 | return (2.0 ... 4.0)
22 | case .medium:
23 | return (3.0 ... 5.0)
24 | case .high:
25 | return (4.0 ... 6.5)
26 | }
27 | }
28 |
29 | var analyticsDescription: String {
30 | switch self {
31 | case .low:
32 | return "low"
33 | case .medium:
34 | return "medium"
35 | case .high:
36 | return "high"
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Tests/VocableUITests/Screens/TimingAndSensitivityScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimingAndSensitivityScreen.swift
3 | // VocableUITests
4 | //
5 | // Created by Alex Facer on 7/1/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 |
12 | class TimingAndSensitivityScreen: BaseScreen {
13 |
14 | static let lowButton = XCUIApplication().buttons[.settings.timingAndSensitivity.lowSensitivityButton]
15 | static let mediumButton = XCUIApplication().buttons[.settings.timingAndSensitivity.mediumSensitivityButton]
16 | static let highButton = XCUIApplication().buttons[.settings.timingAndSensitivity.highSensitivityButton]
17 | static let reduceHoverTimeButton = XCUIApplication().buttons[.settings.timingAndSensitivity.decreaseHoverTimeButton]
18 | static let increaseHoverTimeButton = XCUIApplication().buttons[.settings.timingAndSensitivity.increaseHoverTimeButton]
19 | static let hoverTimeLabel = XCUIApplication().staticTexts[.settings.timingAndSensitivity.hoverTimeLabel]
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/Vocable/HeadTracking/UIHeadGazeCursorWindow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIHeadGazeCursorWindow.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 4/24/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class UIHeadGazeCursorWindow: UIWindow {
12 |
13 | var cursorViewController: UIVirtualCursorViewController {
14 | return rootViewController as! UIVirtualCursorViewController
15 | }
16 |
17 | override init(frame: CGRect) {
18 | super.init(frame: frame)
19 | commonInit()
20 | }
21 |
22 | override init(windowScene: UIWindowScene) {
23 | super.init(windowScene: windowScene)
24 | commonInit()
25 | }
26 |
27 | required init?(coder: NSCoder) {
28 | super.init(coder: coder)
29 | commonInit()
30 | }
31 |
32 | private func commonInit() {
33 | self.rootViewController = UIVirtualCursorViewController()
34 | self.isUserInteractionEnabled = false
35 | self.backgroundColor = .clear
36 | self.isOpaque = false
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/Layout/Components/KeyboardBody.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardBody.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 7/31/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct KeyboardBody {
12 | private(set) var root: any KeyboardLayoutElement
13 |
14 | init(root: any KeyboardLayoutElement) {
15 | self.root = root
16 | var baseIndex = 0
17 | KeyboardBody.rebaseElement(&self.root, baseRowIndex: &baseIndex)
18 | }
19 |
20 | private static func rebaseElement(
21 | _ element: inout any KeyboardLayoutElement,
22 | baseRowIndex: inout Int
23 | ) {
24 | if let row = element as? KeyboardLayoutRow {
25 | element = row.withIndex(baseRowIndex)
26 | baseRowIndex += 1
27 | }
28 | for index in element.children.indices {
29 | rebaseElement(
30 | &element.children[index],
31 | baseRowIndex: &baseRowIndex
32 | )
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/SuggestionsUI/AnimationContainerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnimationContainerView.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 7/31/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | class AnimationContainerView: UIView {
13 |
14 | let child: T
15 |
16 | init(child: T) {
17 | self.child = child
18 | super.init(frame: .zero)
19 | commonInit()
20 | }
21 |
22 | required init?(coder: NSCoder) {
23 | fatalError()
24 | }
25 |
26 | private func commonInit() {
27 | child.translatesAutoresizingMaskIntoConstraints = false
28 | addSubview(child)
29 |
30 | NSLayoutConstraint.activate([
31 | child.topAnchor.constraint(equalTo: topAnchor),
32 | child.bottomAnchor.constraint(equalTo: bottomAnchor),
33 | child.leadingAnchor.constraint(equalTo: leadingAnchor),
34 | child.trailingAnchor.constraint(equalTo: trailingAnchor)
35 | ])
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Vocable/SwiftUI/GazeBlocking.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GazeBlocking.swift
3 | // Vocable
4 | //
5 | // Created by Robert Moyer on 5/16/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | extension View {
12 | /// Prevents any gaze events from being passed to views beneath this one
13 | ///
14 | /// This can be useful when presenting a sheet or a full-screen cover on
15 | /// top of views that are capable of receiving gaze events.
16 | func gazeBlocking() -> some View {
17 | modifier(GazeBlocking())
18 | }
19 | }
20 |
21 | private struct GazeBlocking: ViewModifier {
22 | func body(content: Content) -> some View {
23 | content.overlay(EatGaze())
24 | }
25 | }
26 |
27 | /// A view that does not allow gazes to pass through it
28 | private struct EatGaze: UIViewRepresentable {
29 | func makeUIView(context: Context) -> GazeEatingView {
30 | return GazeEatingView()
31 | }
32 |
33 | func updateUIView(
34 | _ uiView: GazeEatingView,
35 | context: Context
36 | ) { /* No op */ }
37 | }
38 |
--------------------------------------------------------------------------------
/Vocable/Extensions/NSPredicate+Operators.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSPredicate+Operators.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 3/14/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension NSPredicate {
12 |
13 | static func && (_ lhs: NSPredicate, _ rhs: NSPredicate) -> NSPredicate {
14 | NSCompoundPredicate(andPredicateWithSubpredicates: [lhs, rhs])
15 | }
16 |
17 | static func || (_ lhs: NSPredicate, _ rhs: NSPredicate) -> NSPredicate {
18 | NSCompoundPredicate(orPredicateWithSubpredicates: [lhs, rhs])
19 | }
20 |
21 | static prefix func ! (_ lhs: NSPredicate) -> NSPredicate {
22 | NSCompoundPredicate(notPredicateWithSubpredicate: lhs)
23 | }
24 |
25 | static func &= (_ lhs: inout NSPredicate, _ rhs: NSPredicate) {
26 | lhs = NSCompoundPredicate(andPredicateWithSubpredicates: [lhs, rhs])
27 | }
28 |
29 | static func |= (_ lhs: inout NSPredicate, _ rhs: NSPredicate) {
30 | lhs = NSCompoundPredicate(orPredicateWithSubpredicates: [lhs, rhs])
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Vocable/Extensions/TextSuggestionController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextSuggestionController.swift
3 | // Vocable AAC
4 | //
5 | // Created by Kyle Ohanian on 4/15/19.
6 | // Copyright © 2019 WillowTree. All rights reserved.
7 | //
8 | import UIKit
9 |
10 | class TextSuggestionController {
11 |
12 | private let checker = UITextChecker()
13 |
14 | func suggestions(for expression: TextExpression) -> [String] {
15 | let fullExpression = expression.value
16 | let lastWord = expression.lastWord() ?? ""
17 | let range = NSRange(location: (fullExpression as NSString).length - (lastWord as NSString).length, length: (lastWord as NSString).length)
18 | var joinedArray: [String] = []
19 | let guesses = checker.guesses(forWordRange: range, in: fullExpression, language: Locale.current.identifier) ?? []
20 | let completions = checker.completions(forPartialWordRange: range, in: fullExpression, language: Locale.current.identifier) ?? []
21 | joinedArray.append(contentsOf: completions)
22 | joinedArray.append(contentsOf: guesses)
23 | return joinedArray
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 WillowTree, LLC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/AXIdentifier/AccessibilityID+Settings+TimingSensitivity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccessibilityID+Settings+TimingSensitivity.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 5/11/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension AccessibilityID.settings {
12 | public struct timingAndSensitivity {
13 | public static let decreaseHoverTimeButton: AccessibilityID = "timing-and-sensitivity-decrease-hover-time-button"
14 | public static let increaseHoverTimeButton: AccessibilityID = "timing-and-sensitivity-increase-hover-time-button"
15 | public static let lowSensitivityButton: AccessibilityID = "timing-and-sensitivity-low-sensitivity-button"
16 | public static let mediumSensitivityButton: AccessibilityID = "timing-and-sensitivity-medium-sensitivity-button"
17 | public static let highSensitivityButton: AccessibilityID = "timing-and-sensitivity-high-sensitivity-button"
18 | public static let hoverTimeLabel: AccessibilityID = "timing-and-sensitivity-hover-time-label"
19 | private init() {}
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Vocable/Common/VocableNavigationController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VocableNavigationController.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 4/30/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class VocableNavigationController: UINavigationController {
12 |
13 | required init?(coder aDecoder: NSCoder) {
14 | super.init(coder: aDecoder)
15 | commonInit()
16 | }
17 | override init(rootViewController: UIViewController) {
18 | super.init(rootViewController: rootViewController)
19 | commonInit()
20 | }
21 |
22 | override init(navigationBarClass: AnyClass?, toolbarClass: AnyClass?) {
23 | super.init(navigationBarClass: navigationBarClass, toolbarClass: toolbarClass)
24 | commonInit()
25 | }
26 |
27 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
28 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
29 | commonInit()
30 | }
31 |
32 | private func commonInit() {
33 | modalPresentationStyle = .fullScreen
34 | setNavigationBarHidden(true, animated: false)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/.github/workflows/on_pull_request.yml:
--------------------------------------------------------------------------------
1 | name: On pull request
2 | on:
3 | workflow_dispatch:
4 | pull_request:
5 | branches:
6 | - develop
7 |
8 | jobs:
9 | on_pull_request:
10 | runs-on: macos-14
11 | steps:
12 | - uses: actions/checkout@v4
13 |
14 | - name: Install SSH key
15 | uses: shimataro/ssh-key-action@v2
16 | with:
17 | key: ${{ secrets.FASTLANE_MATCH_GIT_PRIVATE_KEY }}
18 | known_hosts: ${{ secrets.MATCH_SSH_KNOWN_HOSTS }}
19 | name: id_rsa_match
20 | if_key_exists: replace
21 |
22 | - name: Set up Ruby environment
23 | uses: ruby/setup-ruby@v1
24 | with:
25 | ruby-version: 3.3.1
26 |
27 | - name: Install bundler
28 | run: gem install bundler
29 |
30 | - name: Install dependencies
31 | run: bundle install
32 |
33 | - name: Install SwiftLint
34 | run: brew install swiftlint
35 |
36 | - name: Run Unit and UI tests
37 | uses: ./.github/workflows/actions/run_lane
38 | with:
39 | lane: test_unit_ui
40 | match_password: ${{ secrets.MATCH_PASSWORD }}
41 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/SuggestionsUI/KeyboardSuggestionAnimationContainer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardSuggestionAnimationContainer.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 7/31/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | extension KeyboardSuggestionsView {
13 | final class SuggestionAnimationContainer: AnimationContainerView {
14 |
15 | typealias ScaleContainer = AnimationContainerView
16 | typealias OpacityContainer = AnimationContainerView
17 |
18 | let button: SuggestionButton
19 | let scaleContainer: ScaleContainer
20 | let opacityContainer: OpacityContainer
21 |
22 | init() {
23 | self.button = SuggestionButton()
24 | self.scaleContainer = AnimationContainerView(child: button)
25 | self.opacityContainer = AnimationContainerView(child: scaleContainer)
26 | super.init(child: opacityContainer)
27 | }
28 |
29 | required init?(coder: NSCoder) {
30 | fatalError("init(coder:) has not been implemented")
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Vocable/Extensions/UIFont+Helpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIFont+Helpers.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 5/17/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | extension UIFont {
13 |
14 | static func settingsCellTitle() -> UIFont {
15 | UIFont.systemFont(ofSize: 22, weight: .bold)
16 | }
17 |
18 | static func textEditor(
19 | satisfying traitCollection: UITraitCollection = .current
20 | ) -> UIFont {
21 | let fontSize: CGFloat = (traitCollection.sizeClass == .hRegular_vRegular) ? 40 : 28
22 | let desiredFont = UIFont.systemFont(ofSize: fontSize, weight: .bold)
23 | return desiredFont
24 | }
25 |
26 | static func keyboardKey(
27 | satisfying traitCollection: UITraitCollection = .current
28 | ) -> UIFont {
29 | return switch traitCollection.sizeClass {
30 | case .hCompact_vCompact, .hRegular_vCompact:
31 | .boldSystemFont(ofSize: 28)
32 | case .hCompact_vRegular:
33 | .boldSystemFont(ofSize: 22)
34 | default:
35 | .boldSystemFont(ofSize: 48)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Vocable/SwiftUI/Gaze Button/ButtonState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ButtonState.swift
3 | // Vocable
4 | //
5 | // Created by Robert Moyer on 4/7/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | /// Constants describing the state of a button
10 | struct ButtonState: OptionSet {
11 | let rawValue: UInt
12 |
13 | /// The normal or default state of a button, neither selected nor highlighted
14 | static let normal = Self([])
15 |
16 | /// Highlighted state of a button
17 | ///
18 | /// A button becomes highlighted when a touch or gaze event
19 | /// enters the button's bounds, and it loses that highlight
20 | /// when there is a touch-up event or when the touch/gaze event
21 | /// exits the button's bounds.
22 | static let highlighted = Self(rawValue: 1 << 0)
23 |
24 | /// Selected state of a button
25 | ///
26 | /// A button becomes selected when a gaze event remains within
27 | /// the button's bounds for the supplied `minimumGazeDuration`.
28 | /// It loses its selection when the gaze eventis cancelled, ended,
29 | /// or when the gaze exits the button's bounds.
30 | static let selected = Self(rawValue: 1 << 1)
31 | }
32 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/Layout/Components/KeyboardLayoutRelativeWidth/KeyboardLayoutRelativeWidth.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardLayoutRelativeWidth.swift
3 | //
4 | //
5 | // Created by Chris Stroud on 6/17/24.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol KeyboardLayoutRelativeWidth: Hashable {
11 | func value(
12 | in containerWidth: CGFloat,
13 | environment: KeyboardLayoutEnvironment
14 | ) -> CGFloat
15 |
16 | static prefix func - (_ value: Self) -> Self
17 | }
18 |
19 | func - (lhs: any KeyboardLayoutRelativeWidth, rhs: any KeyboardLayoutRelativeWidth) -> KeyboardLayoutRelativeWidthCompound {
20 | .compound(lhs, -AnyKeyboardLayoutRelativeWidth(rhs))
21 | }
22 |
23 | func -= (lhs: inout any KeyboardLayoutRelativeWidth, rhs: any KeyboardLayoutRelativeWidth) {
24 | lhs = .compound(lhs, -AnyKeyboardLayoutRelativeWidth(rhs))
25 | }
26 |
27 | func + (lhs: any KeyboardLayoutRelativeWidth, rhs: any KeyboardLayoutRelativeWidth) -> KeyboardLayoutRelativeWidthCompound {
28 | .compound(lhs, rhs)
29 | }
30 |
31 | func += (lhs: inout any KeyboardLayoutRelativeWidth, rhs: any KeyboardLayoutRelativeWidth) {
32 | lhs = KeyboardLayoutRelativeWidthCompound(lhs: lhs, rhs: rhs)
33 | }
34 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/Layout/Components/KeyboardLayoutElement+Sizing.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardLayoutElement+Sizing.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 7/31/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension KeyboardLayoutElement {
12 | func keyWidth(
13 | count: Int,
14 | span: Int? = nil
15 | ) -> some KeyboardLayoutElement {
16 | self.mutatingEnvironment { environment in
17 | environment.columnCount = count
18 | if let span {
19 | environment.spanCount = span
20 | }
21 | environment.keySizingStrategy = .columns
22 | }
23 | }
24 |
25 | func keyWidth(
26 | span: Int
27 | ) -> some KeyboardLayoutElement {
28 | self.mutatingEnvironment { environment in
29 | environment.spanCount = span
30 | }
31 | }
32 |
33 | func keyWidth(
34 | proportional multiplier: CGFloat
35 | ) -> some KeyboardLayoutElement {
36 | self.mutatingEnvironment { environment in
37 | environment.keySizingStrategy = .proportional(multiplier: multiplier)
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Vocable/Common/CategoryReordering/CategoryReordering.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CategoryReordering.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 3/29/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct CategoryOrderabilityModel {
12 |
13 | private let upwardOrderableIndices: Range
14 | private let downwardOrderableIndices: Range
15 |
16 | init(categories: T) {
17 | let lowestDownwardOrderableIndex = categories.index(categories.startIndex, offsetBy: 1, limitedBy: categories.endIndex) ?? categories.endIndex
18 | downwardOrderableIndices = lowestDownwardOrderableIndex ..< categories.endIndex
19 |
20 | let highestUpwardOrderableIndex = categories.index(categories.endIndex, offsetBy: -1, limitedBy: categories.startIndex) ?? categories.startIndex
21 | upwardOrderableIndices = categories.startIndex ..< highestUpwardOrderableIndex
22 | }
23 |
24 | func canMoveToHigherIndex(from index: T.Index) -> Bool {
25 | upwardOrderableIndices.contains(index)
26 | }
27 |
28 | func canMoveToLowerIndex(from index: T.Index) -> Bool {
29 | downwardOrderableIndices.contains(index)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Tests/UITests.xctestplan:
--------------------------------------------------------------------------------
1 | {
2 | "configurations" : [
3 | {
4 | "id" : "77005B39-8254-403F-B2F5-DC55FF074BF6",
5 | "name" : "Configuration 1",
6 | "options" : {
7 |
8 | }
9 | }
10 | ],
11 | "defaultOptions" : {
12 | "codeCoverage" : false,
13 | "commandLineArgumentEntries" : [
14 |
15 | ],
16 | "environmentVariableEntries" : [
17 | {
18 | "enabled" : false,
19 | "key" : "mixpanelToken",
20 | "value" : "Do not commit a real token"
21 | },
22 | {
23 | "key" : "IDEPreferLogStreaming",
24 | "value" : "YES"
25 | }
26 | ],
27 | "repeatInNewRunnerProcess" : true,
28 | "targetForVariableExpansion" : {
29 | "containerPath" : "container:Vocable.xcodeproj",
30 | "identifier" : "130C00B320D2AD2A007C3163",
31 | "name" : "Vocable"
32 | },
33 | "testExecutionOrdering" : "random",
34 | "testRepetitionMode" : "retryOnFailure",
35 | "testTimeoutsEnabled" : true
36 | },
37 | "testTargets" : [
38 | {
39 | "target" : {
40 | "containerPath" : "container:Vocable.xcodeproj",
41 | "identifier" : "17191A2E245089270079F9BE",
42 | "name" : "VocableUITests"
43 | }
44 | }
45 | ],
46 | "version" : 1
47 | }
48 |
--------------------------------------------------------------------------------
/Vocable/Common/CarouselGridLayout+Masks.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CarouselGridLayout+Masks.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 4/17/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct CellSeparatorMask: OptionSet {
12 | let rawValue: Int
13 |
14 | static let top = CellSeparatorMask(rawValue: 1 << 0)
15 | static let bottom = CellSeparatorMask(rawValue: 1 << 1)
16 | static let both: CellSeparatorMask = [.top, .bottom]
17 | }
18 |
19 | struct CellOrdinalButtonMask: OptionSet {
20 | let rawValue: Int
21 |
22 | static let topUpArrow = CellOrdinalButtonMask(rawValue: 1 << 0)
23 | static let bottomDownArrow = CellOrdinalButtonMask(rawValue: 1 << 1)
24 | static let both: CellOrdinalButtonMask = [.topUpArrow, .bottomDownArrow]
25 | static let none: CellOrdinalButtonMask = []
26 | }
27 |
28 | extension CarouselGridLayout {
29 |
30 | func separatorMask(for indexPath: IndexPath) -> CellSeparatorMask {
31 | let index = indexPath.item
32 | let rowIndexWithinPage = Int((index % itemsPerPage) / columnCount)
33 |
34 | if rowIndexWithinPage == 0 {
35 | return .both
36 | } else {
37 | return .bottom
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/AXIdentifier/AccessibilityID+Settings.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccessibilityID+Settings.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 5/11/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension AccessibilityID {
12 | public struct settings {
13 | public static let categoriesAndPhrasesCell: AccessibilityID = "settings-categories-and-phrases-cell"
14 | public static let timingAndSensitivityCell: AccessibilityID = "settings-timing-and-sensitivity-cell"
15 | public static let resetAppSettingsCell: AccessibilityID = "settings-reset-app-settings-cell"
16 | public static let listeningModeCell: AccessibilityID = "settings-listening-mode-cell"
17 | public static let selectionModeCell: AccessibilityID = "settings-selection-mode-cell"
18 | public static let privacyPolicyCell: AccessibilityID = "settings-privacy-policy-cell"
19 | public static let contactDevelopersCell: AccessibilityID = "settings-contact-developers-cell"
20 | public static let voiceSettingsCell: AccessibilityID = "settings-voice-settings-cell"
21 | public static let keyboardLayoutCell: AccessibilityID = "settings-keyboard-layout-cell"
22 | private init() {}
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Vocable/CoreData/ModelObjectExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ModelObjectExtensions.swift
3 | // Vocable AAC
4 | //
5 | // Created by Chris Stroud on 2/21/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 | import CoreData
9 | import Foundation
10 |
11 | extension Category: NSManagedObjectIdentifiable {
12 | typealias IdentifierType = String
13 |
14 | public override func awakeFromInsert() {
15 | super.awakeFromInsert()
16 | setPrimitiveValue(Date(), forKey: #keyPath(creationDate))
17 | setPrimitiveValue(false, forKey: #keyPath(isUserGenerated))
18 | setPrimitiveValue(Int32.max, forKey: #keyPath(ordinal))
19 | setPrimitiveValue(false, forKey: #keyPath(isUserRemoved))
20 | setPrimitiveValue(false, forKey: #keyPath(isUserRenamed))
21 | }
22 | }
23 |
24 | extension Phrase: NSManagedObjectIdentifiable {
25 | typealias IdentifierType = String
26 |
27 | public override func awakeFromInsert() {
28 | super.awakeFromInsert()
29 | setPrimitiveValue(Date(), forKey: #keyPath(creationDate))
30 | setPrimitiveValue(false, forKey: #keyPath(isUserGenerated))
31 | setPrimitiveValue(false, forKey: #keyPath(isUserRemoved))
32 | setPrimitiveValue(false, forKey: #keyPath(isUserRenamed))
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Vocable/Common/NSAttributedString+Helpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSAttributedString+Helpers.swift
3 | // Vocable
4 | //
5 | // Created by Jesse Morgan on 4/8/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension NSAttributedString {
12 |
13 | static func imageAttachedString(for string: String, with image: UIImage, attributes: [Key: Any]? = nil) -> NSAttributedString {
14 | let isRightToLeftLayout = UITraitCollection.current.layoutDirection == .rightToLeft
15 |
16 | let formatString: String = isRightToLeftLayout ?
17 | .localizedStringWithFormat("%@ ", string) :
18 | .localizedStringWithFormat(" %@", string)
19 |
20 | let text = NSMutableAttributedString(string: formatString, attributes: attributes)
21 | let attachment = NSMutableAttributedString(attachment: NSTextAttachment(image: image))
22 | if let attributes = attributes {
23 | attachment.addAttributes(attributes, range: .entireRange(of: attachment.string))
24 | }
25 |
26 | let textRange = NSRange(of: text.string)
27 | let insertionIndex = isRightToLeftLayout ? textRange.upperBound : textRange.lowerBound
28 |
29 | text.insert(attachment, at: insertionIndex)
30 | return text
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/Vocable/GoogleService-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CLIENT_ID
6 | 292071094533-86uue52ck8hejnsh3unfoccvk5g7svnl.apps.googleusercontent.com
7 | REVERSED_CLIENT_ID
8 | com.googleusercontent.apps.292071094533-86uue52ck8hejnsh3unfoccvk5g7svnl
9 | API_KEY
10 | AIzaSyCeSvWmH6Z8O3fnxxmUH9Wl64HfeJeqzz8
11 | GCM_SENDER_ID
12 | 292071094533
13 | PLIST_VERSION
14 | 1
15 | BUNDLE_ID
16 | com.willowtreeapps.eyespeakaac
17 | PROJECT_ID
18 | vocable-fcb07
19 | STORAGE_BUCKET
20 | vocable-fcb07.appspot.com
21 | IS_ADS_ENABLED
22 |
23 | IS_ANALYTICS_ENABLED
24 |
25 | IS_APPINVITE_ENABLED
26 |
27 | IS_GCM_ENABLED
28 |
29 | IS_SIGNIN_ENABLED
30 |
31 | GOOGLE_APP_ID
32 | 1:292071094533:ios:958a5ecf0d4fcabb909f6c
33 | DATABASE_URL
34 | https://vocable-fcb07.firebaseio.com
35 |
36 |
--------------------------------------------------------------------------------
/Tests/VocableUITests/XCUIElement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // XCUIElement.swift
3 | // VocableUITests
4 | //
5 | // Created by Rudy Salas on 4/15/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 |
12 | extension XCUIElement {
13 |
14 | /// Waits the amount of time you specify for the element’s exists property to become true and then taps on it.
15 | ///
16 | /// Returns false if the timeout expires without the element coming into existence. In this case, the `tap` action
17 | /// will not occur.
18 | func tapWhenExists(
19 | timeout: TimeInterval = 0.5,
20 | file: StaticString = #file,
21 | line: UInt = #line
22 | ) throws {
23 | try assertExistence(timeout: timeout, file: file, line: line)
24 | self.tap()
25 | }
26 |
27 | func assertExistence(
28 | timeout: TimeInterval = 0.5,
29 | _ message: String? = nil,
30 | file: StaticString = #file,
31 | line: UInt = #line
32 | ) throws {
33 | guard self.waitForExistence(timeout: timeout) else {
34 | let message = message ?? "Element did not come into existence before timeout."
35 | XCTFail(message, file: file, line: line)
36 | throw XCTestError(.timeoutWhileWaiting)
37 | }
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/Vocable/Extensions/Phrase+Helpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Phrase+Helpers.swift
3 | // Vocable AAC
4 | //
5 | // Created by Jesse Morgan on 3/18/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CoreData
11 |
12 | extension Phrase {
13 |
14 | var analyticsName: String? {
15 | isUserGenerated ? "Custom Phrase" : utterance
16 | }
17 |
18 | var isAnalyticsReportable: Bool {
19 | !isUserRenamed && !isUserGenerated
20 | }
21 |
22 | static func create(withUserEntry text: String, in context: NSManagedObjectContext) throws -> Phrase {
23 | let userFavorites = try Category.fetch(.userFavorites, in: context)
24 | return create(withUserEntry: text, category: userFavorites, in: context)
25 | }
26 |
27 | static func create(withUserEntry text: String, category: Category, in context: NSManagedObjectContext) -> Phrase {
28 | let newIdentifier = "user_\(UUID().uuidString)"
29 | let phrase = Phrase.fetchOrCreate(in: context, matching: newIdentifier)
30 | phrase.isUserGenerated = true
31 | phrase.creationDate = Date()
32 | phrase.utterance = text
33 | phrase.languageCode = AppConfig.activePreferredLanguageCode
34 | phrase.category = category
35 | category.addToPhrases(phrase)
36 | return phrase
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/UI/KeyboardKeyAction+Representation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardKeyAction.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 7/31/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | extension KeyboardKeyAction {
13 |
14 | enum Representation: Hashable {
15 | case image(UIImage)
16 | case string(String)
17 | }
18 |
19 | var representation: Representation {
20 | return switch self {
21 | case .insertCharacter(let value):
22 | .string(String(value))
23 | case .clear:
24 | .image(UIImage(systemName: "trash")!)
25 | case .backspace:
26 | .image(UIImage(systemName: "delete.left")!)
27 | case .space:
28 | .image(UIImage(systemName: "space")!)
29 | case .speak:
30 | .image(UIImage(systemName: "person.wave.2.fill")!)
31 | case .numberPad:
32 | .image(UIImage(systemName: "textformat.123")!)
33 | case .alphabet:
34 | .image(UIImage(systemName: "abc")!)
35 | case .openModifierPicker, .closeModifierPicker:
36 | .image(UIImage(systemName: "ellipsis")!)
37 | case .beginModifier(let value), .endModifier(let value):
38 | .string("\u{25CC}\(value)")
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Vocable/HeadTracking/Extensions.swift:
--------------------------------------------------------------------------------
1 | import simd
2 |
3 | /*
4 | * The transformation in ARKit is column-major, which means for transformation matrix m,
5 | * m[i] is the ith column of the matrix, and m[i][j] is the elements in the ith column and jth row
6 | * In addition, (m[3][0], m[3][1], m[3][2], m[3][3]) holds the translation amount in x, y, and z direction respectively.
7 | */
8 | extension matrix_float4x4: CustomStringConvertible {
9 | public var debugDescription: String {
10 | let m = self
11 | return """
12 | \(m[0][0]), \(m[1][0]), \(m[2][0]), \(m[3][0])
13 | \(m[0][1]), \(m[1][1]), \(m[2][1]), \(m[3][1])
14 | \(m[0][2]), \(m[1][2]), \(m[2][2]), \(m[3][2])
15 | \(m[0][3]), \(m[1][3]), \(m[2][3]), \(m[3][3])
16 | """
17 | }
18 | public var description: String {
19 | return self.debugDescription
20 | }
21 | public static let identity: matrix_float4x4 = matrix_float4x4(columns: (simd_float4(1, 0, 0, 0), simd_float4(0, 1, 0, 0), simd_float4(0, 0, 1, 0), simd_float4(0, 0, 0, 1)))
22 | }
23 |
24 | extension BinaryInteger {
25 | var degreesToRadians: Float {
26 | return Float(Int(self)) * .pi / 180.0
27 | }
28 | }
29 |
30 | extension FloatingPoint {
31 | var degreesToRadians: Self { return self * .pi / 180 }
32 | var radiansToDegrees: Self { return self * 180 / .pi }
33 | }
34 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Vocable Contributing Guidelines
2 |
3 | 🥳🎉 Thank you for contributing to Vocable! 🎉🥳
4 |
5 | Contributions from the open-source community are crucial in helping us meet make the best AAC app possible.
6 |
7 | # How to Contribute
8 |
9 | ### Reporting Bugs
10 | - Check the existing issues to make sure the bug has not already been reported.
11 | - Open an issue describing the bug.
12 | - Be sure to use a descriptive title and include detailed steps on how to reproduce the issue.
13 | - Explain the expected behavior vs. the actual behavior.
14 | - Include the version number.
15 | - Tag the issue with the `bug` label.
16 |
17 |
18 | ### Suggesting Enhancements
19 | - Open an issue clearly describing the behavior you would like to see.
20 | - If applicable, provide a few examples of how the feature would work and what purpose it would serve.
21 | - Tag the issue with the `enhancement` label.
22 |
23 | ### Contributing Code by Opening a Pull Request
24 | To contribute to Vocable, please fork the GitHub repo and submit a pull request to the `develop` branch.
25 |
26 | Our code owners will review your work and merge once any issues have been addressed. If the PR gets too outdated we may ask you to rebase and force push to update the PR.
27 |
28 | That's it! Our team will merge your PR, and your changes will be included in the next app version. Thanks for your contribution! 💯
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/Layout/Components/KeyboardLayoutPreviewView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardLayoutPreviewView.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 7/31/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 | import UIKit
12 |
13 | @available(iOS 17.0, *)
14 | private struct KeyboardViewRepresentable: UIViewRepresentable {
15 |
16 | let layout: Layout
17 | private(set) var mode: KeyboardLayoutMode = .alphabetical
18 |
19 | func makeUIView(context: Context) -> KeyboardView {
20 | KeyboardView(previewLayout: layout)
21 | }
22 |
23 | func updateUIView(_ uiView: KeyboardView, context: Context) {
24 | uiView.mode = mode
25 | }
26 | }
27 |
28 | struct KeyboardLayoutPreviewView: View {
29 |
30 | let layout: Layout
31 | private(set) var mode: KeyboardLayoutMode = .alphabetical
32 |
33 | var body: some View {
34 | if #available(iOS 17.0, *) {
35 | KeyboardViewRepresentable(layout: layout, mode: mode)
36 | .safeAreaPadding(.all)
37 | .background(Color(uiColor: .collectionViewBackgroundColor))
38 | } else {
39 | Text(verbatim: "Preview requires iOS 17.0")
40 | .font(.title)
41 | .frame(maxWidth: .infinity, maxHeight: .infinity)
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/Layout/Components/KeyboardLayoutRelativeWidth/KeyboardLayoutRelativeWidthFixed.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardLayoutRelativeWidthFixed.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 7/31/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct KeyboardLayoutRelativeWidthFixed: KeyboardLayoutRelativeWidth {
12 | let value: CGFloat
13 |
14 | init(value: CGFloat) {
15 | self.value = value
16 | }
17 |
18 | func value(
19 | in containerWidth: CGFloat,
20 | environment: KeyboardLayoutEnvironment
21 | ) -> CGFloat {
22 | self.value * signMultiplier()
23 | }
24 |
25 | private var sign: FloatingPointSign = .plus
26 |
27 | private func signMultiplier() -> CGFloat {
28 | switch sign {
29 | case .plus: 1.0
30 | case .minus: -1.0
31 | }
32 | }
33 |
34 | static prefix func - (value: Self) -> Self {
35 | var value = value
36 | switch value.sign {
37 | case .plus:
38 | value.sign = .minus
39 | case .minus:
40 | value.sign = .plus
41 | }
42 | return value
43 | }
44 | }
45 |
46 | extension KeyboardLayoutRelativeWidth where Self == KeyboardLayoutRelativeWidthFixed {
47 |
48 | static func fixed(_ value: CGFloat) -> Self {
49 | KeyboardLayoutRelativeWidthFixed(value: value)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Vocable/SwiftUI/Gaze Button/DefaultGazeButtonStyle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultGazeButtonStyle.swift
3 | // Vocable
4 | //
5 | // Created by Robert Moyer on 4/7/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// The default button style.
12 | ///
13 | /// You can also use ``GazeButtonStyle/automatic`` to construct this style.
14 | struct DefaultGazeButtonStyle: GazeButtonStyle {
15 | func makeBody(_ configuration: Configuration) -> some View {
16 | _Body(configuration)
17 | }
18 |
19 | private struct _Body: View {
20 | @Environment(\.isEnabled)
21 | private var isEnabled
22 |
23 | @Binding var state: ButtonState
24 |
25 | var label: Label
26 | var buttonRole: ButtonRole?
27 |
28 | init(_ configuration: Configuration) where Configuration.Label == Label {
29 | self._state = configuration.state
30 | self.label = configuration.label
31 | self.buttonRole = configuration.role
32 | }
33 |
34 | var body: some View {
35 | label
36 | .foregroundColor(buttonRole == .destructive ? .red : .accentColor)
37 | .opacity(
38 | (state.contains(.highlighted) || !isEnabled) ? 0.5 : 1
39 | )
40 | }
41 | }
42 | }
43 |
44 | extension GazeButtonStyle where Self == DefaultGazeButtonStyle {
45 | /// The default button style
46 | static var automatic: DefaultGazeButtonStyle { .init() }
47 | }
48 |
--------------------------------------------------------------------------------
/Tests/VocableUITests/Tests/PaginationBaseTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomCategoriesBaseTest.swift
3 | // VocableUITests
4 | //
5 | // Created by Rudy Salas and Canan Arikan on 5/06/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class PaginationBaseTest: XCTestCase {
12 |
13 | static private func phrases(count: Int) -> [Phrase] {
14 | (1...count).map { _ in
15 | Phrase("Hello")
16 | }
17 | }
18 |
19 | let eightPhrasesCategory = Category(id: "first_category", "Category 1", phrases: PaginationBaseTest.phrases(count: 8))
20 | let ninePhrasesCategory = Category(id: "second_category", "Category 2", phrases: PaginationBaseTest.phrases(count: 9))
21 | let sevenPhrasesCategory = Category(id: "third_category", "Category 3", phrases: PaginationBaseTest.phrases(count: 7))
22 | let twoPhrasesCategory = Category(id: "fourth_category", "Category 4", phrases: PaginationBaseTest.phrases(count: 2))
23 |
24 | override func setUp() {
25 | let app = XCUIApplication()
26 | app.configure {
27 | Arguments(.resetAppDataOnLaunch, .enableListeningMode, .disableAnimations)
28 | Environment(.overridePresets) {
29 | Presets {
30 | eightPhrasesCategory
31 | ninePhrasesCategory
32 | sevenPhrasesCategory
33 | twoPhrasesCategory
34 | }
35 | }
36 | }
37 | app.launch()
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/Layout/Components/KeyboardLayoutGroup.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardLayoutGroup.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 7/31/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct KeyboardLayoutGroup: KeyboardLayoutElement {
12 |
13 | var children: [any KeyboardLayoutElement]
14 |
15 | init(children: [any KeyboardLayoutElement]) {
16 | self.children = children
17 | }
18 |
19 | init(@KeyboardLayoutContent builder: () -> [any KeyboardLayoutElement]) {
20 | self.init(children: builder())
21 | }
22 |
23 | func makeKeys(environment: KeyboardLayoutEnvironment) -> [KeyboardLayoutKey] {
24 | let indexed = children.indexed()
25 | return indexed.flatMap { (index, key) -> [KeyboardLayoutKey] in
26 | let isFirst = (index == indexed.startIndex)
27 | let isLast = (index == indexed.endIndex)
28 | return if isFirst && isLast {
29 | key.makeKeys(environment: environment)
30 | } else if isFirst {
31 | key.environment(\.padding.trailing, .zero)
32 | .makeKeys(environment: environment)
33 | } else if isLast {
34 | key.environment(\.padding.leading, .zero)
35 | .makeKeys(environment: environment)
36 | } else {
37 | key.environment(\.padding, .zero)
38 | .makeKeys(environment: environment)
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Tests/VocableUITests/Tests/PresetsOverrideTestCase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PresetsOverrideTestCase.swift
3 | // VocableUITests
4 | //
5 | // Created by Chris Stroud on 4/22/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class PresetsOverrideTestCase: XCTestCase {
12 |
13 | override class func setUp() {
14 | super.setUp()
15 |
16 | let app = XCUIApplication()
17 | app.configure {
18 | Arguments(.resetAppDataOnLaunch, .enableListeningMode, .disableAnimations)
19 | Environment(.overridePresets) {
20 | Presets {
21 | Category("Custom Basic Needs") {
22 | Phrase("Test Phrase")
23 | Phrase("Another Phrase")
24 | Phrase(id: "specific_id", "With some text")
25 | Phrase(id: "another_id", languageCode: "es", "No sé donde estoy")
26 | }
27 | Category("Another Category") {
28 | // Empty
29 | }
30 | Category(id: "custom-identifier", "Category Name Here") {
31 | // Also Empty
32 | }
33 | }
34 | }
35 | }
36 | app.launch()
37 | }
38 |
39 | func testCustomPresets() throws {
40 |
41 | // Use those custom presets!
42 |
43 | // Uncomment if you want to play with the categories on the simulator
44 | // Thread.sleep(until: .distantFuture)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Tests/VocableUITests/Tests/BaseTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseScreen.swift
3 | // VocableUITests
4 | //
5 | // Created by Kevin Stechler on 4/27/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 |
12 | let app = XCUIApplication()
13 |
14 | class BaseTest: XCTestCase {
15 |
16 | override func setUpWithError() throws {
17 |
18 | app.configure {
19 | Arguments(.resetAppDataOnLaunch, .enableListeningMode, .disableAnimations)
20 | }
21 | continueAfterFailure = false
22 | app.launch()
23 |
24 | addUIInterruptionMonitor(withDescription: "SpeechRecognition") { (alert) -> Bool in
25 | alert.buttons["OK"].tap()
26 | return true
27 | }
28 |
29 | // Ensure the main screen has loaded before continuing
30 | try MainScreen.outputText.assertExistence(timeout: 2.0, "Did not arrive on main screen")
31 | }
32 |
33 | override func tearDown() {
34 | super.tearDown()
35 | captureFailure(name: self.name)
36 | }
37 |
38 | func captureFailure(name: String) {
39 | let screenshot = XCUIScreen.main.screenshot()
40 | let attachment = XCTAttachment(screenshot: screenshot)
41 | attachment.name = name
42 | attachment.lifetime = .deleteOnSuccess
43 | add(attachment)
44 | }
45 |
46 | func randomString(length: Int) -> String {
47 | let letters = "abcdefghijklmnopqrstuvwxyz"
48 | return String((0..(_ keyPath: WritableKeyPath, _ value: T) -> some KeyboardLayoutElement {
13 | KeyboardLayoutEnvironmentModifier(content: self) { env in
14 | env[keyPath: keyPath] = value
15 | }
16 | }
17 |
18 | func mutatingEnvironment(_ t: @escaping (inout KeyboardLayoutEnvironment) -> Void) -> some KeyboardLayoutElement {
19 | KeyboardLayoutEnvironmentModifier(content: self, transform: t)
20 | }
21 | }
22 |
23 | private struct KeyboardLayoutEnvironmentModifier: KeyboardLayoutElement {
24 | private(set) var content: Content
25 | let transform: (inout KeyboardLayoutEnvironment) -> Void
26 |
27 | func makeKeys(environment: KeyboardLayoutEnvironment) -> [KeyboardLayoutKey] {
28 | var newEnvironment = environment
29 | transform(&newEnvironment)
30 | let merged = newEnvironment.merging(ancestor: environment)
31 | return content.makeKeys(environment: merged)
32 | }
33 |
34 | var children: [any KeyboardLayoutElement] {
35 | get {
36 | [content]
37 | }
38 | set {
39 | if let converted = newValue.first as? Content {
40 | content = converted
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/Layout/Components/KeyboardLayoutRelativeWidth/KeyboardLayoutRelativeWidthProportional.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardLayoutRelativeWidthProportional.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 7/31/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct KeyboardLayoutRelativeWidthProportional: KeyboardLayoutRelativeWidth {
12 |
13 | let multiplier: CGFloat
14 |
15 | init(multiplier: CGFloat) {
16 | self.multiplier = multiplier
17 | }
18 |
19 | func value(
20 | in containerWidth: CGFloat,
21 | environment: KeyboardLayoutEnvironment
22 | ) -> CGFloat {
23 | max(containerWidth * multiplier, .zero) * signMultiplier()
24 | }
25 |
26 | private var sign: FloatingPointSign = .plus
27 |
28 | private func signMultiplier() -> CGFloat {
29 | switch sign {
30 | case .plus: 1.0
31 | case .minus: -1.0
32 | }
33 | }
34 |
35 | static prefix func - (value: Self) -> Self {
36 | var value = value
37 | switch value.sign {
38 | case .plus:
39 | value.sign = .minus
40 | case .minus:
41 | value.sign = .plus
42 | }
43 | return value
44 | }
45 | }
46 |
47 | extension KeyboardLayoutRelativeWidth where Self == KeyboardLayoutRelativeWidthProportional {
48 |
49 | static var zero: Self {
50 | KeyboardLayoutRelativeWidthProportional(multiplier: .zero)
51 | }
52 |
53 | static func proportional(_ multiplier: CGFloat) -> Self {
54 | KeyboardLayoutRelativeWidthProportional(multiplier: multiplier)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Vocable/Common/AppStorage+Init.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppStorage+Init.swift
3 | // Vocable
4 | //
5 | // Created by Jesse Morgan on 4/21/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 |
12 | extension AppStorage {
13 |
14 | init(wrappedValue: Value, _ key: UserDefaultsKey, store: UserDefaults? = nil) where Value == String {
15 | self.init(wrappedValue: wrappedValue, key.value, store: store)
16 | }
17 |
18 | init(wrappedValue: Value, _ key: UserDefaultsKey, store: UserDefaults? = nil) where Value == Bool {
19 | self.init(wrappedValue: wrappedValue, key.value, store: store)
20 | }
21 |
22 | init(wrappedValue: Value, _ key: UserDefaultsKey, store: UserDefaults? = nil) where Value == Int {
23 | self.init(wrappedValue: wrappedValue, key.value, store: store)
24 | }
25 |
26 | init(wrappedValue: Value, _ key: UserDefaultsKey, store: UserDefaults? = nil) where Value == Double {
27 | self.init(wrappedValue: wrappedValue, key.value, store: store)
28 | }
29 |
30 | init(wrappedValue: Value, _ key: UserDefaultsKey, store: UserDefaults? = nil) where Value == URL {
31 | self.init(wrappedValue: wrappedValue, key.value, store: store)
32 | }
33 |
34 | init(wrappedValue: Value, _ key: UserDefaultsKey, store: UserDefaults? = nil) where Value == Data {
35 | self.init(wrappedValue: wrappedValue, key.value, store: store)
36 | }
37 |
38 | init(wrappedValue: Value, _ key: UserDefaultsKey, store: UserDefaults? = nil) where Value: RawRepresentable, Value.RawValue == Int {
39 | self.init(wrappedValue: wrappedValue, key.value, store: store)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Vocable/HeadTracking/Interpolation/PulseController/PIDControlConstants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PulseConstants.swift
3 | // Pulse
4 | //
5 | // Created by Dawid Cieslak on 15/04/2018.
6 | // Copyright © 2018 Dawid Cieslak. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Configuration for tuning
12 | enum PulseConstants: SingleControlDisplayable {
13 |
14 | case minimumValueStep
15 | case proportionalGain
16 | case integralGain
17 | case derivativeGain
18 |
19 | /// Name of control
20 | public var name: String {
21 | switch self {
22 | case .minimumValueStep:
23 | return "Minimum Value Step"
24 | case .proportionalGain:
25 | return "Proportional Gain"
26 | case .integralGain:
27 | return "Integral Gain"
28 | case .derivativeGain:
29 | return "Derivative Gain"
30 | }
31 | }
32 |
33 | /// Minimum value that can be set
34 | public var minimumValue: Float {
35 | switch self {
36 | case .minimumValueStep:
37 | return 0.003
38 | case .proportionalGain:
39 | return 1
40 | case .integralGain:
41 | return 0.1
42 | case .derivativeGain:
43 | return 0.1
44 | }
45 | }
46 |
47 | /// Maximum value that can be set
48 | public var maximumValue: Float {
49 | switch self {
50 | case .minimumValueStep:
51 | return 0.3
52 | case .proportionalGain:
53 | return 5
54 | case .integralGain:
55 | return 1
56 | case .derivativeGain:
57 | return 1
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/Layout/Components/KeyboardLayoutRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardLayoutRow.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 7/31/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct KeyboardLayoutRow: KeyboardLayoutElement {
12 |
13 | private(set) var index: Int = .zero
14 | var children: [any KeyboardLayoutElement]
15 |
16 | let debugID: String
17 |
18 | init(
19 | debugID: String = "",
20 | @KeyboardLayoutContent builder: () -> [any KeyboardLayoutElement]
21 | ) {
22 | self.debugID = debugID
23 | self.children = builder()
24 | }
25 |
26 | func makeKeys(environment: KeyboardLayoutEnvironment) -> [KeyboardLayoutKey] {
27 |
28 | var childEnvironment = environment
29 | childEnvironment.padding = .zero
30 |
31 | return children
32 | .indexed()
33 | .flatMap { (keyIndex, child) in
34 | child.mutatingEnvironment { env in
35 | env.rowIndex = index
36 | if keyIndex == children.indices.first {
37 | env.padding.leading += environment.padding.leading
38 | }
39 | if keyIndex == children.indices.last {
40 | env.padding.trailing += environment.padding.trailing
41 | }
42 | }
43 | .makeKeys(environment: childEnvironment)
44 | }
45 | }
46 |
47 | func withIndex(_ index: Int) -> KeyboardLayoutRow {
48 | var result = self
49 | result.index = index
50 | return result
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Vocable/Common/Views/GazeableAlertController/GazeableAlertPresentationController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GazeableAlertPresentationController.swift
3 | // Vocable ACC
4 | //
5 | // Created by Steve Foster on 3/23/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | final class GazeableAlertPresentationController: UIPresentationController {
12 |
13 | private lazy var dimmedBackgroundView: GazeEatingView = {
14 | let view = GazeEatingView()
15 | view.backgroundColor = .black
16 | view.alpha = 0
17 | return view
18 | }()
19 |
20 | override var frameOfPresentedViewInContainerView: CGRect {
21 | return CGRect(origin: .zero, size: containerView?.bounds.size ?? .zero)
22 | }
23 |
24 | override func dismissalTransitionWillBegin() {
25 | presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
26 | self.dimmedBackgroundView.alpha = 0
27 | }, completion: nil)
28 | }
29 |
30 | override func presentationTransitionWillBegin() {
31 | dimmedBackgroundView.alpha = 0
32 | containerView?.insertSubview(dimmedBackgroundView, at: 0)
33 |
34 | presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
35 | self.dimmedBackgroundView.alpha = 0.4
36 | }, completion: nil)
37 | }
38 |
39 | override func containerViewWillLayoutSubviews() {
40 | super.containerViewWillLayoutSubviews()
41 |
42 | guard let containerView = containerView else { return }
43 |
44 | presentedView?.frame = frameOfPresentedViewInContainerView
45 | dimmedBackgroundView.frame = containerView.bounds
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/Layout/Components/KeyboardLayoutKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardLayoutKey.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 7/31/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct KeyboardLayoutKey: Hashable, KeyboardLayoutElement {
12 |
13 | typealias Action = KeyboardKeyAction
14 |
15 | private(set) var environment: KeyboardLayoutEnvironment = .init()
16 |
17 | var children: [any KeyboardLayoutElement] {
18 | get {
19 | []
20 | }
21 | // swiftlint:disable:next unused_setter_value
22 | set {
23 | // no-op
24 | }
25 | }
26 |
27 | func makeKeys(environment: KeyboardLayoutEnvironment) -> [KeyboardLayoutKey] {
28 | var result = self
29 | result.environment = environment
30 | return [result]
31 | }
32 |
33 | private(set) var action: Action
34 |
35 | init(_ value: Character) {
36 | self.init(.insertCharacter(value))
37 | }
38 |
39 | init(_ action: Action) {
40 | self.action = action
41 | }
42 |
43 | func withModifier(modifier: Character?) -> KeyboardLayoutKey {
44 | guard let modifier else {
45 | return self
46 | }
47 | var result = self
48 | if case .insertCharacter(let character) = result.action {
49 | var unicodeScalars = character.unicodeScalars
50 | unicodeScalars.append(contentsOf: modifier.unicodeScalars)
51 | if let newValue = String(unicodeScalars).first {
52 | result.action = .insertCharacter(newValue)
53 | }
54 | }
55 | return result
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/UI/KeyboardKeyContainerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardKeyContainerView.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 7/31/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | extension KeyboardView {
13 |
14 | final class KeyContainerView: UIView {
15 | let button: KeyButton
16 |
17 | lazy private(set) var buttonWidthConstraint: NSLayoutConstraint = {
18 | button.widthAnchor.constraint(equalToConstant: 10.0).withPriority(999)
19 | }()
20 |
21 | required init?(coder: NSCoder) {
22 | fatalError("init(coder:) has not been implemented")
23 | }
24 |
25 | init(button: KeyButton) {
26 | self.button = button
27 | super.init(frame: .zero)
28 |
29 | button.translatesAutoresizingMaskIntoConstraints = false
30 | addSubview(button)
31 | NSLayoutConstraint.activate(
32 | [
33 | button.topAnchor.constraint(
34 | equalTo: self.topAnchor
35 | ),
36 | button.leadingAnchor.constraint(
37 | equalTo: self.layoutMarginsGuide.leadingAnchor
38 | ),
39 | button.bottomAnchor.constraint(
40 | equalTo: self.bottomAnchor
41 | ),
42 | button.trailingAnchor.constraint(
43 | equalTo: self.layoutMarginsGuide.trailingAnchor
44 | ),
45 | buttonWidthConstraint
46 | ]
47 | )
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Vocable/Common/AddPhraseCollectionViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddPhraseCollectionViewCell.swift
3 | // Vocable
4 | //
5 | // Created by Jesse Morgan on 4/8/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class AddPhraseCollectionViewCell: PresetItemCollectionViewCell {
12 |
13 | private let borderWidth = 6.0
14 | private let cornerRadius = 8.0
15 |
16 | override init(frame: CGRect) {
17 | super.init(frame: frame)
18 | commonInit()
19 | }
20 |
21 | required init?(coder: NSCoder) {
22 | super.init(coder: coder)
23 | commonInit()
24 | }
25 |
26 | private func commonInit() {
27 | contentView.preservesSuperviewLayoutMargins = true
28 |
29 | fillColor = .collectionViewBackgroundColor
30 |
31 | textLabel.numberOfLines = 1
32 | setup(title: String(localized: "preset.category.add.phrase.title"),
33 | with: UIImage(systemName: "plus"))
34 | }
35 |
36 | override func updateContent() {
37 | super.updateContent()
38 |
39 | if isHighlighted && !isSelected {
40 | borderedView.borderColor = .cellBorderHighlightColor
41 | } else if isSelected {
42 | borderedView.borderColor = .cellSelectionColor
43 | } else {
44 | borderedView.borderColor = .categoryBackgroundColor
45 | }
46 |
47 | borderedView.backgroundColor = .collectionViewBackgroundColor
48 | borderedView.borderWidth = isSelected ? 0 : borderWidth
49 | borderedView.isOpaque = true
50 | borderedView.cornerRadius = cornerRadius
51 | borderedView.borderDashPattern = [6, 6]
52 |
53 | layoutMargins = .init(uniform: cornerRadius + borderWidth)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Vocable/Features/Voice/ListeningFeedbackSuccessView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListeningFeedbackSuccessView.swift
3 | // Vocable
4 | //
5 | // Created by Jesse Morgan on 4/18/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | final class ListeningFeedbackSuccessView: UIView {
12 |
13 | // MARK: Properties
14 |
15 | private let label = UILabel()
16 |
17 | // MARK: Initializers
18 |
19 | init() {
20 | super.init(frame: .zero)
21 | commonInit()
22 | }
23 |
24 | required init?(coder: NSCoder) {
25 | super.init(coder: coder)
26 | commonInit()
27 | }
28 |
29 | private func commonInit() {
30 | let font: UIFont = sizeClass == .hRegular_vRegular
31 | ? .systemFont(ofSize: 34, weight: .bold)
32 | : .systemFont(ofSize: 22, weight: .bold)
33 | let symbol = UIImage(systemName: "checkmark.circle.fill")!.withTintColor(.cellSelectionColor)
34 |
35 | let text = String(localized: "listening_mode.feedback.confirmation.title")
36 | let attributes: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: UIColor.defaultTextColor]
37 | label.attributedText = NSAttributedString.imageAttachedString(for: text, with: symbol, attributes: attributes)
38 |
39 | addSubview(label)
40 | label.translatesAutoresizingMaskIntoConstraints = false
41 | NSLayoutConstraint.activate([label.centerXAnchor.constraint(equalTo: layoutMarginsGuide.centerXAnchor),
42 | label.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
43 | label.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor)])
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Vocable/HeadTracking/Interpolation/PulseController/Queue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Queue.swift
3 | // Pulse
4 | //
5 | // Created by Dawid Cieslak on 16/04/2018.
6 | // Copyright © 2018 Dawid Cieslak. All rights reserved.
7 | //
8 | import UIKit
9 |
10 | /// Provides FILO queue in fixed size
11 | ///
12 | /// This is "ring" type of queue - older values will be overwritten with new ones
13 | struct Queue {
14 |
15 | /// Current index of cell to store new value
16 | private var writeHeadIndex: Int = 0
17 |
18 | /// Stores all elements
19 | private var array: [T]
20 |
21 | /// Init with queue configuration
22 | ///
23 | /// - Parameters:
24 | /// - count: Maximum count of elements
25 | /// - initialValue: Initial value for all elements
26 | init(count: Int, initialValue: T) {
27 | array = [T](repeating: initialValue, count: count)
28 | }
29 |
30 | /// Adds given value to queue
31 | ///
32 | /// - Parameter value: Value to be added
33 | mutating func append(value: T) {
34 | array[writeHeadIndex] = value
35 |
36 | // Move pointer
37 | writeHeadIndex = (writeHeadIndex + 1) % array.count
38 | }
39 |
40 | /// Retrives of all elements in queue and returns in revered order (oldest element is firest)
41 | ///
42 | /// - Returns: Array of all elements in reversed order
43 | func allValuesReversed() -> [T] {
44 | var allValues: [T] = [T]()
45 |
46 | for i in 1...array.count {
47 | let readIndex = (writeHeadIndex - i + array.count) % array.count
48 | let currentValue = array[readIndex]
49 | allValues.append(currentValue)
50 | }
51 |
52 | return allValues
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/Tests/VocableUITests/Tests/CategoryIdentifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CategoryIdentifiers.swift
3 | // Vocable
4 | //
5 | // Created by Rudy Salas and Canan Arikan on 3/18/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct CategoryIdentifier {
12 |
13 | let identifier: String
14 | init(_ identifier: String) {
15 | self.identifier = identifier
16 | }
17 |
18 | static let mySayings = CategoryIdentifier("preset_user_favorites")
19 | static let recents = CategoryIdentifier("preset_user_recents")
20 | static let listen = CategoryIdentifier("preset_listening_mode")
21 | static let keyPad = CategoryIdentifier("preset_user_keypad")
22 | static let general = CategoryIdentifier("preset_C0B2A1A8-8333-4121-B4A8-FFFA185EB5D2")
23 | static let basicNeeds = CategoryIdentifier("preset_F8F5D1C9-0AA6-4152-BF7C-0851ACD1406B")
24 | static let personalCare = CategoryIdentifier("preset_E7ADBE88-2722-4DE7-BDC1-994F07EA294B")
25 | static let conversation = CategoryIdentifier("preset_EB7A9732-E28E-4440-A88B-BA2A1ACFBD76")
26 | static let environment = CategoryIdentifier("preset_52CA4E71-4A8C-4EA8-8EA8-C4B18AA16EC8")
27 |
28 | }
29 |
30 | struct PresetCategories {
31 |
32 | var list: [CategoryIdentifier]
33 |
34 | init() {
35 | self.list = [
36 | CategoryIdentifier.general,
37 | CategoryIdentifier.basicNeeds,
38 | CategoryIdentifier.personalCare,
39 | CategoryIdentifier.conversation,
40 | CategoryIdentifier.environment,
41 | CategoryIdentifier.keyPad,
42 | CategoryIdentifier.mySayings,
43 | CategoryIdentifier.recents,
44 | CategoryIdentifier.listen
45 | ]
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/Vocable/Extensions/UICollectionDiffableDatasource+Mutations.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UICollectionDiffableDatasource+Mutations.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 8/21/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | /// A handful of convenience mutation functions to apply changes
13 | /// to the current snapshot with or without animation
14 | extension UICollectionViewDiffableDataSource {
15 |
16 | func reloadItem(_ item: ItemIdentifierType, animated: Bool = true) {
17 | reloadItems([item], animated: animated)
18 | }
19 |
20 | func reloadItems(_ items: [ItemIdentifierType], animated: Bool = true) {
21 | var snapshot = snapshot()
22 | snapshot.reloadItems(items)
23 | apply(snapshot, animatingDifferences: animated)
24 | }
25 |
26 | func appendItem(
27 | _ item: ItemIdentifierType,
28 | in section: SectionIdentifierType,
29 | animated: Bool = true
30 | ) {
31 | appendItems([item], in: section, animated: animated)
32 | }
33 |
34 | func appendItems(
35 | _ items: [ItemIdentifierType],
36 | in section: SectionIdentifierType,
37 | animated: Bool = true
38 | ) {
39 | var snapshot = snapshot()
40 | snapshot.appendItems(items, toSection: section)
41 | apply(snapshot, animatingDifferences: animated)
42 | }
43 |
44 | func removeItem(_ item: ItemIdentifierType, animated: Bool = true) {
45 | removeItems([item], animated: animated)
46 | }
47 |
48 | func removeItems(_ items: [ItemIdentifierType], animated: Bool = true) {
49 | var snapshot = snapshot()
50 | snapshot.deleteItems(items)
51 | apply(snapshot, animatingDifferences: animated)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/Layout/Components/KeyboardLayoutDirectionalInsets.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardLayoutDirectionalInsets.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 7/31/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct KeyboardLayoutDirectionalInsets: Hashable {
12 |
13 | var leading: any KeyboardLayoutRelativeWidth
14 | var trailing: any KeyboardLayoutRelativeWidth
15 |
16 | static var zero: KeyboardLayoutDirectionalInsets {
17 | KeyboardLayoutDirectionalInsets(leading: .zero, trailing: .zero)
18 | }
19 |
20 | init(leading: some KeyboardLayoutRelativeWidth) {
21 | self.leading = AnyKeyboardLayoutRelativeWidth(leading)
22 | self.trailing = AnyKeyboardLayoutRelativeWidth(.zero)
23 | }
24 |
25 | init(trailing: some KeyboardLayoutRelativeWidth) {
26 | self.leading = AnyKeyboardLayoutRelativeWidth(.zero)
27 | self.trailing = AnyKeyboardLayoutRelativeWidth(trailing)
28 | }
29 |
30 | init(leading: some KeyboardLayoutRelativeWidth, trailing: some KeyboardLayoutRelativeWidth) {
31 | self.leading = AnyKeyboardLayoutRelativeWidth(leading)
32 | self.trailing = AnyKeyboardLayoutRelativeWidth(trailing)
33 | }
34 |
35 | func hash(into hasher: inout Hasher) {
36 | hasher.combine(AnyKeyboardLayoutRelativeWidth(leading))
37 | hasher.combine(AnyKeyboardLayoutRelativeWidth(trailing))
38 | }
39 |
40 | static func == (lhs: KeyboardLayoutDirectionalInsets, rhs: KeyboardLayoutDirectionalInsets) -> Bool {
41 | AnyKeyboardLayoutRelativeWidth(lhs.leading) == AnyKeyboardLayoutRelativeWidth(rhs.leading) &&
42 | AnyKeyboardLayoutRelativeWidth(lhs.trailing) == AnyKeyboardLayoutRelativeWidth(rhs.trailing)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Mac
6 | *.DS_Store
7 |
8 | ## Build generated
9 | build/
10 | DerivedData/
11 |
12 | ## Various settings
13 | *.pbxuser
14 | !default.pbxuser
15 | *.mode1v3
16 | !default.mode1v3
17 | *.mode2v3
18 | !default.mode2v3
19 | *.perspectivev3
20 | !default.perspectivev3
21 | xcuserdata/
22 |
23 | ## Other
24 | *.moved-aside
25 | *.xccheckout
26 | *.xcscmblueprint
27 |
28 | ## Obj-C/Swift specific
29 | *.hmap
30 | *.ipa
31 | *.dSYM.zip
32 | *.dSYM
33 |
34 | ## Playgrounds
35 | timeline.xctimeline
36 | playground.xcworkspace
37 |
38 | # Swift Package Manager
39 | #
40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
41 | # Packages/
42 | # Package.pins
43 | # Package.resolved
44 | .build/
45 |
46 | # CocoaPods
47 | #
48 | # We recommend against adding the Pods directory to your .gitignore. However
49 | # you should judge for yourself, the pros and cons are mentioned at:
50 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
51 | #
52 | # Pods/
53 |
54 | # Carthage
55 | #
56 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
57 | # Carthage/Checkouts
58 |
59 | Carthage/Checkouts
60 | !Carthage/Build
61 |
62 | # fastlane
63 | #
64 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
65 | # screenshots whenever they are needed.
66 | # For more information about the recommended setup visit:
67 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
68 |
69 | fastlane/report.xml
70 | fastlane/Preview.html
71 | fastlane/screenshots/**/*.png
72 | fastlane/test_output
73 |
--------------------------------------------------------------------------------
/Tests/VocableUITests/Screens/KeyboardScreen.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardScreen.swift
3 | // VocableUITests
4 | //
5 | // Created by Kevin Stechler on 4/24/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class KeyboardScreen: BaseScreen {
12 | private static let app = XCUIApplication()
13 |
14 | static let keyboardView = XCUIApplication().otherElements[.shared.keyboard.view]
15 | static let keyboardTextView = XCUIApplication().textViews[.shared.keyboard.outputTextView]
16 | static let favoriteButton = XCUIApplication().buttons[.shared.keyboard.favoriteButton]
17 | static let checkmarkAddButton = XCUIApplication().buttons[.shared.keyboard.saveButton]
18 | static let createDuplicateButton = XCUIApplication().buttons[.shared.alert.createDuplicateButton]
19 |
20 | static func typeText(
21 | _ textToType: String,
22 | file: StaticString = #file,
23 | line: UInt = #line
24 | ) throws {
25 | // Ensure the keyboard is visible on screen before tapping any cells
26 | try keyboardView.assertExistence(
27 | timeout: 0.5,
28 | "Failed to locate keyboard",
29 | file: file,
30 | line: line
31 | )
32 |
33 | // The entire keyboard is visible by design, so it is okay to
34 | // not wait for the existence of each cell before tapping. It's
35 | // a minor optimization, but the savings add up.
36 | for char in textToType.uppercased() {
37 | keyboardView.buttons[.shared.keyboard.key(.insertCharacter(char))].tap()
38 | }
39 | }
40 |
41 | static func randomString(length: Int) -> String {
42 | let letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
43 | return String((0.. [any KeyboardLayoutElement] {
15 | components
16 | }
17 |
18 | static func buildExpression(_ expression: any KeyboardLayoutElement) -> [any KeyboardLayoutElement] {
19 | [expression]
20 | }
21 |
22 | static func buildExpression(_ expression: [any KeyboardLayoutElement]) -> [any KeyboardLayoutElement] {
23 | expression
24 | }
25 |
26 | static func buildEither(first component: [any KeyboardLayoutElement]) -> [any KeyboardLayoutElement] {
27 | component
28 | }
29 |
30 | static func buildEither(second component: [any KeyboardLayoutElement]) -> [any KeyboardLayoutElement] {
31 | component
32 | }
33 |
34 | static func buildArray(_ components: [[any KeyboardLayoutElement]]) -> [any KeyboardLayoutElement] {
35 | Array(components.joined())
36 | }
37 |
38 | static func buildOptional(_ component: [any KeyboardLayoutElement]?) -> [any KeyboardLayoutElement] {
39 | component ?? []
40 | }
41 |
42 | static func buildFinalResult(_ component: [any KeyboardLayoutElement]) -> [any KeyboardLayoutElement] {
43 | component
44 | }
45 |
46 | static func buildPartialBlock(first: [any KeyboardLayoutElement]) -> [any KeyboardLayoutElement] {
47 | first
48 | }
49 |
50 | static func buildPartialBlock(
51 | accumulated: [any KeyboardLayoutElement],
52 | next: [any KeyboardLayoutElement]
53 | ) -> [any KeyboardLayoutElement] {
54 | accumulated + next
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Vocable/CoreData/Phrases.xcdatamodeld/Phrases.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/Vocable/HeadTracking/ToastWindow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NotificationWindow.swift
3 | // Vocable
4 | //
5 | // Created by Thomas Shealy on 3/25/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import ARKit
11 | import Combine
12 |
13 | class ToastWindow: UIWindow {
14 |
15 | private static var _shared: ToastWindow?
16 |
17 | static var shared: ToastWindow {
18 | if _shared == nil {
19 | let shared = ToastWindow(frame: UIScreen.main.bounds)
20 | shared.backgroundColor = .clear
21 | shared.translatesAutoresizingMaskIntoConstraints = false
22 | shared.rootViewController = ToastContainerViewController()
23 | _shared = shared
24 | }
25 | return _shared!
26 | }
27 |
28 | var toastContainerViewController: ToastContainerViewController {
29 | return self.rootViewController as! ToastContainerViewController
30 | }
31 |
32 | override init(frame: CGRect) {
33 | super.init(frame: frame)
34 | commonInit()
35 |
36 | }
37 |
38 | override init(windowScene: UIWindowScene) {
39 | super.init(windowScene: windowScene)
40 | commonInit()
41 | }
42 |
43 | required init?(coder: NSCoder) {
44 | super.init(coder: coder)
45 | commonInit()
46 | }
47 |
48 | private func commonInit() {
49 | self.isUserInteractionEnabled = false
50 | }
51 |
52 | func presentPersistentWarning(with title: String) {
53 | toastContainerViewController.handleWarning(with: title, shouldDisplay: true)
54 | }
55 |
56 | func dismissPersistentWarning() {
57 | toastContainerViewController.handleWarning(with: nil, shouldDisplay: false)
58 | }
59 |
60 | func presentEphemeralToast(withTitle: String) {
61 | toastContainerViewController.handlePhraseSaved(toastLabelText: withTitle)
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/Layout/Components/KeyboardLayoutRelativeWidth/KeyboardLayoutRelativeWidthCompound.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardLayoutRelativeWidthCompound.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 7/31/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct KeyboardLayoutRelativeWidthCompound: KeyboardLayoutRelativeWidth {
12 |
13 | private let lhs: AnyKeyboardLayoutRelativeWidth
14 | private let rhs: AnyKeyboardLayoutRelativeWidth
15 | private var sign: FloatingPointSign = .plus
16 |
17 | init(lhs: any KeyboardLayoutRelativeWidth, rhs: any KeyboardLayoutRelativeWidth) {
18 | self.lhs = AnyKeyboardLayoutRelativeWidth(lhs)
19 | self.rhs = AnyKeyboardLayoutRelativeWidth(rhs)
20 | }
21 |
22 | func value(in containerWidth: CGFloat, environment: KeyboardLayoutEnvironment) -> CGFloat {
23 | let lhs_value = lhs.value(in: containerWidth, environment: environment)
24 | let rhs_value = rhs.value(in: containerWidth, environment: environment)
25 | return (lhs_value + rhs_value) * signMultiplier()
26 | }
27 |
28 | private func signMultiplier() -> CGFloat {
29 | switch sign {
30 | case .plus: 1.0
31 | case .minus: -1.0
32 | }
33 | }
34 |
35 | static prefix func - (value: KeyboardLayoutRelativeWidthCompound) -> KeyboardLayoutRelativeWidthCompound {
36 | var value = value
37 | switch value.sign {
38 | case .plus:
39 | value.sign = .minus
40 | case .minus:
41 | value.sign = .plus
42 | }
43 | return value
44 | }
45 | }
46 |
47 | extension KeyboardLayoutRelativeWidth where Self == KeyboardLayoutRelativeWidthCompound {
48 | static func compound(
49 | _ lhs: any KeyboardLayoutRelativeWidth,
50 | _ rhs: any KeyboardLayoutRelativeWidth
51 | ) -> Self {
52 | KeyboardLayoutRelativeWidthCompound(
53 | lhs: lhs,
54 | rhs: rhs
55 | )
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Vocable/Extensions/UICollectionView+Helpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UICollectionView+Helpers.swift
3 | // Vocable
4 | //
5 | // Created by Jesse Morgan on 4/20/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | extension UICollectionView {
13 | func indexPath(containing view: UIView) -> IndexPath? {
14 | for cell in self.visibleCells where view.isDescendant(of: cell) {
15 | if let indexPath = self.indexPath(for: cell) {
16 | return indexPath
17 | }
18 | }
19 | return nil
20 | }
21 |
22 | func indexPath(nearestTo indexPath: IndexPath) -> IndexPath? {
23 | let validSections = 0.. IndexPath? {
35 | let validSections = 0..= itemsInSection {
41 | return nil
42 | } else {
43 | return IndexPath(row: candidateIndex, section: indexPath.section)
44 | }
45 | }
46 |
47 | func indexPath(before indexPath: IndexPath) -> IndexPath? {
48 | let validSections = 0.. Void)
22 |
23 | // Called when developer changes `PID` configuration
24 | var configurationChanged: ((TunningViewController, Pulse.Configuration) -> Void)
25 |
26 | override func loadView() {
27 | view = tunningView
28 | }
29 |
30 | init(pulse: Pulse, configuration: TunningView.Configuration, closeClosure: @escaping ((TunningViewController) -> Void), configurationChanged: @escaping ((TunningViewController, Pulse.Configuration) -> Void)) {
31 | self.closeClosure = closeClosure
32 | self.configurationChanged = configurationChanged
33 | self.pulse = pulse
34 |
35 | super.init(nibName: nil, bundle: nil)
36 |
37 | self.tunningView = TunningView(isHorizontal: pulse.isHorizontal, configuration: configuration, closeClosure: { [weak self] _ in
38 | guard let self = self else { return }
39 | self.closeClosure(self)
40 | }, configurationChanged: { [weak self] (_, configuration) in
41 | guard let self = self else { return }
42 | self.configurationChanged(self, configuration)
43 | })
44 | }
45 |
46 | required init?(coder aDecoder: NSCoder) {
47 | fatalError("init(coder:) has not been implemented")
48 | }
49 |
50 | func show() {
51 | tunningView?.layoutIfNeeded()
52 | tunningView?.visibilityState = .fullyVisible
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/.github/workflows/crowdin_pull.yml:
--------------------------------------------------------------------------------
1 | name: Pull Translations From Crowdin
2 | on:
3 | workflow_dispatch:
4 | schedule:
5 | - cron: "0 */3 * * *" # Run every 3 hours
6 |
7 | jobs:
8 | synchronize-with-crowdin:
9 | runs-on: macos-14
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v4
13 |
14 | - name: Install Crowdin CLI
15 | run: brew install crowdin
16 | shell: sh
17 |
18 | - name: Download Translations
19 | run: crowdin bundle download 6
20 | shell: sh
21 | env:
22 | GITHUB_TOKEN: ${{ secrets.CROWDIN_GITHUB_TOKEN }}
23 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
24 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
25 |
26 | - name: Set up Ruby environment
27 | uses: ruby/setup-ruby@v1
28 | with:
29 | ruby-version: 3.0
30 |
31 | - name: Install bundler
32 | run: gem install bundler
33 |
34 | - name: Install dependencies
35 | run: bundle install
36 |
37 | - name: Install SwiftLint
38 | run: brew install swiftlint
39 |
40 | - name: Import XLIFFs to project
41 | uses: ./.github/workflows/actions/run_lane
42 | with:
43 | lane: xliff_import
44 |
45 | - name: Create Pull Request
46 | uses: peter-evans/create-pull-request@v6
47 | with:
48 | token: ${{ secrets.CROWDIN_PR_BOT_TOKEN }}
49 | commit-message: Update Localizations
50 | committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
51 | author: ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>
52 | signoff: false
53 | branch: crowdin/update-translations-bot
54 | base: develop
55 | delete-branch: true
56 | add-paths: |
57 | Vocable/**/*.xcstrings
58 | Vocable/**/*.strings
59 | title: '[CI] Update Localizations'
60 | body: |
61 | Automated localization update from Crowdin.
62 | labels: |
63 | localization
64 | automated
65 | draft: false
--------------------------------------------------------------------------------
/Vocable/Supporting Files/AXIdentifier/AccessibilityID+Shared+Keyboard.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccessibilityID+Shared+Keyboard.swift
3 | // Vocable
4 | //
5 | // Created by Rudy Salas on 5/18/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension AccessibilityID.shared {
12 | public struct keyboard {
13 | public static let outputTextView: AccessibilityID = "keyboard-text-view"
14 | public static let favoriteButton: AccessibilityID = "favorite-button"
15 | public static let saveButton: AccessibilityID = "checkmark-save-button"
16 | public static let view: AccessibilityID = "keyboard-view"
17 |
18 | public static func key(_ action: KeyboardKeyAction) -> AccessibilityID {
19 | switch action {
20 | case .insertCharacter(let char):
21 | AccessibilityID(stringLiteral: "keyboard_function_insert_character_\(char)")
22 | case .clear:
23 | AccessibilityID(stringLiteral: "keyboard_function_clear")
24 | case .backspace:
25 | AccessibilityID(stringLiteral: "keyboard_function_backspace")
26 | case .space:
27 | AccessibilityID(stringLiteral: "keyboard_function_space")
28 | case .speak:
29 | AccessibilityID(stringLiteral: "keyboard_function_speak")
30 | case .numberPad:
31 | AccessibilityID(stringLiteral: "keyboard_function_navigate_numpad")
32 | case .alphabet:
33 | AccessibilityID(stringLiteral: "keyboard_function_navigate_alphabet")
34 | case .openModifierPicker:
35 | AccessibilityID(stringLiteral: "keyboard_function_navigate_open_modifiers")
36 | case .closeModifierPicker:
37 | AccessibilityID(stringLiteral: "keyboard_function_navigate_close_modifiers")
38 | case .beginModifier(let mod):
39 | AccessibilityID(stringLiteral: "keyboard_function_begin_modifier_\(mod)")
40 | case .endModifier(let mod):
41 | AccessibilityID(stringLiteral: "keyboard_function_end_modifier_\(mod)")
42 | }
43 | }
44 |
45 | private init() {}
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Vocable/AppConfig/AppConfig.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppConfig.swift
3 | // Vocable AAC
4 | //
5 | // Created by Patrick Gatewood on 2/7/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Combine
11 | import ARKit
12 | import AVFoundation
13 |
14 | extension UserDefaultsKey {
15 |
16 | static let listeningModeEnabledPreference: UserDefaultsKey = "listeningModeEnabledPreference"
17 | static let listeningModeSmartAssistEnabledPreference: UserDefaultsKey = "listeningModeSmartAssistEnabledPreference"
18 | static let listeningModeHotWordEnabledPreference: UserDefaultsKey = "listeningModeHotWordEnabledPreference"
19 | static let sensitivitySetting: UserDefaultsKey = "sensitivitySetting"
20 | static let dwellDuration: UserDefaultsKey = "dwellDuration"
21 | static let isHeadTrackingEnabled: UserDefaultsKey = "isHeadTrackingEnabled"
22 | static let isCompactQWERTYKeyboardEnabled: UserDefaultsKey = "isCompactQWERTYKeyboardEnabled"
23 | static let selectedVoiceIdentifier: UserDefaultsKey = "selectedVoiceIdentifier"
24 | }
25 |
26 | struct AppConfig {
27 |
28 | static let showDebugOptions: Bool = {
29 | #if DEBUG
30 | return true
31 | #else
32 | return false
33 | #endif
34 | }()
35 |
36 | @PublishedDefault(.isHeadTrackingEnabled)
37 | static var isHeadTrackingEnabled: Bool = AppConfig.isHeadTrackingSupported
38 | static var isHeadTrackingSupported: Bool {
39 | return ARFaceTrackingConfiguration.isSupported
40 | }
41 |
42 | @PublishedDefault(.dwellDuration)
43 | static var selectionHoldDuration: TimeInterval = 1
44 |
45 | @PublishedDefault(.sensitivitySetting)
46 | static var cursorSensitivity: CursorSensitivity = CursorSensitivity.medium
47 |
48 | static let defaultLanguageCode = "en"
49 | static var activePreferredLanguageCode: String {
50 | return Locale.preferredLanguages.first ?? defaultLanguageCode
51 | }
52 |
53 | static let listeningMode = ListenModeFeatureConfiguration.shared
54 |
55 | @PublishedDefault(.isCompactQWERTYKeyboardEnabled)
56 | static var isCompactQWERTYKeyboardEnabled: Bool = false
57 |
58 | @PublishedDefault(.selectedVoiceIdentifier)
59 | static var selectedVoiceIdentifier: String? = .none
60 | }
61 |
--------------------------------------------------------------------------------
/Vocable/Features/Voice/ListenModeDebugStorage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListenModeDebugStorage.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 3/19/21.
6 | // Copyright © 2021 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import VocableListenCore
11 |
12 | final class ListenModeDebugStorage: ObservableObject {
13 |
14 | static let shared = ListenModeDebugStorage()
15 |
16 | private init() {
17 |
18 | }
19 |
20 | private let maxHistoryCount = 50
21 | private static let defaultsKey = "loggingContextHistory"
22 | private var defaultsKey: String {
23 | return ListenModeDebugStorage.defaultsKey
24 | }
25 |
26 | private static func orderedContexts(_ contexts: [VLLoggingContext]) -> [VLLoggingContext] {
27 | let sorted = contexts.sorted { (lhs, rhs) -> Bool in
28 | guard let lhsStartDate = lhs.startDate, let rhsStartDate = rhs.startDate else {
29 | return false
30 | }
31 | return lhsStartDate > rhsStartDate
32 | }
33 | return sorted
34 | }
35 |
36 | private static func retrieveContexts() -> [VLLoggingContext] {
37 | guard let data = UserDefaults.standard.data(forKey: defaultsKey) else {
38 | return []
39 | }
40 | guard let result = try? JSONDecoder().decode([VLLoggingContext].self, from: data) else {
41 | return []
42 | }
43 |
44 | return orderedContexts(result)
45 | }
46 |
47 | func append(_ context: VLLoggingContext) {
48 | contexts.insert(context, at: 0)
49 | }
50 |
51 | func clear() {
52 | contexts = []
53 | }
54 |
55 | func delete(at offsets: IndexSet) {
56 | contexts.remove(atOffsets: offsets)
57 | }
58 |
59 | @Published private(set) var contexts: [VLLoggingContext] = ListenModeDebugStorage.retrieveContexts() {
60 | didSet {
61 | let sorted = ListenModeDebugStorage.orderedContexts(contexts)
62 | let truncated = Array(sorted.suffix(maxHistoryCount))
63 | guard let data = try? JSONEncoder().encode(truncated) else {
64 | assertionFailure("Failed to encode data")
65 | return
66 | }
67 | UserDefaults.standard.setValue(data, forKey: defaultsKey)
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Vocable/CoreData/Phrases.xcdatamodeld/Phrases v3.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Vocable/CoreData/Phrases.xcdatamodeld/Phrases v2.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Vocable/Extensions/TextExpression.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextExpression.swift
3 | // Vocable AAC
4 | //
5 | // Created by Kyle Ohanian on 4/16/19.
6 | // Copyright © 2019 WillowTree. All rights reserved.
7 | //
8 | import Foundation
9 |
10 | class TextExpression {
11 | private(set) var value: String
12 |
13 | private let textSuggestionController = TextSuggestionController()
14 |
15 | init(value: String = "") {
16 | self.value = value
17 | }
18 |
19 | var wordCount: Int {
20 | return value.split(separator: " ").count
21 | }
22 |
23 | var splitExpression: [String] {
24 | let trimmedExpression = self.value.trimmingCharacters(in: .whitespacesAndNewlines)
25 | return trimmedExpression.split(separator: " ").map { String($0) }
26 | }
27 |
28 | func add(word: String) {
29 | let trimmedExpression = self.value.trimmingCharacters(in: .whitespacesAndNewlines)
30 | var newSplitExpression = trimmedExpression.split(separator: " ").map { String($0) }
31 | newSplitExpression.append(word)
32 | self.value = newSplitExpression.joined(separator: " ")
33 | }
34 |
35 | func replaceWord(at index: Int, with newWord: String) {
36 | var newSplitExpression = self.splitExpression
37 | if newSplitExpression.count > index && index >= 0 {
38 | newSplitExpression[index] = newWord
39 | }
40 | self.value = newSplitExpression.joined(separator: " ")
41 | }
42 |
43 | func replaceLastWord(with newWord: String) {
44 | self.replaceWord(at: self.splitExpression.count - 1, with: newWord)
45 | }
46 |
47 | func word(at index: Int) -> String? {
48 | return self.splitExpression[safe: index]
49 | }
50 |
51 | func lastWord() -> String? {
52 | return word(at: self.splitExpression.count - 1)
53 | }
54 |
55 | func backspace() {
56 | if !self.value.isEmpty {
57 | _ = self.value.removeLast()
58 | }
59 | }
60 |
61 | func append(text: String) {
62 | value.append(text)
63 | }
64 |
65 | func replace(text: String) {
66 | value = text
67 | }
68 |
69 | func clear() {
70 | value = ""
71 | }
72 |
73 | func space() {
74 | self.value.append(" ")
75 | }
76 |
77 | func suggestions() -> [String] {
78 | return textSuggestionController.suggestions(for: self)
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Vocable/Features/Settings/VoiceSettings/PersonalVoicePermissionPromptController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PersonalVoicePermissionPromptController.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 4/26/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AVFoundation
11 | import Combine
12 | import UIKit
13 |
14 | @available(iOS 17.0, *)
15 | final class PersonalVoicePermissionPromptController {
16 |
17 | private typealias AuthorizationStatus = AVSpeechSynthesizer.PersonalVoiceAuthorizationStatus
18 |
19 | struct PersonalVoicePermissionEmptyState {
20 | let state: PersonalVoiceEmptyState
21 | let action: EmptyStateView.ButtonConfiguration
22 | }
23 |
24 | @Published private(set) var state: PersonalVoicePermissionEmptyState? = .none
25 |
26 | private var cancellables = Set()
27 |
28 | private var authorizationStatus: AuthorizationStatus {
29 | AVSpeechSynthesizer.personalVoiceAuthorizationStatus
30 | }
31 |
32 | init() {
33 | self.authorizationStatusDidChange(authorizationStatus)
34 | NotificationCenter.default
35 | .publisher(
36 | for: AVSpeechSynthesizer.availableVoicesDidChangeNotification,
37 | object: nil
38 | )
39 | .compactMap { [weak self] _ in
40 | self?.authorizationStatus
41 | }
42 | .receive(on: DispatchQueue.main)
43 | .sink { [weak self] (status) in
44 | self?.authorizationStatusDidChange(status)
45 | }
46 | .store(in: &cancellables)
47 | }
48 |
49 | private func authorizationStatusDidChange(_ status: AuthorizationStatus) {
50 | switch status {
51 | case .authorized:
52 | self.state = nil
53 | case .denied: // Need to go to settings
54 | self.state = .init(state: .denied) {
55 | UIApplication.openSettingsURL()
56 | }
57 | case .notDetermined: // Need to present alert
58 | self.state = .init(state: .notAuthorized) { [weak self] in
59 | AVSpeechSynthesizer.requestPersonalVoiceAuthorization { status in
60 | self?.authorizationStatusDidChange(status)
61 | }
62 | }
63 | default:
64 | self.state = nil
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Vocable/SwiftUI/UIKit Bridge/ContentHuggingPriority.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentHuggingPriority.swift
3 | // Vocable
4 | //
5 | // Created by Robert Moyer on 4/6/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import UIKit
11 |
12 | extension EnvironmentValues {
13 | private struct HorizontalContentHuggingPriorityKey: EnvironmentKey {
14 | static var defaultValue: UILayoutPriority = .defaultHigh
15 | }
16 |
17 | private struct VerticalContentHuggingPriorityKey: EnvironmentKey {
18 | static var defaultValue: UILayoutPriority = .defaultHigh
19 | }
20 |
21 | /// The priority with which a view associated with this
22 | /// environment resists being made larger than its intrinsic
23 | /// content size in the horizontal direction.
24 | ///
25 | /// The default value is `.defaultHigh`.
26 | var horizontalContentHuggingPriority: UILayoutPriority {
27 | get { self[HorizontalContentHuggingPriorityKey.self] }
28 | set { self[HorizontalContentHuggingPriorityKey.self] = newValue }
29 | }
30 |
31 | /// The priority with which a view associated with this
32 | /// environment resists being made larger than its intrinsic
33 | /// content size in the vertical direction.
34 | ///
35 | /// The default value is `.defaultHigh`.
36 | var verticalContentHuggingPriority: UILayoutPriority {
37 | get { self[VerticalContentHuggingPriorityKey.self] }
38 | set { self[VerticalContentHuggingPriorityKey.self] = newValue }
39 | }
40 | }
41 |
42 | extension View {
43 | /// Sets the content hugging priority for this view on the specified axis.
44 | ///
45 | /// - Parameters:
46 | /// - priority: The new priority.
47 | /// - axis: The axis for which the content hugging priority should be set.
48 | /// - Returns: A view that resists being made larger than its intrinsic content size, with the given priority level.
49 | @ViewBuilder func contentHuggingPriority(
50 | _ priority: UILayoutPriority,
51 | axis: NSLayoutConstraint.Axis
52 | ) -> some View {
53 | switch axis {
54 | case .vertical:
55 | environment(\.verticalContentHuggingPriority, priority)
56 | case .horizontal:
57 | environment(\.horizontalContentHuggingPriority, priority)
58 | @unknown default:
59 | self
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/Layout/Components/KeyboardLayoutBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardLayoutBuilders.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 5/24/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | @resultBuilder
12 | struct KeyboardBuilder {
13 |
14 | typealias Result = KeyboardBody
15 | typealias Row = (any KeyboardLayoutElement)
16 |
17 | struct Partial {
18 | var element: any KeyboardLayoutElement
19 | var lastRowIndex: Int = .zero
20 |
21 | init(element: any KeyboardLayoutElement, baseRowIndex: Int = .zero) {
22 | self.lastRowIndex = baseRowIndex
23 | self.element = element
24 | }
25 |
26 | init(elements: [any KeyboardLayoutElement], baseRowIndex: Int = .zero) {
27 | self.init(
28 | element: KeyboardLayoutGroup(children: elements),
29 | baseRowIndex: baseRowIndex
30 | )
31 | }
32 | }
33 |
34 | // MARK: Keys
35 |
36 | static func buildBlock(_ components: Row...) -> Partial {
37 | Partial(elements: components)
38 | }
39 |
40 | static func buildExpression(_ expression: Row) -> Partial {
41 | Partial(element: expression)
42 | }
43 |
44 | static func buildExpression(_ expression: Partial) -> Partial {
45 | expression
46 | }
47 |
48 | static func buildEither(first component: Partial) -> Partial {
49 | component
50 | }
51 |
52 | static func buildEither(second component: Partial) -> Partial {
53 | component
54 | }
55 |
56 | static func buildArray(_ components: [any KeyboardLayoutElement]) -> Partial {
57 | Partial(elements: components)
58 | }
59 |
60 | static func buildArray(_ components: [Partial]) -> Partial {
61 | Partial(elements: components.map(\.element))
62 | }
63 |
64 | static func buildOptional(_ component: Row?) -> Partial {
65 | Partial(elements: Array([component].compacted()))
66 | }
67 |
68 | static func buildPartialBlock(first: Partial) -> Partial {
69 | first
70 | }
71 |
72 | static func buildPartialBlock(accumulated: Partial, next: Partial) -> Partial {
73 | Partial(elements: [accumulated.element, next.element])
74 | }
75 |
76 | static func buildFinalResult(_ component: Partial) -> Result {
77 | return Result(root: component.element)
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Vocable/SwiftUI/Gaze Button/GazeButtonStyleConfiguration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GazeButtonStyleConfiguration.swift
3 | // Vocable
4 | //
5 | // Created by Robert Moyer on 4/7/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// The properties of a ``GazeButton`` instance.
12 | ///
13 | /// When you define a custom button style by creating a type that conforms to
14 | /// the ``GazeButtonStyle`` protocol, you implement the
15 | /// ``GazeButtonStyle/makeBody(_:)`` method. That method takes a
16 | /// `GazeButtonStyleConfiguration` input that has the information you need
17 | /// to define the behavior and appearance of a ``GazeButton``.
18 | ///
19 | /// The configuration structure's ``label-swift.property`` reflects the
20 | /// button's content, which might be the value that you supply to the
21 | /// `label` parameter of the ``GazeButton/init(minimumGazeDuration:role:action:label:)`` initializer.
22 | /// Alternatively, it could be another view that SwiftUI builds from an
23 | /// initializer that takes a string input, like ``GazeButton/init(_:minimumGazeDuration:role:action:)-8xknk``.
24 | /// In either case, incorporate the label into the button's view to help
25 | /// the user understand that the view is interactive.
26 | ///
27 | /// The structure's ``state`` property provides a `Binding` to the state
28 | /// of the button. Adjust the appearance of the button based on this value.
29 | /// For example, the built-in ``GazeButtonStyle/vocable`` style adds a thick
30 | /// border and shrinks the size when the vaule ``ButtonState/highlighted``.
31 | struct GazeButtonStyleConfiguration {
32 |
33 | /// A type-erased label of a ``GazeButton``.
34 | struct Label: View {
35 | private var view: AnyView
36 | var body: some View { view }
37 |
38 | fileprivate init(_ view: V) {
39 | self.view = AnyView(view)
40 | }
41 | }
42 |
43 | /// A binding to a state property that indicates whether the
44 | /// button is highlighted or pressed.
45 | let state: Binding
46 |
47 | /// A view that describes the effect of pressing the button.
48 | let label: Label
49 |
50 | /// An optional semantic role that describes the button’s purpose.
51 | let role: ButtonRole?
52 |
53 | init(label: V, state: Binding, role: ButtonRole?) {
54 | self.label = Label(label)
55 | self.state = state
56 | self.role = role
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Vocable/Features/Settings/VoiceSettings/VocableListContentConfiguration+VoiceProfileItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VoiceProfileCell.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 4/24/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AVFoundation
11 | import UIKit
12 |
13 | extension VocableListContentConfiguration {
14 |
15 | @MainActor
16 | static func voiceProfileItem(
17 | _ item: VoiceProfileItem,
18 | controller: VoiceProfilePreviewController,
19 | voiceSelectedAction: (() -> Void)? = nil
20 | ) -> Self {
21 |
22 | let sampleAction: VocableListCellAction
23 | if item.isPlaying {
24 | sampleAction = VocableListCellAction.stopAudio(
25 | accessibilityIdentifier: .settings.voiceSettings.audioPlaying
26 | ) {
27 | controller.stopPreview()
28 | }
29 | } else {
30 | sampleAction = VocableListCellAction.startAudio(
31 | accessibilityIdentifier: .settings.voiceSettings.playButton
32 | ) {
33 | controller.playPreview(item)
34 | }
35 | }
36 |
37 | return VocableListContentConfiguration(
38 | title: item.voice.name,
39 | actions: [sampleAction],
40 | trailingAccessory: (voiceSelectedAction != nil && item.isSelected) ? .checkmark : nil,
41 | isPrimaryActionEnabled: voiceSelectedAction != nil,
42 | accessibilityIdentifier: .settings.voiceSettings.previewVoiceCell,
43 | primaryAction: voiceSelectedAction ?? {}
44 | )
45 | }
46 |
47 | @MainActor
48 | static func voiceSelectionPreview(
49 | _ item: VoiceProfileItem,
50 | controller: VoiceProfilePreviewController
51 | ) -> Self {
52 |
53 | let accessory: VocableListCellAccessory
54 | if item.isPlaying {
55 | accessory = .stopAudio
56 | } else {
57 | accessory = .playAudio
58 | }
59 |
60 | return VocableListContentConfiguration(
61 | title: item.voice.name,
62 | leadingAccessory: accessory,
63 | accessibilityIdentifier: AccessibilityID.settings.voiceSettings.previewVoiceCell
64 | ) {
65 | if item.isPlaying {
66 | controller.stopPreview()
67 | } else {
68 | controller.playPreview(item)
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Vocable/Supporting Files/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | English
7 | CFBundleDisplayName
8 | Localized in Supporting Files > InfoPlist.strings
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | ITSAppUsesNonExemptEncryption
24 |
25 | LSRequiresIPhoneOS
26 |
27 | NSCameraUsageDescription
28 | Localized in Supporting Files > InfoPlist.strings
29 | NSMicrophoneUsageDescription
30 | Localized in Supporting Files > InfoPlist.strings
31 | NSSpeechRecognitionUsageDescription
32 | Localized in Supporting Files > InfoPlist.strings
33 | UIApplicationSceneManifest
34 |
35 | UIApplicationSupportsMultipleScenes
36 |
37 |
38 | UILaunchStoryboardName
39 | LaunchScreen
40 | UIMainStoryboardFile
41 | Root
42 | UIRequiredDeviceCapabilities
43 |
44 | armv7
45 |
46 | UIRequiresFullScreen
47 |
48 | UIStatusBarHidden
49 |
50 | UISupportedInterfaceOrientations
51 |
52 | UIInterfaceOrientationPortrait
53 | UIInterfaceOrientationLandscapeLeft
54 | UIInterfaceOrientationLandscapeRight
55 |
56 | UISupportedInterfaceOrientations~ipad
57 |
58 | UIInterfaceOrientationPortrait
59 | UIInterfaceOrientationPortraitUpsideDown
60 | UIInterfaceOrientationLandscapeLeft
61 | UIInterfaceOrientationLandscapeRight
62 |
63 | UIViewControllerBasedStatusBarAppearance
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/Vocable/Features/Settings/VoiceSettings/VoiceProfilePreviewDataSource+Filter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VoiceProfilePreviewDataSource+Filter.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 4/25/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AVFoundation
11 |
12 | extension VoiceProfilePreviewDataSource {
13 |
14 | // This is a cheap stand-in until the deployment target can
15 | // support iOS 17's Foundation Predicates. Goal is to allow
16 | // injecting a filter into the datasource externally so it can
17 | // be configured per-presentation context.
18 | struct Filter {
19 |
20 | private let isIncluded: ((voice: AVSpeechSynthesisVoice, isSelected: Bool)) -> Bool
21 |
22 | func shouldInclude(_ voice: AVSpeechSynthesisVoice, isSelected: Bool) -> Bool {
23 | isIncluded((voice: voice, isSelected: isSelected))
24 | }
25 | }
26 | }
27 |
28 | extension VoiceProfilePreviewDataSource.Filter {
29 |
30 | /// Only allow the selected voice to remain in the collection
31 | static var selectedVoice: Self {
32 | Self { (_: AVSpeechSynthesisVoice, isSelected: Bool) in
33 | isSelected
34 | }
35 | }
36 |
37 | /// Only allow the standard selection of system voices to remain in the collection,
38 | /// excluding novelty voices. For pre-iOS 17.0 devices, this will filter any non-binary
39 | /// gendered voices because that appears to be the best (only?) strategy we have for
40 | /// filtering out novelty voices due to the lack of relevant metadata.
41 | static var systemVoices: Self {
42 | Self { (voice: AVSpeechSynthesisVoice, _: Bool) in
43 | if #available(iOS 17.0, *) {
44 | return !voice.voiceTraits.contains(.isNoveltyVoice)
45 | } else {
46 | // Best workaround we have, given the state of pre-iOS 17 APIs
47 | // Not trying to force a gender binary, merely trying to exclude
48 | // all the novelty voices without a proper API to do so.
49 | return [.male, .female].contains(voice.gender)
50 | }
51 | }
52 | }
53 |
54 | static var personalVoices: Self {
55 | Self { (voice: AVSpeechSynthesisVoice, _: Bool) in
56 | if #available(iOS 17.0, *) {
57 | return voice.voiceTraits.contains(.isPersonalVoice)
58 | } else {
59 | return false
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Tests/VocableUITests/Tests/KeyboardScreenTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardScreenTests.swift
3 | // KeyboardScreenTests
4 | //
5 | // Created by Kevin Stechler on 4/22/20.
6 | // Updated by Rudy Salas and Canan Arikan on 03/17/2022
7 | // Copyright © 2022 WillowTree. All rights reserved.
8 | //
9 |
10 | import XCTest
11 |
12 | class KeyboardScreenTests: BaseTest {
13 |
14 | func testKeyboardOutputIsDisplayed() throws {
15 | let testPhrase = "Test"
16 |
17 | try MainScreen.keyboardButton.tapWhenExists()
18 | try KeyboardScreen.typeText(testPhrase)
19 |
20 | try KeyboardScreen.keyboardTextView.staticTexts[testPhrase]
21 | .assertExistence("Expected the text \(testPhrase) to be displayed")
22 | }
23 |
24 | func testAddPhraseToMySayingsFromKeyboard() throws {
25 | let testPhrase = "Test"
26 |
27 | try MainScreen.keyboardButton.tapWhenExists()
28 | try KeyboardScreen.typeText(testPhrase)
29 | try KeyboardScreen.favoriteButton.tapWhenExists()
30 | try KeyboardScreen.navBarDismissButton.tapWhenExists()
31 |
32 | MainScreen.locateAndSelectDestinationCategory(.mySayings)
33 |
34 | try MainScreen.locatePhraseCell(phrase: testPhrase)
35 | .assertExistence("Expected the phrase \(testPhrase) to be added to and displayed in 'My Sayings'")
36 | }
37 |
38 | func testRemovePhraseFromMySayingsFromKeyboard() throws {
39 | let testPhrase = "Test"
40 |
41 | try MainScreen.keyboardButton.tapWhenExists()
42 | try KeyboardScreen.typeText(testPhrase)
43 | try KeyboardScreen.favoriteButton.tapWhenExists()
44 | try KeyboardScreen.navBarDismissButton.tapWhenExists()
45 |
46 | MainScreen.locateAndSelectDestinationCategory(.mySayings)
47 |
48 | try MainScreen.locatePhraseCell(phrase: testPhrase).assertExistence("Expected the phrase \(testPhrase) to be added to and displayed in 'My Sayings'")
49 |
50 | try MainScreen.keyboardButton.tapWhenExists()
51 | try KeyboardScreen.typeText(testPhrase)
52 | try KeyboardScreen.favoriteButton.tapWhenExists()
53 | try KeyboardScreen.navBarDismissButton.tapWhenExists()
54 | MainScreen.locateAndSelectDestinationCategory(.mySayings)
55 |
56 | // We expect 'My Sayings' to be empty now.
57 | try MainScreen.emptyStateAddPhraseButton.assertExistence("Expected the phrase \(testPhrase) to be deleted from 'My Sayings'")
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/Tests/VocableUITests/Tests/TimingAndSensitivityTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimingAndSensitivityTests.swift
3 | // VocableUITests
4 | //
5 | // Created by Alex Facer on 7/1/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XCTest
11 |
12 | class TimingAndSensitivityTests: BaseTest {
13 |
14 | func testSwitchCursorSensitivity() {
15 | //navigate to timing and sensitivity screen and check medium selected by default
16 | MainScreen.settingsButton.tap()
17 | SettingsScreen.timingAndSensitivityCell.tap()
18 | XCTAssertTrue(TimingAndSensitivityScreen.mediumButton.isSelected)
19 |
20 | //Tap low button and check medium is not selected
21 | TimingAndSensitivityScreen.lowButton.tap()
22 | XCTAssertTrue(TimingAndSensitivityScreen.lowButton.isSelected)
23 | XCTAssertFalse(TimingAndSensitivityScreen.mediumButton.isSelected)
24 |
25 | //Tap high button and check low is not selected
26 | TimingAndSensitivityScreen.highButton.tap()
27 | XCTAssertTrue(TimingAndSensitivityScreen.highButton.isSelected)
28 | XCTAssertFalse(TimingAndSensitivityScreen.lowButton.isSelected)
29 | }
30 |
31 | func testHoverTimeButtons() {
32 | //navigate to timing and sensitivity screen and check hover time is 1.0
33 | MainScreen.settingsButton.tap()
34 | SettingsScreen.timingAndSensitivityCell.tap()
35 | XCTAssertEqual(TimingAndSensitivityScreen.hoverTimeLabel.label, "1.0s")
36 | XCTAssertTrue(TimingAndSensitivityScreen.increaseHoverTimeButton.isEnabled)
37 | XCTAssertTrue(TimingAndSensitivityScreen.reduceHoverTimeButton.isEnabled)
38 |
39 | //decrease hover time and check minus icon is disabled
40 | TimingAndSensitivityScreen.reduceHoverTimeButton.tap()
41 | XCTAssertFalse(TimingAndSensitivityScreen.reduceHoverTimeButton.isEnabled)
42 |
43 | //increase hover time and ensure decrease button becomes enabled
44 | TimingAndSensitivityScreen.increaseHoverTimeButton.tap()
45 | XCTAssertTrue(TimingAndSensitivityScreen.reduceHoverTimeButton.isEnabled)
46 |
47 | //increase hover time to max 5.0s and check that increase button disabled
48 | while TimingAndSensitivityScreen.hoverTimeLabel.label != "5.0s" {
49 | TimingAndSensitivityScreen.increaseHoverTimeButton.tap()
50 | }
51 | XCTAssertFalse(TimingAndSensitivityScreen.increaseHoverTimeButton.isEnabled)
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/Vocable/Common/VocableListCell/VocableListCellAccessory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccessoryAction.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 3/21/22.
6 | // Copyright © 2022 WillowTree. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | struct VocableListCellAccessory: Equatable {
12 |
13 | enum Content: Equatable {
14 | case toggle(isOn: Bool)
15 | case image(UIImage)
16 |
17 | static func == (lhs: Content, rhs: Content) -> Bool {
18 | switch (lhs, rhs) {
19 | case (.toggle(let isOnLeft), .toggle(let isOnRight)):
20 | return isOnLeft == isOnRight
21 | case (.image(let leftImage), .image(let rightImage)):
22 | return leftImage.isEqual(rightImage)
23 | default:
24 | return false
25 | }
26 | }
27 | }
28 |
29 | let content: Content
30 | let isEnabled: Bool
31 |
32 | private static var trailingDefaultSymbolConfiguration: UIImage.SymbolConfiguration {
33 | UIImage.SymbolConfiguration(pointSize: 28, weight: .bold)
34 | }
35 |
36 | static func disclosureIndicator(isEnabled: Bool = true) -> VocableListCellAccessory {
37 | let symbolName: String
38 | if UITraitCollection.current.layoutDirection == .leftToRight {
39 | symbolName = "chevron.right"
40 | } else {
41 | symbolName = "chevron.left"
42 | }
43 | return .systemImage(symbolName, isEnabled: isEnabled)
44 | }
45 |
46 | static func systemImage(_ name: String, isEnabled: Bool = true) -> VocableListCellAccessory {
47 | let image = UIImage(systemName: name, withConfiguration: trailingDefaultSymbolConfiguration)!
48 | return VocableListCellAccessory(content: .image(image), isEnabled: isEnabled)
49 | }
50 |
51 | static func toggle(isOn: Bool, isEnabled: Bool = true) -> VocableListCellAccessory {
52 | return VocableListCellAccessory(content: .toggle(isOn: isOn), isEnabled: isEnabled)
53 | }
54 |
55 | static var checkmark: VocableListCellAccessory {
56 | .checkmark(isEnabled: true)
57 | }
58 |
59 | static func checkmark(isEnabled: Bool) -> VocableListCellAccessory {
60 | .systemImage("checkmark", isEnabled: true)
61 | }
62 |
63 | static var playAudio: VocableListCellAccessory {
64 | .systemImage("play.circle", isEnabled: true)
65 | }
66 |
67 | static var stopAudio: VocableListCellAccessory {
68 | .systemImage("stop.circle", isEnabled: true)
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Vocable/CoreData/NSManagedObject+Helpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSManagedObject+Helpers.swift
3 | // Vocable AAC
4 | //
5 | // Created by Chris Stroud on 2/21/20.
6 | // Copyright © 2020 WillowTree. All rights reserved.
7 | //
8 |
9 | import CoreData
10 |
11 | protocol NSManagedObjectIdentifiable {
12 | associatedtype IdentifierType
13 | }
14 |
15 | extension NSManagedObjectIdentifiable where Self: NSManagedObject {
16 |
17 | static func fetchObject(in context: NSManagedObjectContext, matching identifier: IdentifierType) -> Self? {
18 | guard let entityName = self.entity().name else {
19 | return nil
20 | }
21 |
22 | let fetchRequest = NSFetchRequest(entityName: entityName)
23 | if let identifier = identifier as? String {
24 | fetchRequest.predicate = NSPredicate(format: "identifier == %@", identifier)
25 | } else {
26 | fetchRequest.predicate = NSPredicate(format: "identifier == \(identifier)")
27 | }
28 | fetchRequest.fetchLimit = 1
29 |
30 | return (try? context.fetch(fetchRequest))?.first
31 | }
32 |
33 | static func fetchObject(in context: NSManagedObjectContext, matching identifier: NSManagedObjectID) -> Self? {
34 | guard let entityName = self.entity().name else {
35 | return nil
36 | }
37 |
38 | let fetchRequest = NSFetchRequest(entityName: entityName)
39 | fetchRequest.predicate = NSPredicate(format: "(SELF = %@)", identifier)
40 | fetchRequest.fetchLimit = 1
41 |
42 | return (try? context.fetch(fetchRequest))?.first
43 | }
44 |
45 | static func fetchAll(in context: NSManagedObjectContext, matching predicate: NSPredicate? = nil, sortDescriptors: [NSSortDescriptor]? = nil) -> [Self] {
46 | guard let entityName = self.entity().name else {
47 | return []
48 | }
49 |
50 | let fetchRequest = NSFetchRequest(entityName: entityName)
51 | fetchRequest.sortDescriptors = sortDescriptors
52 | fetchRequest.predicate = predicate
53 | return (try? context.fetch(fetchRequest)) ?? []
54 | }
55 |
56 | static func fetchOrCreate(in context: NSManagedObjectContext, matching identifier: IdentifierType) -> Self {
57 | if let existingObject = self.fetchObject(in: context, matching: identifier) {
58 | return existingObject
59 | }
60 | let newObject = self.init(context: context)
61 | newObject.setValue(identifier, forKeyPath: "identifier")
62 | return newObject
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/Vocable/Features/Keyboard/Keypad/Layout/CompactKeyboardLayoutEN.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CompactKeyboardLayoutEN.swift
3 | // Vocable
4 | //
5 | // Created by Chris Stroud on 6/12/24.
6 | // Copyright © 2024 WillowTree. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import SwiftUI
11 | import Collections
12 |
13 | struct CompactKeyboardLayoutEN: KeyboardLayout {
14 |
15 | let identifier = "compact_layout_EN"
16 |
17 | func makeLayout(
18 | configuration: KeyboardLayoutConfiguration
19 | ) -> KeyboardBody {
20 | switch configuration.mode {
21 | case .alphabetical:
22 | let values = "ABCDEFGHIJKLMNOPQRSTUVWXYZ',.?"
23 | KeyboardLayoutGroup {
24 | for row in values.chunks(ofCount: 6) {
25 | KeyboardLayoutRow {
26 | for key in row {
27 | KeyboardLayoutKey(key)
28 | }
29 | }
30 | }
31 | KeyboardLayoutRow {
32 | for key in "()!\"" {
33 | KeyboardLayoutKey(key)
34 | }
35 | KeyboardLayoutKey(.backspace)
36 | .keyWidth(span: 2)
37 | }
38 | }
39 | .keyWidth(count: 6)
40 | case .numerical:
41 | let values = "123456789"
42 | for row in values.chunks(ofCount: 3) {
43 | KeyboardLayoutRow {
44 | for key in row {
45 | KeyboardLayoutKey(key)
46 | .keyWidth(count: 3)
47 | }
48 | }
49 | }
50 | KeyboardLayoutRow {
51 | KeyboardLayoutKey(".")
52 | KeyboardLayoutKey("0")
53 | KeyboardLayoutKey(.backspace)
54 | }
55 | .keyWidth(count: 3)
56 | case .modifierPicker:
57 | KeyboardLayoutGroup { }
58 | }
59 | KeyboardLayoutRow {
60 | KeyboardLayoutKey(
61 | configuration.mode == .alphabetical ? .numberPad : .alphabet
62 | )
63 | .keyWidth(span: 3)
64 | KeyboardLayoutKey(.space)
65 | .keyWidth(span: 6)
66 | KeyboardLayoutKey(.speak)
67 | .keyWidth(span: 3)
68 | }
69 | .keyWidth(count: 12)
70 | }
71 | }
72 |
73 | #Preview {
74 | KeyboardLayoutPreviewView(
75 | layout: CompactKeyboardLayoutEN(),
76 | mode: .alphabetical
77 | )
78 | }
79 |
--------------------------------------------------------------------------------