2
0

Rewrite handling of collection view and table view reloading for reliability.

This commit is contained in:
Maarten Billemont
2017-04-27 02:20:49 -04:00
parent c8b4933c3d
commit ea5be8efcb
8 changed files with 144 additions and 178 deletions

View File

@@ -34,7 +34,7 @@
@property(assign, nonatomic) BOOL active;
- (void)setActive:(BOOL)active animated:(BOOL)animated completion:(void ( ^ )(BOOL finished))completion;
- (void)updatePasswords;
- (void)reloadPasswords;
- (IBAction)dismissPopdown:(id)sender;

View File

@@ -25,6 +25,8 @@
#import "MPAnswersViewController.h"
#import "MPMessageViewController.h"
static const NSString *MPTransientPasswordItem = @"MPTransientPasswordItem";
typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
MPPasswordsBadNameTip = 1 << 0,
};
@@ -32,7 +34,6 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
@interface MPPasswordsViewController()<NSFetchedResultsControllerDelegate>
@property(nonatomic, strong) IBOutlet UINavigationBar *navigationBar;
@property(nonatomic, readonly) NSString *query;
@end
@@ -42,11 +43,9 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
UIColor *_backgroundColor;
UIColor *_darkenedBackgroundColor;
__weak UIViewController *_popdownVC;
BOOL _showTransientItem;
NSUInteger _transientItem;
NSCharacterSet *_siteNameAcceptableCharactersSet;
NSArray *_fuzzyGroups;
NSMutableArray *_passwordCollectionViewUpdatesBatch;
NSMutableArray<NSMutableArray *> *_passwordCollectionSections;
}
#pragma mark - Life
@@ -62,8 +61,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
_backgroundColor = self.passwordCollectionView.backgroundColor;
_darkenedBackgroundColor = [_backgroundColor colorWithAlphaComponent:0.6f];
_transientItem = NSNotFound;
_passwordCollectionViewUpdatesBatch = [NSMutableArray arrayWithCapacity:4];
_passwordCollectionSections = [NSMutableArray new];
self.view.backgroundColor = [UIColor clearColor];
[self.passwordCollectionView automaticallyAdjustInsetsForKeyboard];
@@ -83,7 +81,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
[self registerObservers];
[self updateConfigKey:nil];
[self updatePasswords];
[self reloadPasswords];
}
- (void)viewDidAppear:(BOOL)animated {
@@ -140,30 +138,42 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
return CGSizeMake( itemWidth, 100 );
}
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout
insetForSectionAtIndex:(NSInteger)section {
UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout *)collectionViewLayout;
UIEdgeInsets occludedInsets = [self.passwordCollectionView occludedInsets];
UIEdgeInsets insets = layout.sectionInset;
if (section == 0)
insets.top += occludedInsets.top;
if (section == collectionView.numberOfSections - 1)
insets.bottom += occludedInsets.bottom;
return insets;
}
#pragma mark - UICollectionViewDataSource
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
return [self.fetchedResultsController.sections count];
return [_passwordCollectionSections count];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
if (![MPiOSAppDelegate get].activeUserOID || !_fetchedResultsController)
return 0;
NSUInteger objects = ((id<NSFetchedResultsSectionInfo>)self.fetchedResultsController.sections[section]).numberOfObjects;
_transientItem = _showTransientItem? objects: NSNotFound;
return objects + (_showTransientItem? 1: 0);
return [_passwordCollectionSections[(NSUInteger)section] count];
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MPPasswordCell *cell = [MPPasswordCell dequeueCellFromCollectionView:collectionView indexPath:indexPath];
[cell setFuzzyGroups:_fuzzyGroups];
if (indexPath.item < ((id<NSFetchedResultsSectionInfo>)self.fetchedResultsController.sections[indexPath.section]).numberOfObjects)
[cell setSite:[self.fetchedResultsController objectAtIndexPath:indexPath] animated:NO];
else
id item = _passwordCollectionSections[(NSUInteger)indexPath.section][(NSUInteger)indexPath.item];
if ([item isKindOfClass:[MPSiteEntity class]])
[cell setSite:item animated:NO];
else // item == MPTransientPasswordItem
[cell setTransientSite:self.query animated:NO];
return cell;
@@ -180,61 +190,14 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
#pragma mark - NSFetchedResultsControllerDelegate
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
Weakify( self );
if (controller == _fetchedResultsController) {
@synchronized (_passwordCollectionViewUpdatesBatch) {
[_passwordCollectionViewUpdatesBatch addObject:[^{
Strongify( self );
switch (type) {
case NSFetchedResultsChangeInsert:
[self.passwordCollectionView insertItemsAtIndexPaths:@[ newIndexPath ]];
break;
case NSFetchedResultsChangeDelete:
[self.passwordCollectionView deleteItemsAtIndexPaths:@[ indexPath ]];
break;
case NSFetchedResultsChangeMove:
if (![indexPath isEqual:newIndexPath])
[self.passwordCollectionView moveItemAtIndexPath:indexPath toIndexPath:newIndexPath];
break;
case NSFetchedResultsChangeUpdate:
[self.passwordCollectionView reloadItemsAtIndexPaths:@[ indexPath ]];
break;
}
} copy]];
}
[controller.managedObjectContext performBlock:^{
PearlMainQueueOperation( ^{
@try {
[self.passwordCollectionView performBatchUpdates:^{
[self updateTransientItem];
@synchronized (_passwordCollectionViewUpdatesBatch) {
for (VoidBlock block in _passwordCollectionViewUpdatesBatch)
block();
[_passwordCollectionViewUpdatesBatch removeAllObjects];
}
} completion:nil];
}
@catch (NSException *exception) {
wrn( @"While updating password cells: %@", [exception fullDescription] );
[self.passwordCollectionView reloadData];
}
} );
}];
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id<NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
if (controller == _fetchedResultsController)
[self.passwordCollectionView reloadData];
PearlMainQueue( ^{
[self.passwordCollectionView updateDataSource:_passwordCollectionSections
toSections:[self createPasswordCollectionSections]
reloadItems:nil completion:nil];
} );
}
#pragma mark - UISearchBarDelegate
@@ -266,7 +229,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
if (_passwordsDismissRecognizer)
[self.view removeGestureRecognizer:_passwordsDismissRecognizer];
[self updatePasswords];
[self reloadPasswords];
[UIView animateWithDuration:0.3f animations:^{
self.passwordCollectionView.backgroundColor = _backgroundColor;
}];
@@ -290,7 +253,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
if ([[self.query stringByTrimmingCharactersInSet:_siteNameAcceptableCharactersSet] length])
[self showTips:MPPasswordsBadNameTip];
[self updatePasswords];
[self reloadPasswords];
}
}
@@ -311,24 +274,29 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
}];
}
- (void)updateTransientItem {
- (NSMutableArray<NSMutableArray *> *)createPasswordCollectionSections {
NSString *query = self.query;
_showTransientItem = [query length] > 0 && ![[[self.fetchedResultsController.sections[0] objects] filteredArrayUsingPredicate:
[NSPredicate predicateWithBlock:^BOOL(MPSiteEntity *site, NSDictionary<NSString *, id> *bindings) {
return [site.name isEqualToString:query];
}]] count];
if (!_showTransientItem && _transientItem != NSNotFound)
[self.passwordCollectionView deleteItemsAtIndexPaths:
@[ [NSIndexPath indexPathForItem:_transientItem inSection:0] ]];
else if (_showTransientItem && _transientItem == NSNotFound) {
NSUInteger objects = [self.fetchedResultsController.sections[0] numberOfObjects];
[self.passwordCollectionView insertItemsAtIndexPaths:
@[ [NSIndexPath indexPathForItem:objects inSection:0] ]];
BOOL needTransientItem = [query length] > 0;
NSArray<id<NSFetchedResultsSectionInfo>> *sectionInfos = [self.fetchedResultsController sections];
NSMutableArray *sections = [[NSMutableArray alloc] initWithCapacity:[sectionInfos count]];
for (id<NSFetchedResultsSectionInfo> sectionInfo in sectionInfos) {
NSArray<MPSiteEntity *> *sites = [sectionInfo.objects copy];
[sections addObject:sites];
if (needTransientItem)
for (MPSiteEntity *site in sites)
if ([site.name isEqualToString:query]) {
needTransientItem = NO;
break;
}
}
else if (_transientItem != NSNotFound)
[self.passwordCollectionView reloadItemsAtIndexPaths:
@[ [NSIndexPath indexPathForItem:_transientItem inSection:0] ]];
if (needTransientItem)
[sections addObject:@[ MPTransientPasswordItem ]];
return sections;
}
- (void)registerObservers {
@@ -340,7 +308,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
} );
PearlAddNotificationObserver( UIApplicationWillEnterForegroundNotification, nil, [NSOperationQueue mainQueue],
^(MPPasswordsViewController *self, NSNotification *note) {
[self updatePasswords];
[self reloadPasswords];
} );
PearlAddNotificationObserver( UIApplicationDidBecomeActiveNotification, nil, [NSOperationQueue mainQueue],
^(MPPasswordsViewController *self, NSNotification *note) {
@@ -372,7 +340,7 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
PearlAddNotificationObserver( NSPersistentStoreCoordinatorStoresDidChangeNotification, nil, nil,
^(MPPasswordsViewController *self, NSNotification *note) {
PearlMainQueue( ^{
[self updatePasswords];
[self reloadPasswords];
[self registerObservers];
} );
} );
@@ -395,81 +363,40 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
[self.passwordCollectionView reloadData];
}
- (void)updatePasswords {
- (void)reloadPasswords {
NSManagedObjectID *activeUserOID = [MPiOSAppDelegate get].activeUserOID;
if (!activeUserOID) {
PearlMainQueue( ^{
self.passwordsSearchBar.text = nil;
[self.passwordCollectionView reloadData];
[self.passwordCollectionView setContentOffset:CGPointMake( 0, -self.passwordCollectionView.contentInset.top ) animated:YES];
} );
return;
}
static NSRegularExpression *fuzzyRE;
static dispatch_once_t once = 0;
dispatch_once( &once, ^{
fuzzyRE = [NSRegularExpression regularExpressionWithPattern:@"(.)" options:0 error:nil];
} );
NSString *queryString = self.query;
NSString *queryPattern;
if ([queryString length] < 13)
queryPattern = [queryString stringByReplacingMatchesOfExpression:fuzzyRE withTemplate:@"*$1*"];
else
// If query is too long, a wildcard per character makes the CoreData fetch take excessively long.
queryPattern = strf( @"*%@*", queryString );
NSMutableArray *fuzzyGroups = [NSMutableArray arrayWithCapacity:[queryString length]];
[fuzzyRE enumerateMatchesInString:queryString options:0 range:NSMakeRange( 0, queryString.length )
usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
[fuzzyGroups addObject:[queryString substringWithRange:result.range]];
}];
_fuzzyGroups = fuzzyGroups;
[self.fetchedResultsController.managedObjectContext performBlock:^{
NSArray *oldSectionInfos = [self.fetchedResultsController sections];
NSMutableArray *oldSections = [[NSMutableArray alloc] initWithCapacity:[oldSectionInfos count]];
for (id<NSFetchedResultsSectionInfo> sectionInfo in oldSectionInfos)
[oldSections addObject:[sectionInfo.objects copy]];
static NSRegularExpression *fuzzyRE;
static dispatch_once_t once = 0;
dispatch_once( &once, ^{
fuzzyRE = [NSRegularExpression regularExpressionWithPattern:@"(.)" options:0 error:nil];
} );
prof_new( @"updateSites" );
NSString *queryString = self.query;
NSString *queryPattern = [[queryString stringByReplacingMatchesOfExpression:fuzzyRE withTemplate:@"*$1"]
stringByAppendingString:@"*"];
prof_rewind( @"queryPattern" );
NSMutableArray *fuzzyGroups = [NSMutableArray new];
[fuzzyRE enumerateMatchesInString:queryString options:0 range:NSMakeRange( 0, queryString.length )
usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
[fuzzyGroups addObject:[queryString substringWithRange:result.range]];
}];
_fuzzyGroups = fuzzyGroups;
NSError *error = nil;
self.fetchedResultsController.fetchRequest.predicate =
[NSPredicate predicateWithFormat:@"(%@ == '' OR name LIKE[cd] %@) AND user == %@",
queryPattern, queryPattern, activeUserOID];
[NSPredicate predicateWithFormat:@"name LIKE[cd] %@ AND user == %@", queryPattern, [MPiOSAppDelegate get].activeUserOID];
if (![self.fetchedResultsController performFetch:&error])
err( @"Couldn't fetch sites: %@", [error fullDescription] );
PearlMainQueue( ^{
@try {
[self.passwordCollectionView performBatchUpdates:^{
[self updateTransientItem];
NSInteger fromSections = self.passwordCollectionView.numberOfSections;
NSInteger toSections = [self numberOfSectionsInCollectionView:self.passwordCollectionView];
for (NSInteger section = 0; section < MAX( toSections, fromSections ); ++section) {
if (section >= fromSections)
[self.passwordCollectionView insertSections:[NSIndexSet indexSetWithIndex:section]];
else if (section >= toSections)
[self.passwordCollectionView deleteSections:[NSIndexSet indexSetWithIndex:section]];
else if (section < [oldSections count])
[self.passwordCollectionView reloadItemsFromArray:oldSections[section]
toArray:[[self.fetchedResultsController sections][section] objects]
inSection:section];
else
[self.passwordCollectionView reloadSections:[NSIndexSet indexSetWithIndex:section]];
}
} completion:^(BOOL finished) {
if (finished)
[self.passwordCollectionView setContentOffset:CGPointMake( 0, -self.passwordCollectionView.contentInset.top )
animated:YES];
for (MPPasswordCell *cell in self.passwordCollectionView.visibleCells)
[cell setFuzzyGroups:_fuzzyGroups];
}];
}
@catch (NSException *exception) {
wrn( @"While updating password cells: %@", [exception fullDescription] );
[self.passwordCollectionView reloadData];
}
[self.passwordCollectionView updateDataSource:_passwordCollectionSections
toSections:[self createPasswordCollectionSections]
reloadItems:@[ MPTransientPasswordItem ] completion:^(BOOL finished) {
for (MPPasswordCell *cell in self.passwordCollectionView.visibleCells)
[cell setFuzzyGroups:_fuzzyGroups];
}];
} );
}];
}
@@ -484,15 +411,15 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
- (NSFetchedResultsController *)fetchedResultsController {
if (!_fetchedResultsController) {
_showTransientItem = NO;
[MPiOSAppDelegate managedObjectContextForMainThreadPerformBlockAndWait:^(NSManagedObjectContext *mainContext) {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPSiteEntity class] )];
fetchRequest.sortDescriptors = @[
[[NSSortDescriptor alloc] initWithKey:NSStringFromSelector( @selector( lastUsed ) ) ascending:NO]
];
fetchRequest.fetchBatchSize = 10;
_fetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest managedObjectContext:mainContext sectionNameKeyPath:nil cacheName:nil];
_fetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:mainContext
sectionNameKeyPath:nil cacheName:nil];
_fetchedResultsController.delegate = self;
}];
[self registerObservers];

