diff --git a/app/AboutAppearanceViewController.m b/app/AboutAppearanceViewController.m index 2195dd3f09..4af685b879 100644 --- a/app/AboutAppearanceViewController.m +++ b/app/AboutAppearanceViewController.m @@ -8,6 +8,7 @@ #import "AboutAppearanceViewController.h" #import "FontPickerViewController.h" #import "UserPreferences.h" +#import "NSObject+SaneKVO.h" static NSString *const ThemeNameCellIdentifier = @"Theme Name"; static NSString *const FontSizeCellIdentifier = @"Font Size"; @@ -21,14 +22,11 @@ @implementation AboutAppearanceViewController - (void)viewDidLoad { [super viewDidLoad]; - [[UserPreferences shared] addObserver:self forKeyPath:@"theme" options:NSKeyValueObservingOptionNew context:nil]; - [[UserPreferences shared] addObserver:self forKeyPath:@"fontSize" options:NSKeyValueObservingOptionNew context:nil]; - [[UserPreferences shared] addObserver:self forKeyPath:@"fontFamily" options:NSKeyValueObservingOptionNew context:nil]; -} - -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - [self.tableView reloadData]; - [self setNeedsStatusBarAppearanceUpdate]; + [UserPreferences.shared observe:@[@"theme", @"fontSize", @"fontFamily"] + options:0 owner:self usingBlock:^(typeof(self) self) { + [self.tableView reloadData]; + [self setNeedsStatusBarAppearanceUpdate]; + }]; } #pragma mark - Table view data source diff --git a/app/AboutExternalKeyboardViewController.m b/app/AboutExternalKeyboardViewController.m index 0b46652cd6..0a4a066552 100644 --- a/app/AboutExternalKeyboardViewController.m +++ b/app/AboutExternalKeyboardViewController.m @@ -7,6 +7,7 @@ #import "AboutExternalKeyboardViewController.h" #import "UserPreferences.h" +#import "NSObject+SaneKVO.h" const int kCapsLockMappingSection = 0; @@ -23,15 +24,13 @@ @implementation AboutExternalKeyboardViewController - (void)viewDidLoad { [super viewDidLoad]; - [UserPreferences.shared addObserver:self forKeyPath:@"capsLockMapping" options:NSKeyValueObservingOptionNew context:nil]; - [UserPreferences.shared addObserver:self forKeyPath:@"optionMapping" options:NSKeyValueObservingOptionNew context:nil]; + [UserPreferences.shared observe:@[@"capsLockMapping", @"optionMapping"] + options:0 owner:self usingBlock:^(typeof(self) self) { + [self.tableView reloadData]; + }]; [self _update]; } -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - [self.tableView reloadData]; -} - - (void)_update { self.optionMetaSwitch.on = UserPreferences.shared.optionMapping == OptionMapEsc; self.backtickEscapeSwitch.on = UserPreferences.shared.backtickMapEscape; diff --git a/app/AboutNavigationController.m b/app/AboutNavigationController.m index 0ccfa91366..51b8751e4e 100644 --- a/app/AboutNavigationController.m +++ b/app/AboutNavigationController.m @@ -7,6 +7,7 @@ #import "AboutNavigationController.h" #import "UserPreferences.h" +#import "NSObject+SaneKVO.h" @interface AboutNavigationController () @@ -16,12 +17,9 @@ @implementation AboutNavigationController - (void)viewDidLoad { [super viewDidLoad]; - [[UserPreferences shared] addObserver:self forKeyPath:@"theme" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionInitial context:nil]; -} - -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - if (@available(iOS 13, *)) { - if ([keyPath isEqualToString:@"theme"]) { + [UserPreferences.shared observe:@[@"theme"] options:NSKeyValueObservingOptionInitial + owner:self usingBlock:^(typeof(self) self) { + if (@available(iOS 13, *)) { UIKeyboardAppearance appearance = UserPreferences.shared.theme.keyboardAppearance; if (appearance == UIKeyboardAppearanceDark) { self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark; @@ -29,7 +27,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N self.overrideUserInterfaceStyle = UIUserInterfaceStyleLight; } } - } + }]; } @end diff --git a/app/AboutViewController.m b/app/AboutViewController.m index db1944cb89..aa109848a8 100644 --- a/app/AboutViewController.m +++ b/app/AboutViewController.m @@ -9,6 +9,7 @@ #import "AboutViewController.h" #import "UserPreferences.h" #import "AppGroup.h" +#import "NSObject+SaneKVO.h" @interface AboutViewController () @property (weak, nonatomic) IBOutlet UITableViewCell *capsLockMappingCell; @@ -46,12 +47,10 @@ - (void)viewDidLoad { [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"], [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]]; - UserPreferences *prefs = [UserPreferences shared]; - NSKeyValueObservingOptions opts = NSKeyValueObservingOptionNew; - [prefs addObserver:self forKeyPath:@"capsLockMapping" options:opts context:nil]; - [prefs addObserver:self forKeyPath:@"fontSize" options:opts context:nil]; - [prefs addObserver:self forKeyPath:@"launchCommand" options:opts context:nil]; - [prefs addObserver:self forKeyPath:@"bootCommand" options:opts context:nil]; + [UserPreferences.shared observe:@[@"capsLockMapping", @"fontSize", @"launchCommand", @"bootCommand"] + options:0 owner:self usingBlock:^(typeof(self) self) { + [self _updatePreferenceUI]; + }]; } - (IBAction)dismiss:(id)sender { @@ -63,14 +62,6 @@ - (void)exitRecovery:(id)sender { exit(0); } -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - if ([object isKindOfClass:[UserPreferences class]]) { - [self _updatePreferenceUI]; - } else { - [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; - } -} - - (void)_updatePreferenceUI { UserPreferences *prefs = UserPreferences.shared; self.themeCell.detailTextLabel.text = prefs.theme.presetName; diff --git a/app/AppDelegate.m b/app/AppDelegate.m index e6e266faad..956531b024 100644 --- a/app/AppDelegate.m +++ b/app/AppDelegate.m @@ -16,6 +16,7 @@ #import "SceneDelegate.h" #import "PasteboardDevice.h" #import "LocationDevice.h" +#import "NSObject+SaneKVO.h" #import "Roots.h" #import "TerminalViewController.h" #import "UserPreferences.h" @@ -208,7 +209,10 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( extern const char *uname_version; uname_version = self.unameVersion.UTF8String; - [UserPreferences.shared addObserver:self forKeyPath:@"shouldDisableDimming" options:NSKeyValueObservingOptionInitial context:nil]; + [UserPreferences.shared observe:@[@"shouldDisableDimming"] options:NSKeyValueObservingOptionInitial + owner:self usingBlock:^(typeof(self) self) { + UIApplication.sharedApplication.idleTimerDisabled = UserPreferences.shared.shouldDisableDimming; + }]; struct sockaddr_in6 address = { .sin6_len = sizeof(address), @@ -237,10 +241,6 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( return YES; } -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - UIApplication.sharedApplication.idleTimerDisabled = UserPreferences.shared.shouldDisableDimming; -} - - (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions API_AVAILABLE(ios(13.0)) { for (UISceneSession *sceneSession in sceneSessions) { NSString *terminalUUID = sceneSession.stateRestorationActivity.userInfo[@"TerminalUUID"]; diff --git a/app/NSObject+SaneKVO.h b/app/NSObject+SaneKVO.h index 2fcd062ea1..1857321191 100644 --- a/app/NSObject+SaneKVO.h +++ b/app/NSObject+SaneKVO.h @@ -9,8 +9,27 @@ NS_ASSUME_NONNULL_BEGIN +typedef void (^KVOBlock)(void); + +@interface KVOObservation : NSObject { + BOOL _enabled; + __weak id _object; + NSString *_keyPath; + KVOBlock _block; +} +- (void)disable; +@end + @interface NSObject (SaneKVO) +- (KVOObservation *)observe:(NSString *)keyPath + options:(NSKeyValueObservingOptions)options + usingBlock:(KVOBlock)block; +- (void)observe:(NSArray *)keyPaths + options:(NSKeyValueObservingOptions)options + owner:(id)owner + usingBlock:(void (^)(id self))block; + @end NS_ASSUME_NONNULL_END diff --git a/app/NSObject+SaneKVO.m b/app/NSObject+SaneKVO.m index 439bcfd69b..a7ed98e058 100644 --- a/app/NSObject+SaneKVO.m +++ b/app/NSObject+SaneKVO.m @@ -8,113 +8,64 @@ #import #import "NSObject+SaneKVO.h" -static void *kKVOObject = &kKVOObject; - -@interface KVOObserver : NSObject { - @public - NSString *keyPath; - void *context; - __weak id object; -} -@end -@implementation KVOObserver -@end - -@interface KVOObject : NSObject - -- (instancetype)initWithOwner:(id)owner; -- (BOOL)removeMatchingObserver:(BOOL (^)(KVOObserver *))test; -@property (nonatomic, weak) id owner; -@property NSMutableArray *observers; +static void *kKVOObservations = &kKVOObservations; +@interface KVOObservation () +- (instancetype)initWithKeyPath:(NSString *)keyPath object:(id)object block:(KVOBlock)block; @end @implementation NSObject (SaneKVO) -- (id)sane_createObserver { - @synchronized (self) { - KVOObject *observer = objc_getAssociatedObject(self, kKVOObject); - if (observer == nil) { - observer = [[KVOObject alloc] initWithOwner:self]; - objc_setAssociatedObject(self, kKVOObject, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - } - return observer; - } -} -- (id)sane_observer { - return objc_getAssociatedObject(self, kKVOObject); +- (KVOObservation *)observe:(NSString *)keyPath options:(NSKeyValueObservingOptions)options usingBlock:(KVOBlock)block { + KVOObservation *observation = [[KVOObservation alloc] initWithKeyPath:keyPath object:self block:block]; + [self addObserver:observation forKeyPath:keyPath options:options context:NULL]; + return observation; } -- (void)sane_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context { - KVOObject *o = [observer sane_createObserver]; - @synchronized (o) { - KVOObserver *obs = [KVOObserver new]; - obs->object = self; - obs->keyPath = keyPath; - obs->context = context; - [o.observers addObject:obs]; - } - [self sane_addObserver:o forKeyPath:keyPath options:options context:context]; -} -- (void)sane_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath { - KVOObject *o = [observer sane_observer]; - if ([o removeMatchingObserver:^BOOL(KVOObserver *obs) { - return [obs->keyPath isEqualToString:keyPath]; - }]) { - [self sane_removeObserver:o forKeyPath:keyPath]; - } -} -- (void)sane_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context { - KVOObject *o = [observer sane_observer]; - if ([o removeMatchingObserver:^BOOL(KVOObserver *obs) { - return [obs->keyPath isEqualToString:keyPath] && obs->context == context; - }]) { - [self sane_removeObserver:o forKeyPath:keyPath context:context]; +- (void)observe:(NSArray *)keyPaths options:(NSKeyValueObservingOptions)options owner:(id)owner usingBlock:(void (^)(id self))block { + __weak id weakOwner = owner; + KVOBlock newBlock = ^{ + id owner = weakOwner; + NSAssert(owner, @"kvo notification shouldn't come to dead object"); + block(owner); + }; + @synchronized (owner) { + for (NSString *keyPath in keyPaths) { + NSMutableSet *observations = objc_getAssociatedObject(owner, kKVOObservations); + if (observations == nil) { + observations = [NSMutableSet new]; + objc_setAssociatedObject(owner, kKVOObservations, observations, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + [observations addObject:[self observe:keyPath options:options usingBlock:newBlock]]; + } } } -+ (void)load { - [self swizzle:@selector(addObserver:forKeyPath:options:context:) with:@selector(sane_addObserver:forKeyPath:options:context:)]; - [self swizzle:@selector(removeObserver:forKeyPath:context:) with:@selector(sane_removeObserver:forKeyPath:context:)]; - [self swizzle:@selector(removeObserver:forKeyPath:) with:@selector(sane_removeObserver:forKeyPath:)]; -} -+ (void)swizzle:(SEL)method with:(SEL)replacement { - method_exchangeImplementations(class_getInstanceMethod(self, method), - class_getInstanceMethod(self, replacement)); -} - @end -@implementation KVOObject +@implementation KVOObservation -- (instancetype)initWithOwner:(id)owner { +- (instancetype)initWithKeyPath:(NSString *)keyPath object:(id)object block:(KVOBlock)block { if (self = [super init]) { - _owner = owner; - _observers = [NSMutableArray new]; + _keyPath = keyPath; + _object = object; + _block = block; } return self; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - [_owner observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + _block(); } -- (BOOL)removeMatchingObserver:(BOOL (^)(KVOObserver *))test { - @synchronized (self) { - NSUInteger i = [_observers indexOfObjectPassingTest:^BOOL(KVOObserver * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - return test(obj); - }]; - if (i == NSNotFound) - return NO; - [_observers removeObjectAtIndex:i]; - return YES; +- (void)disable { + if (_enabled) { + [_object removeObserver:self forKeyPath:_keyPath context:NULL]; + _enabled = NO; } } - - (void)dealloc { - for (KVOObserver *obs in _observers) { - [obs->object removeObserver:self forKeyPath:obs->keyPath context:obs->context ]; - } + [self disable]; } @end diff --git a/app/Roots.m b/app/Roots.m index 0697ef3d19..133d73cf2b 100644 --- a/app/Roots.m +++ b/app/Roots.m @@ -8,6 +8,7 @@ #import #import "Roots.h" #import "AppGroup.h" +#import "NSObject+SaneKVO.h" #include "tools/fakefs.h" static NSURL *RootsDir() { @@ -50,21 +51,28 @@ - (instancetype)init { NSAssert(NO, @"failed to import alpine, error %@", error); } } - [self addObserver:self forKeyPath:@"roots" options:0 context:nil]; + [self observe:@[@"roots"] options:0 owner:self usingBlock:^(typeof(self) self) { + if (self.defaultRoot == nil && self.roots.count) + self.defaultRoot = self.roots[0]; + [self syncFileProviderDomains]; + }]; [self syncFileProviderDomains]; - self.defaultRoot = [NSUserDefaults.standardUserDefaults stringForKey:kDefaultRoot]; - [self addObserver:self forKeyPath:@"defaultRoot" options:0 context:nil]; if ((!self.defaultRoot || ![self.roots containsObject:self.defaultRoot]) && self.roots.count) self.defaultRoot = self.roots.firstObject; } return self; } +- (NSString *)defaultRoot { + return [NSUserDefaults.standardUserDefaults stringForKey:kDefaultRoot]; +} +- (void)setDefaultRoot:(NSString *)defaultRoot { + [NSUserDefaults.standardUserDefaults setObject:defaultRoot forKey:kDefaultRoot]; +} + - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - if ([keyPath isEqualToString:@"defaultRoot"]) { - [NSUserDefaults.standardUserDefaults setObject:self.defaultRoot forKey:kDefaultRoot]; - } else if ([keyPath isEqualToString:@"roots"]) { + if ([keyPath isEqualToString:@"roots"]) { if (self.defaultRoot == nil && self.roots.count) self.defaultRoot = self.roots[0]; [self syncFileProviderDomains]; diff --git a/app/RootsTableViewController.m b/app/RootsTableViewController.m index 718a43d169..258d3d0126 100644 --- a/app/RootsTableViewController.m +++ b/app/RootsTableViewController.m @@ -10,6 +10,7 @@ #import "ProgressReportViewController.h" #import "UIApplication+OpenURL.h" #import "UIViewController+Extras.h" +#import "NSObject+SaneKVO.h" @interface RootsTableViewController () @end @@ -29,11 +30,10 @@ @implementation RootsTableViewController - (void)viewDidLoad { [super viewDidLoad]; - [Roots.instance addObserver:self forKeyPath:@"roots" options:0 context:nil]; - [Roots.instance addObserver:self forKeyPath:@"defaultRoot" options:0 context:nil]; -} -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - [self.tableView reloadData]; + [Roots.instance observe:@[@"roots", @"defaultRoot"] + options:0 owner:self usingBlock:^(typeof(self) self) { + [self.tableView reloadData]; + }]; } - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { diff --git a/app/TerminalView.m b/app/TerminalView.m index 5a7d34a15c..45b6bcc104 100644 --- a/app/TerminalView.m +++ b/app/TerminalView.m @@ -9,6 +9,7 @@ #import "TerminalView.h" #import "UserPreferences.h" #import "UIApplication+OpenURL.h" +#import "NSObject+SaneKVO.h" struct rowcol { int row; @@ -36,9 +37,6 @@ @implementation TerminalView @synthesize inputDelegate; @synthesize tokenizer; -static int kObserverMappings; -static int kObserverStyling; - - (void)awakeFromNib { [super awakeFromNib]; self.inputAssistantItem.leadingBarButtonGroups = @[]; @@ -52,13 +50,14 @@ - (void)awakeFromNib { [self addSubview:scrollbarView]; UserPreferences *prefs = UserPreferences.shared; - [prefs addObserver:self forKeyPath:@"capsLockMapping" options:0 context:&kObserverMappings]; - [prefs addObserver:self forKeyPath:@"optionMapping" options:0 context:&kObserverMappings]; - [prefs addObserver:self forKeyPath:@"backtickMapEscape" options:0 context:&kObserverMappings]; - [prefs addObserver:self forKeyPath:@"overrideControlSpace" options:0 context:&kObserverMappings]; - [prefs addObserver:self forKeyPath:@"fontFamily" options:0 context:&kObserverStyling]; - [prefs addObserver:self forKeyPath:@"fontSize" options:0 context:&kObserverStyling]; - [prefs addObserver:self forKeyPath:@"theme" options:0 context:&kObserverStyling]; + [prefs observe:@[@"capsLockMapping", @"optionMapping", @"backtickMapEscape", @"overrideControlSpace"] + options:0 owner:self usingBlock:^(typeof(self) self) { + self->_keyCommands = nil; + }]; + [prefs observe:@[@"fontFamily", @"fontSize", @"theme"] + options:0 owner:self usingBlock:^(typeof(self) self) { + [self _updateStyle]; + }]; self.markedRange = [UITextRange new]; self.selectedRange = [UITextRange new]; @@ -73,10 +72,6 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N if (self.terminal.loaded) { [self _updateStyle]; } - } else if (context == &kObserverMappings) { - _keyCommands = nil; - } else if (context == &kObserverStyling) { - [self _updateStyle]; } } diff --git a/app/TerminalViewController.m b/app/TerminalViewController.m index 3409d929eb..f428067fa3 100644 --- a/app/TerminalViewController.m +++ b/app/TerminalViewController.m @@ -12,6 +12,7 @@ #import "ArrowBarButton.h" #import "UserPreferences.h" #import "AboutViewController.h" +#import "NSObject+SaneKVO.h" #include "kernel/init.h" #include "kernel/task.h" #include "kernel/calls.h" @@ -104,8 +105,10 @@ - (void)viewDidLoad { [self.escapeKey setImage:[UIImage systemImageNamed:@"escape"] forState:UIControlStateNormal]; } - [[UserPreferences shared] addObserver:self forKeyPath:@"theme" options:NSKeyValueObservingOptionNew context:nil]; - [[UserPreferences shared] addObserver:self forKeyPath:@"hideExtraKeysWithExternalKeyboard" options:NSKeyValueObservingOptionNew context:nil]; + [UserPreferences.shared observe:@[@"theme", @"hideExtraKeysWithExternalKeyboard"] + options:0 owner:self usingBlock:^(typeof(self) self) { + [self _updateStyleFromPreferences:YES]; + }]; } - (void)awakeFromNib { @@ -233,6 +236,9 @@ - (void)_updateStyleFromPreferences:(BOOL)animated { } [self setNeedsStatusBarAppearanceUpdate]; } +- (void)_updateStyleAnimated { + [self _updateStyleFromPreferences:YES]; +} - (UIStatusBarStyle)preferredStatusBarStyle { return UserPreferences.shared.theme.statusBarStyle;