feat(mac): migrate keyman packages individually to group containers
Some checks are pending
Keyman Build Summary / Summarize build status checks (push) Waiting to run

also rename Keyman-Keyboards directory to
Keyman-Packages
This commit is contained in:
Shawn Schantz 2026-04-17 14:13:23 -04:00
parent 10a2c8ddb3
commit 364ccc2d52
10 changed files with 257 additions and 93 deletions

View file

@ -24,6 +24,12 @@ struct ConfigView: View {
Button("debug") {
settings.debug()
}
Button("log defaults") {
settings.logSettings()
}
Button("clear defaults") {
settings.clearSettings()
}
Spacer()
}
.padding()

View file

@ -9,7 +9,10 @@
NS_ASSUME_NONNULL_BEGIN
extern NSString *const kKeymanGroupId;
@interface KMDataRepository : NSObject
// keyman18DataDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman'
@property (readonly) NSURL *keyman18DataDirectory;

View file

@ -11,6 +11,11 @@
#import "KMDataRepository.h"
#import "KMLogs.h"
// the name of the app group shared by the Keyman input method and the Keyman configuration app
// an iOS-style app group of this name is defined for Keyman on developer.apple.com and linked
// to the each app's provisioning profile
NSString *const kKeymanGroupId = @"group.com.keyman";
@interface KMDataRepository ()
@property (readonly) NSURL *applicationSupportSubDirectory;
@property (readonly) NSURL *documentsSubDirectory;
@ -22,7 +27,7 @@
* Three directory trees are represented by the following properties, one in active use
* and two that are obsolete.
* The actively used directories, introduced in Keyman 19, are shared via the app group `group.com.keyman`:
* 'Group Containers/group.com.keyman/Library/Application Support/Keyman-Keyboards/
* 'Group Containers/group.com.keyman/Library/Application Support/Keyman-Packages/
* The obsolete directories from Keyman 18 are:
* applicationSupportSubDirectory: '~/Library/Application Support'
* keyman18DataDirectory: '~/Library/Application Support/keyman.inputmethod.Keyman'
@ -49,10 +54,7 @@ NSString *const kKeyboardsDirectoryName = @"Keyman-Keyboards";
*/
NSString *const kKeymanSubdirectoryName = @"keyman.inputmethod.Keyman";
//NSString *const kKeymanGroupId = @"3YE4W86L3G.com.keyman";
NSString *const kKeymanGroupId = @"group.com.keyman";
NSString *const kContainerKeyboardsPartialPath = @"Library/Application Support/Keyman-Keyboards";
NSString *const kContainerKeyboardsPartialPath = @"Library/Application Support/Keyman-Packages";
+ (KMDataRepository *)shared {
static KMDataRepository *shared = nil;
@ -63,6 +65,52 @@ NSString *const kContainerKeyboardsPartialPath = @"Library/Application Support/K
return shared;
}
/**
* Returns true if URL is an existing directory that is not empty
*/
+ (BOOL)isExistingNonEmptyDirectory:(NSURL *)directoryUrl {
return [KMDataRepository isExistingDirectory: directoryUrl] && ![KMDataRepository isEmptyDirectory: directoryUrl];
}
/**
* Returns true if URL is an existing directory that is empty
*/
+ (BOOL)isExistingEmptyDirectory:(NSURL *)directoryUrl {
return [KMDataRepository isExistingDirectory: directoryUrl] && [KMDataRepository isEmptyDirectory: directoryUrl];
}
/**
* Returns true if the directory is empty
*/
+ (BOOL)isExistingDirectory:(NSURL *)directoryUrl {
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL nonEmptyDirectoryExists = false;
BOOL isDirectory;
BOOL urlExists = ([fileManager fileExistsAtPath:directoryUrl.path isDirectory:&isDirectory]);
return urlExists && isDirectory;
}
/**
* Returns true if the directory is empty
*/
+ (BOOL)isEmptyDirectory:(NSURL *)directoryUrl {
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isEmpty = true;
NSError *error = nil;
NSArray *contents = [fileManager contentsOfDirectoryAtPath:directoryUrl.path error:&error];
if (error) {
os_log_debug([KMLogs dataLog], "cannot read directory: %@", directoryUrl.path);
} else if (contents.count > 0) {
isEmpty = false;
} else {
isEmpty = true;
}
return isEmpty;
}
- (NSURL *)documentsSubDirectory {
if (_documentsSubDirectory == nil) {
NSError *directoryError = nil;
@ -225,35 +273,24 @@ NSString *const kContainerKeyboardsPartialPath = @"Library/Application Support/K
* otherwise we would not be attempting to migrate.
*/
- (BOOL)keyboardsExistInDocumentsDirectory {
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isDir;
BOOL exists = ([fileManager fileExistsAtPath:self.keyman17KeyboardsDirectory.path isDirectory:&isDir]);
return exists;
return [KMDataRepository isExistingNonEmptyDirectory: self.keyman17KeyboardsDirectory];
}
/**
* Only called from migrateDataForKeyman19.
* Checks to see if the keyboards directory exists in '~/Library/Application Support/keyman.inputmethod.Keyman'
* Checks to see if the keyboards directory exists in '~/Library/Application Support/keyman.inputmethod.Keyman' and is not empty
*/
- (BOOL)keyboardsExistInInputMethodDataDirectory {
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isDir;
BOOL exists = ([fileManager fileExistsAtPath:self.keyman18KeyboardsDirectory.path isDirectory:&isDir]);
return exists;
return [KMDataRepository isExistingNonEmptyDirectory: self.keyman18KeyboardsDirectory];
}
// TODO: unused?
/**
* Checks to see if the keyboards directory exists in '~/Library/Group Containers/group.com.keyman/Library/Application Support'
* Checks to see if the keyboards directory exists in '~/Library/Group Containers/group.com.keyman/Library/Application Support' and is not empty
*/
- (BOOL)keyboardsExistInGroupContainerDirectory {
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isDir;
BOOL exists = ([fileManager fileExistsAtPath:self.keyman19KeyboardsDirectory.path isDirectory:&isDir]);
return exists;
return [KMDataRepository isExistingNonEmptyDirectory: self.keyman19KeyboardsDirectory];
}
// TODO: delete - no reason to move data more than once
/**
* Migrate the keyboards data from the old location in '~/Documents' to the location '~/Library/Application Support/keyman.inputmethod.Keyman'
@ -283,6 +320,49 @@ NSString *const kContainerKeyboardsPartialPath = @"Library/Application Support/K
return didMoveData;
}
/**
* Move all the packages contained in the directory
*/
+ (void)movePackages: (NSURL*)sourceDirectory to: (NSURL*)destinationDirectory {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *keys = @[NSURLIsDirectoryKey];
// fetch the individual keyboard package directories within the keyboards directory
NSArray *packageDirectories = [fileManager contentsOfDirectoryAtURL:sourceDirectory includingPropertiesForKeys:keys options:NSDirectoryEnumerationSkipsHiddenFiles error:nil];
// Iterate and filter for directories
for (NSURL *packageUrl in packageDirectories) {
NSNumber *isDirectory;
[packageUrl getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil];
if ([isDirectory boolValue]) {
NSString *packageName = packageUrl.lastPathComponent;
NSURL *packageSourceUrl = [sourceDirectory URLByAppendingPathComponent:packageName isDirectory: true];
NSURL *packageDestinationUrl = [destinationDirectory URLByAppendingPathComponent:packageName isDirectory: true];
os_log_info([KMLogs dataLog], "moving package directory: '%{public}@'", packageName);
BOOL movedDirectory = [KMDataRepository moveDirectory: packageSourceUrl to: packageDestinationUrl];
}
}
}
/**
* Move a single directory
*/
+ (BOOL)moveDirectory: (NSURL*)sourceDirectory to: (NSURL*)destinationDirectory {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *moveError = nil;
BOOL didMoveData = [fileManager moveItemAtURL:sourceDirectory
toURL:destinationDirectory
error:&moveError];
if (moveError) {
os_log_error([KMLogs dataLog], "data migration failed: '%{public}@'", moveError.localizedDescription);
} else {
os_log_info([KMLogs dataLog], "data migrated successfully to: '%{public}@'", destinationDirectory.path);
}
return didMoveData;
}
/**
* Migrate the keyboards data from the input method specific location in '~/Application Support/keyman.inputmethod.Keyman/'
* to the shared location in '~/Library/Group Containers/group.com.keyman/Library/Application Support'
@ -296,20 +376,15 @@ NSString *const kContainerKeyboardsPartialPath = @"Library/Application Support/K
// only move data if there is something to move
if (dataExistsInOldLocation) {
NSError *moveError = nil;
didMoveData = [fileManager moveItemAtURL:self.keyman18KeyboardsDirectory
toURL:self.keyman19KeyboardsDirectory
error:&moveError];
if (moveError) {
os_log_error([KMLogs dataLog], "data migration failed: '%{public}@'", moveError.localizedDescription);
} else {
os_log_info([KMLogs dataLog], "data migrated successfully to: '%{public}@'", self.keyman19KeyboardsDirectory.path);
}
[KMDataRepository.shared createKeyman19SharedDirectoriesIfNecessary];
[KMDataRepository movePackages:[self keyman18KeyboardsDirectory] to:[self keyman19KeyboardsDirectory]];
}
return didMoveData;
}
- (NSString*)buildFullPath:(NSString *)fromPartialPath {
NSString *fullPath = [self.keyman18KeyboardsDirectory.path stringByAppendingString:fromPartialPath];
os_log_debug([KMLogs dataLog], "buildFullPath: '%{public}@' fromPartialPath '%{public}@'",

View file

@ -756,29 +756,23 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef
- (void)prepareStorage {
os_log_debug([KMLogs dataLog], "*** prepareStorage ***");
// TODO: create directory before copy during migration?
// [KMDataRepository.shared createKeyman19SharedDirectoriesIfNecessary];
// [KMDataRepository.shared createKeyman18DataDirectoryIfNecessary];
// Keyman 18 data migration: TODO: uncomment and modify for double migration
// Keyman 18 data migration: TODO: uncomment and modify for double migration
/*
if ([KMSettingsRepository.shared keyman18DataMigrationNeeded]) {
[KMDataRepository.shared migrateDataForKeyman18];
[KMSettingsRepository.shared migrateSettingsForKeyman18];
}
*/
// Keyman 19 data migration
// if necessary, migrate settings and keyboard data for compatibility with Keyman 19
if ([KMSettingsRepository.shared keyman19SettingsMigrationNeeded]) {
[KMDataRepository.shared migrateDataForKeyman19];
//[KMSettingsRepository.shared migrateSettingsForKeyman18];
[KMSettingsRepository.shared migrateSettingsForKeyman19];
}
[KMDataRepository.shared createKeyman19SharedDirectoriesIfNecessary];
// TODO: delete
// TODO: delete, instead manage shared settings
//[KMDataRepository.shared createKeyboardsDirectoryIfNecessary];
[KMSettingsRepository.shared setDataModelVersionIfNecessary];
//[KMSettingsRepository.shared setDataModelVersionIfNecessary];
}
- (void)setDefaultKeymanMenuItems {

View file

@ -18,6 +18,8 @@ NS_ASSUME_NONNULL_BEGIN
- (BOOL)keyman19SettingsMigrationNeeded;
- (void)migrateSettingsForKeyman18;
- (void)setDataModelVersionIfNecessary;
- (void)migrateSettingsForKeyman19;
- (BOOL)migrateInputMethodSettingsToAppGroup;
- (NSString *)readSelectedKeyboard;
- (void)writeSelectedKeyboard:(NSString *)selectedKeyboard;
- (NSArray *)readActiveKeyboards;

View file

@ -11,6 +11,9 @@
#import "KMLogs.h"
#import "KMDataRepository.h"
// same as app group ID, kKeymanGroupId
//NSString *const kKeymanSuiteName = @"group.com.keyman";
NSString *const kActiveKeyboardsKey = @"KMActiveKeyboardsKey";
NSString *const kSelectedKeyboardKey = @"KMSelectedKeyboardKey";
NSString *const kPersistedOptionsKey = @"KMPersistedOptionsKey";
@ -45,7 +48,8 @@ NSString *const kNewPathComponent = @"/Library/Application Support/keyman.inputm
* directory instead of in the Documents directory.
*/
NSString *const kDataModelVersion = @"KMDataModelVersion";
NSInteger const kVersionStoreDataInLibraryDirectory = 1;
NSInteger const kVersionStoreDataInLibraryDirectory = 1; // introduced with Keyman 18
NSInteger const kVersionStoreDataInGroupContainer = 2; // introduced with Keyman 19
NSInteger const kCurrentDataModelVersionNumber = kVersionStoreDataInLibraryDirectory;
@implementation KMSettingsRepository
@ -60,6 +64,100 @@ NSInteger const kCurrentDataModelVersionNumber = kVersionStoreDataInLibraryDirec
return shared;
}
/**
* Determines whether the keyboard data needs to be moved from pre-Keyman-18 location to the Keyman 18 location
* This is true if
* 1) the UserDefaults exist (indicating that this is not a new installation of Keyman) and
* 2) the value for kVersionStoreDataInLibraryDirectory is < 1,
*/
- (BOOL)keyman18DataMigrationNeeded {
BOOL keymanSettingsExist = [self settingsExist];
os_log([KMLogs dataLog], "keyman settings exist: %{public}@", keymanSettingsExist ? @"YES" : @"NO" );
BOOL keyboardsStoredInLibrary = [self dataModelWithKeyboardsInLibrary];
os_log([KMLogs dataLog], "settings indicate that keyboards are stored in ~/Library: %{public}@", keyboardsStoredInLibrary ? @"YES" : @"NO" );
BOOL migrationNeeded = keymanSettingsExist && !keyboardsStoredInLibrary;
os_log([KMLogs dataLog], "dataMigrationNeeded: %{public}@", migrationNeeded ? @"YES" : @"NO" );
return migrationNeeded;
}
/**
* Determines whether the keyboard data needs to be moved from the Keyman 18 location to the Keyman 19 location
* This is true if the UserDefaults exist in the old location for the input method
*/
- (BOOL)keyman19SettingsMigrationNeeded {
BOOL keymanSettingsExistForInputMethod = [self inputMethodUserDefaultsExist];
os_log([KMLogs dataLog], "keyman input method settings exist (for 18 and earlier): %{public}@", keymanSettingsExistForInputMethod ? @"YES" : @"NO" );
BOOL keyboardsStoredInLibrary = [self dataModelWithKeyboardsInLibrary];
os_log([KMLogs dataLog], "settings indicate that keyboards are stored in ~/Library: %{public}@", keyboardsStoredInLibrary ? @"YES" : @"NO" );
BOOL migrationNeeded = keymanSettingsExistForInputMethod;
os_log([KMLogs dataLog], "dataMigrationNeeded: %{public}@", migrationNeeded ? @"YES" : @"NO" );
return migrationNeeded;
}
- (void)migrateSettingsForKeyman19 {
[self migrateInputMethodSettingsToAppGroup];
NSUserDefaults *groupUserDefaults = [[NSUserDefaults alloc] initWithSuiteName:kKeymanGroupId];
// set kDataModelVersion to indicate that we are using the group container
[groupUserDefaults setInteger:kVersionStoreDataInGroupContainer forKey:kDataModelVersion];
}
/**
* Move userdefaults from app to app group
* Read the settings in the input method's user defaults
* Write them to the shared app group user defaults
* Delete them from the input method's user defaults
*/
- (BOOL)migrateInputMethodSettingsToAppGroup {
NSUserDefaults *appUserDefaults = [NSUserDefaults standardUserDefaults];
NSUserDefaults *groupUserDefaults = [[NSUserDefaults alloc] initWithSuiteName:kKeymanGroupId];
NSString *selectedKeyboard = [appUserDefaults stringForKey:kSelectedKeyboardKey];
if (selectedKeyboard != nil) {
[groupUserDefaults setObject:selectedKeyboard forKey:kSelectedKeyboardKey];
}
NSArray * activeKeyboards = [appUserDefaults arrayForKey:kActiveKeyboardsKey];
if (activeKeyboards != nil) {
[groupUserDefaults setObject:activeKeyboards forKey:kActiveKeyboardsKey];
}
if ([appUserDefaults objectForKey:kShowOskOnActivate] != nil) {
BOOL showOsk = [appUserDefaults boolForKey:kShowOskOnActivate];
[groupUserDefaults setBool:showOsk forKey:kShowOskOnActivate];
}
if ([appUserDefaults objectForKey:kForceSentryError] != nil) {
BOOL forceSentryError = [appUserDefaults boolForKey:kForceSentryError];
[groupUserDefaults setBool:forceSentryError forKey:kForceSentryError];
}
return true;
}
/*
NSString *const kActiveKeyboardsKey = @"KMActiveKeyboardsKey";
NSString *const kSelectedKeyboardKey = @"KMSelectedKeyboardKey";
NSString *const kPersistedOptionsKey = @"KMPersistedOptionsKey";
NSString *const kShowOskOnActivate = @"KMShowOskOnActivate";
NSString *const kForceSentryError = @"KMForceSentryError";
*/
- (void)migrateSettingsForKeyman18 {
os_log_debug([KMLogs dataLog], "converting settings in UserDefaults for migration");
[self convertSelectedKeyboardPathForKeyman18Migration];
[self convertActiveKeyboardArrayForKeyman18Migration];
[self convertOptionsPathsForKeyman18Migration];
}
- (void)setDataModelVersionIfNecessary {
if (![self dataModelWithKeyboardsInLibrary]) {
[[NSUserDefaults standardUserDefaults] setInteger:kVersionStoreDataInLibraryDirectory forKey:kDataModelVersion];
@ -85,7 +183,6 @@ NSInteger const kCurrentDataModelVersionNumber = kVersionStoreDataInLibraryDirec
return ([[NSUserDefaults standardUserDefaults] objectForKey:kSelectedKeyboardKey] != nil);
}
- (void)writeOptionForSelectedKeyboard:(NSString *)key withValue:(NSString*)value {
NSDictionary *optionsMap = [self readOptionsForSelectedKeyboard];
NSDictionary *newOptionsMap = nil;
@ -134,43 +231,6 @@ NSInteger const kCurrentDataModelVersionNumber = kVersionStoreDataInLibraryDirec
return dataModelVersion >= kVersionStoreDataInLibraryDirectory;
}
/**
* Determines whether the keyboard data needs to be moved from pre-Keyman-18 location to the Keyman 18 location
* This is true if
* 1) the UserDefaults exist (indicating that this is not a new installation of Keyman) and
* 2) the value for kVersionStoreDataInLibraryDirectory is < 1,
*/
- (BOOL)keyman18DataMigrationNeeded {
BOOL keymanSettingsExist = [self settingsExist];
os_log([KMLogs dataLog], "keyman settings exist: %{public}@", keymanSettingsExist ? @"YES" : @"NO" );
BOOL keyboardsStoredInLibrary = [self dataModelWithKeyboardsInLibrary];
os_log([KMLogs dataLog], "settings indicate that keyboards are stored in ~/Library: %{public}@", keyboardsStoredInLibrary ? @"YES" : @"NO" );
BOOL migrationNeeded = keymanSettingsExist && !keyboardsStoredInLibrary;
os_log([KMLogs dataLog], "dataMigrationNeeded: %{public}@", migrationNeeded ? @"YES" : @"NO" );
return migrationNeeded;
}
/**
* Determines whether the keyboard data needs to be moved from the Keyman 18 location to the Keyman 19 location
* This is true if the UserDefaults exist in the old location for the input method
*/
- (BOOL)keyman19SettingsMigrationNeeded {
BOOL keymanSettingsExistForInputMethod = [self inputMethodUserDefaultsExist];
os_log([KMLogs dataLog], "keyman input method settings exist (for 18 and earlier): %{public}@", keymanSettingsExistForInputMethod ? @"YES" : @"NO" );
BOOL keyboardsStoredInLibrary = [self dataModelWithKeyboardsInLibrary];
os_log([KMLogs dataLog], "settings indicate that keyboards are stored in ~/Library: %{public}@", keyboardsStoredInLibrary ? @"YES" : @"NO" );
BOOL migrationNeeded = keymanSettingsExistForInputMethod;
os_log([KMLogs dataLog], "dataMigrationNeeded: %{public}@", migrationNeeded ? @"YES" : @"NO" );
return migrationNeeded;
}
- (NSString *)readSelectedKeyboard {
return [[NSUserDefaults standardUserDefaults] objectForKey:kSelectedKeyboardKey];
}
@ -254,13 +314,6 @@ NSInteger const kCurrentDataModelVersionNumber = kVersionStoreDataInLibraryDirec
return [userData removeObjectForKey:kPersistedOptionsKey];
}
- (void)migrateSettingsForKeyman18 {
os_log_debug([KMLogs dataLog], "converting settings in UserDefaults for migration");
[self convertSelectedKeyboardPathForKeyman18Migration];
[self convertActiveKeyboardArrayForKeyman18Migration];
[self convertOptionsPathsForKeyman18Migration];
}
- (void)convertSelectedKeyboardPathForKeyman18Migration {
NSString *selectedKeyboardPath = [self readSelectedKeyboard];
if (selectedKeyboardPath != nil) {

View file

@ -52,7 +52,15 @@ public class SettingsContainer : ObservableObject {
}
}
}
public func logSettings() {
self.settingsRepository.logSettings()
}
public func clearSettings() {
self.settingsRepository.clearSettings()
}
// TODO: extract method for keyboard search
// TODO: throw error for keyboard not found or log and fail silently (should never happen)
public func isKeyboardEnabled(packageId: UUID, keyboardId: String) -> Bool {

View file

@ -30,7 +30,7 @@ import Foundation
* containerDirectory: '~/Library/Group Containers'
* containerKeymanDirectory: '~/Library/Group Containers/group.com.keyman'
* groupKeymanSupportDirectory: '~/Library/Group Containers/group.com.keyman/Library/Application Support'
* keyman19KeyboardsDirectory: '~/Library/Group Containers/group.com.keyman/Library/Application Support/Keyman-Keyboards'
* keyman19KeyboardsDirectory: '~/Library/Group Containers/group.com.keyman/Library/Application Support/Keyman-Packages'
*/
public struct KeymanPaths {
@ -43,7 +43,7 @@ public struct KeymanPaths {
static private let keymanSubdirectoryName = "keyman.inputmethod.Keyman"
static private let containerPreferencesPartialPath = "Library/Preferences"
static private let containerKeyboardsPartialPath = "Library/Application Support/Keyman-Keyboards"
static private let containerKeyboardsPartialPath = "Library/Application Support/Keyman-Packages"
// keyman 17 and earlier
let keyman17DocumentsDirectory: URL?

View file

@ -22,7 +22,7 @@ public struct SettingsRepository {
let kActiveKeyboardsKey = "KMActiveKeyboardsKey"
let kSelectedKeyboardKey = "KMSelectedKeyboardKey"
let kPersistedOptionsKey = "KMPersistedOptionsKey"
let kDataModelVersionKey = "KMDataModelVersionKey"
let kDataModelVersionKey = "KMDataModelVersion"
let kShowOskOnActivateKey = "KMShowOskOnActivate"
let kForceSentryError = "KMForceSentryError"
@ -68,4 +68,27 @@ public struct SettingsRepository {
sharedDefaults.set(keyboardName, forKey: kSelectedKeyboardKey)
}
}
public func logSettings() {
print("UserDefaults:")
if let sharedDefaults = self.defaults {
print("kSelectedKeyboardKey: \(sharedDefaults.value(forKey: kSelectedKeyboardKey) ?? "nil")")
print("kDataModelVersionKey: \(sharedDefaults.value(forKey: kDataModelVersionKey) ?? "nil")")
print("kForceSentryError: \(sharedDefaults.value(forKey: kForceSentryError) ?? "nil")")
print("kShowOskOnActivateKey: \(sharedDefaults.value(forKey: kShowOskOnActivateKey) ?? "nil")")
print("kActiveKeyboardsKey: \(sharedDefaults.value(forKey: kActiveKeyboardsKey) ?? "nil")")
print("kPersistedOptionsKey: \(sharedDefaults.value(forKey: kPersistedOptionsKey) ?? "nil")")
}
}
public func clearSettings() {
guard let sharedDefaults = self.defaults else {
print("Group container UserDefaults not found.")
return
}
sharedDefaults.dictionaryRepresentation().keys.forEach { key in
sharedDefaults.removeObject(forKey: key)
}
}
}

View file

@ -46,7 +46,7 @@ import Foundation
@Test("Check Keyman 19 keyboards directory") func testKeyman19KeyboardsDirectory() async throws {
#expect(true)
let keyboardsDirectory = try #require(KeymanPaths().keyman19KeyboardsDirectory)
#expect(keyboardsDirectory.absoluteString.hasSuffix("Group%20Containers/group.com.keyman/Library/Application%20Support/Keyman-Keyboards/"))
#expect(keyboardsDirectory.absoluteString.hasSuffix("Group%20Containers/group.com.keyman/Library/Application%20Support/Keyman-Packages/"))
}
}