View File

@@ -73,7 +73,7 @@ PearlEnum( MPDevelopmentFuelConsumption,
self.tableView.contentInset = UIEdgeInsetsMake( 64, 0, 49, 0 );
[self reloadCellsHiding:self.allCellsBySection[0] showing:@[ self.loadingCell ]];
[self updateCellsHiding:self.allCellsBySection[0] showing:@[ self.loadingCell ]];
[self.allCellsBySection[0] enumerateObjectsUsingBlock:^(MPStoreProductCell *cell, NSUInteger idx, BOOL *stop) {
if ([cell isKindOfClass:[MPStoreProductCell class]]) {
cell.purchasedIndicator.visible = NO;
@@ -257,7 +257,7 @@ PearlEnum( MPDevelopmentFuelConsumption,
}
[hideCells removeObjectsInArray:showCells];
[self reloadCellsHiding:hideCells showing:showCells];
[self updateCellsHiding:hideCells showing:showCells];
}
- (void)updateFuel {

View File

@@ -25,6 +25,7 @@
@property(nonatomic, strong) UIDocumentInteractionController *interactionController;
@property(nonatomic) UIBackgroundTaskIdentifier task;
@end
@implementation MPiOSAppDelegate
@@ -244,6 +245,44 @@
if (![[MPiOSConfig get].rememberLogin boolValue])
[self signOutAnimated:NO];
// self.task = [application beginBackgroundTaskWithExpirationHandler:^{
// [application endBackgroundTask:self.task];
// dbg( @"background expiring" );
// }];
// PearlNotMainQueueOperation( ^{
// NSString *pbstring = [UIPasteboard generalPasteboard].string;
// while (YES) {
// NSString *newString = [UIPasteboard generalPasteboard].string;
// if (![newString isEqualToString:pbstring]) {
// dbg( @"pasteboard changed to: %@", newString );
// pbstring = newString;
// NSURL *url = [NSURL URLWithString:pbstring];
// if (url) {
// NSString *siteName = [url host];
// }
// MPKey *key = [MPiOSAppDelegate get].key;
// if (key)
// [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
// NSFetchRequest<MPSiteEntity *>
// *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPSiteEntity class] )];
// fetchRequest.sortDescriptors = @[
// [[NSSortDescriptor alloc] initWithKey:NSStringFromSelector( @selector( lastUsed ) ) ascending:NO]
// ];
// fetchRequest.fetchBatchSize = 2;
// fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(name LIKE[cd] %@) AND user == %@", siteName,
// [[MPiOSAppDelegate get] activeUserOID]];
// NSError *error = nil;
// NSArray<MPSiteEntity *> *results = [fetchRequest execute:&error];
// dbg( @"site search, error: %@, results:\n%@", error, results );
// if ([results count]) {
// [UIPasteboard generalPasteboard].string = [[results firstObject] resolvePasswordUsingKey:key];
// }
// }];
// }
// [NSThread sleepForTimeInterval:5];
// }
// } );
[super applicationDidEnterBackground:application];
}

View File

@@ -526,7 +526,7 @@
<objects>
<navigationController definesPresentationContext="YES" navigationBarHidden="YES" id="Q1S-vU-GGO" customClass="MPNavigationController" sceneMemberID="viewController">
<simulatedStatusBarMetrics key="simulatedStatusBarMetrics" statusBarStyle="lightContent"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="4yl-zs-iUd">
<navigationBar key="navigationBar" contentMode="scaleToFill" misplaced="YES" id="4yl-zs-iUd">
<rect key="frame" x="0.0" y="0.0" width="1000" height="1000"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
@@ -1155,7 +1155,7 @@
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="K2e-Gh-7hH" userLabel="Passwords Container">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<subviews>
<collectionView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" minimumZoomScale="0.0" maximumZoomScale="0.0" keyboardDismissMode="onDrag" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="aXw-tn-8Sj" userLabel="Password Collection">
<collectionView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" minimumZoomScale="0.0" maximumZoomScale="0.0" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="aXw-tn-8Sj" userLabel="Password Collection">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<inset key="scrollIndicatorInsets" minX="0.0" minY="108" maxX="0.0" maxY="0.0"/>
@@ -1163,11 +1163,11 @@
<size key="itemSize" width="355" height="100"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="10" minY="118" maxX="10" maxY="10"/>
<inset key="sectionInset" minX="10" minY="10" maxX="10" maxY="10"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="MPPasswordCell" id="W2g-yv-V3V" customClass="MPPasswordCell">
<rect key="frame" x="10" y="118" width="355" height="100"/>
<rect key="frame" x="10" y="10" width="355" height="100"/>
<autoresizingMask key="autoresizingMask"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="355" height="100"/>
@@ -2022,7 +2022,7 @@ eg. apple.com, rmitchell@twitter.com</string>
<objects>
<navigationController definesPresentationContext="YES" id="LBn-EA-NAH" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Settings" image="icon_gears.png" id="6n3-Ay-Knn"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="h6j-1o-LHf">
<navigationBar key="navigationBar" contentMode="scaleToFill" misplaced="YES" id="h6j-1o-LHf">
<rect key="frame" x="0.0" y="0.0" width="1000" height="1000"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
@@ -2273,7 +2273,7 @@ Suspendisse potenti. Etiam ut nisi id augue tempor ultrices et sit amet sapien.
<objects>
<navigationController definesPresentationContext="YES" id="Ec4-G7-5Td" customClass="PearlNavigationController" sceneMemberID="viewController">
<extendedEdge key="edgesForExtendedLayout"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" barStyle="black" id="MA4-cY-VCZ">
<navigationBar key="navigationBar" contentMode="scaleToFill" misplaced="YES" barStyle="black" id="MA4-cY-VCZ">
<rect key="frame" x="0.0" y="0.0" width="1000" height="1000"/>
<autoresizingMask key="autoresizingMask"/>
<color key="tintColor" red="0.47450980390000003" green="0.86666666670000003" blue="0.98431372549999996" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>