git.y1.nz

SameBoy

Accurate GB/GBC emulator
download: https://git.y1.nz/archives/sameboy.tar.gz
README | Files | Log | Refs | LICENSE

iOS/GBPalettePicker.m

      1 #import "GBPalettePicker.h"
      2 #import "GBPaletteEditor.h"
      3 #import <CoreServices/CoreServices.h>
      4 
      5 /* TODO: Unify with Cocoa? */
      6 #define MAGIC 'SBPL'
      7 
      8 typedef struct __attribute__ ((packed)) {
      9     uint32_t magic;
     10     bool manual:1;
     11     bool disabled_lcd_color:1;
     12     unsigned padding:6;
     13     struct GB_color_s colors[5];
     14     int32_t brightness_bias;
     15     uint32_t hue_bias;
     16     uint32_t hue_bias_strength;
     17 } theme_t;
     18 
     19 
     20 @interface GBPalettePicker () <UIDocumentPickerDelegate>
     21 @end
     22 
     23 @implementation GBPalettePicker
     24 {
     25     NSArray <NSString *>* _cacheNames;
     26     NSIndexPath *_renamingPath;
     27     NSMutableSet *_tempFiles;
     28 }
     29 
     30 + (NSString *)makeUnique:(NSString *)name
     31 {
     32     NSArray *builtins = @[
     33         @"Greyscale",
     34         @"Lime (Game Boy)",
     35         @"Olive (Pocket)",
     36         @"Teal (Light)",
     37     ];
     38     
     39     NSDictionary *dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBThemes"];
     40     if (dict[name] || [builtins containsObject:name]) {
     41         unsigned i = 2;
     42         while (true) {
     43             NSString *attempt = [NSString stringWithFormat:@"%@ %u", name, i];
     44             if (!dict[attempt] && ![builtins containsObject:attempt]) {
     45                 return attempt;
     46             }
     47             i++;
     48         }
     49     }
     50     return name;
     51 }
     52 
     53 + (const GB_palette_t *)paletteForTheme:(NSString *)theme
     54 {
     55     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
     56     if ([theme isEqualToString:@"Greyscale"]) {
     57         return &GB_PALETTE_GREY;
     58     }
     59     if ([theme isEqualToString:@"Lime (Game Boy)"]) {
     60         return &GB_PALETTE_DMG;
     61     }
     62     if ([theme isEqualToString:@"Olive (Pocket)"]) {
     63         return &GB_PALETTE_MGB;
     64     }
     65     if ([theme isEqualToString:@"Teal (Light)"]) {
     66         return &GB_PALETTE_GBL;
     67     }
     68     static GB_palette_t customPalette;
     69     NSArray *colors = [defaults dictionaryForKey:@"GBThemes"][theme][@"Colors"];
     70     if (colors.count != 5) return &GB_PALETTE_DMG;
     71     unsigned i = 0;
     72     for (NSNumber *color in colors) {
     73         uint32_t c = [color unsignedIntValue];
     74         customPalette.colors[i++] = (struct GB_color_s) {c, c >> 8, c >> 16};
     75     }
     76     return &customPalette;
     77 }
     78 
     79 + (UIColor *)colorFromGBColor:(const struct GB_color_s *)color
     80 {
     81     return [UIColor colorWithRed:color->r / 255.0
     82                            green:color->g / 255.0
     83                             blue:color->b / 255.0
     84                            alpha:1.0];
     85 }
     86 
     87 + (UIImage *)previewImageForTheme:(NSString *)theme
     88 {
     89     const GB_palette_t *palette = [self paletteForTheme:theme];
     90     UIGraphicsBeginImageContextWithOptions((CGSize){29, 29}, false, [UIScreen mainScreen].scale);
     91     UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 29, 29) cornerRadius:7];
     92     [[self colorFromGBColor:&palette->colors[4]] set];
     93     [path fill];
     94     
     95     path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(4, 4, 9, 9) cornerRadius:2];
     96     [[self colorFromGBColor:&palette->colors[0]] set];
     97     [path fill];
     98     
     99     path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(16, 4, 9, 9) cornerRadius:2];
    100     [[self colorFromGBColor:&palette->colors[1]] set];
    101     [path fill];
    102     
    103     path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(4, 16, 9, 9) cornerRadius:2];
    104     [[self colorFromGBColor:&palette->colors[2]] set];
    105     [path fill];
    106     
    107     path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(16, 16, 9, 9) cornerRadius:2];
    108     [[self colorFromGBColor:&palette->colors[3]] set];
    109     [path fill];
    110     
    111     UIImage *ret = UIGraphicsGetImageFromCurrentImageContext();
    112     UIGraphicsEndImageContext();
    113     
    114     return ret;
    115 }
    116 
    117 - (instancetype)initWithStyle:(UITableViewStyle)style
    118 {
    119     self = [super initWithStyle:style];
    120     self.navigationItem.rightBarButtonItem = self.editButtonItem;
    121     _tempFiles = [NSMutableSet set];
    122     self.tableView.allowsSelectionDuringEditing = true;
    123     return self;
    124 }
    125 
    126 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
    127 {
    128     return 3;
    129 }
    130 
    131 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
    132 {
    133     if (section == 0) return 4;
    134     if (section == 2) return 3;
    135     _cacheNames = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBThemes"].allKeys sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
    136     return _cacheNames.count;
    137 }
    138 
    139 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    140 {
    141     UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:nil];
    142     
    143     NSString *name = nil;
    144     if (indexPath.section == 2) {
    145         switch (indexPath.row) {
    146             case 0:
    147                 cell.textLabel.text = @"New Palette";
    148                 break;
    149             case 1:
    150                 cell.textLabel.text = @"Import Palette";
    151                 break;
    152             case 2:
    153                 cell.textLabel.text = @"Restore Defaults";
    154                 cell.textLabel.textColor = [UIColor systemRedColor];
    155                 break;
    156         }
    157         return cell;
    158     }
    159     else if (indexPath.section == 0) {
    160         name = @[
    161             @"Greyscale",
    162             @"Lime (Game Boy)",
    163             @"Olive (Pocket)",
    164             @"Teal (Light)",
    165         ][indexPath.row];
    166     }
    167     else {
    168         name = _cacheNames[indexPath.row];
    169     }
    170     
    171     cell.textLabel.text = name;
    172     if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"] isEqual:name]) {
    173         cell.accessoryType = UITableViewCellAccessoryCheckmark;
    174     }
    175 
    176     cell.imageView.image = [self.class previewImageForTheme:name];
    177     return cell;
    178 
    179 }
    180 
    181 
    182 - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentAtURL:(NSURL *)url
    183 {
    184     [url startAccessingSecurityScopedResource];
    185     if ([self.class importPalette:url.path]) {
    186         [self.tableView reloadData];
    187     }
    188     else {
    189         UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Import Failed"
    190                                                                                  message:@"The imported palette file is invalid."
    191                                                                           preferredStyle:UIAlertControllerStyleAlert];
    192         [alertController addAction:[UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]];
    193         [self presentViewController:alertController animated:true completion:nil];
    194         return;
    195     }
    196     [url stopAccessingSecurityScopedResource];
    197 }
    198 
    199 - (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
    200 {
    201     if (self.editing) {
    202         if (indexPath.section != 1) return nil;
    203         if (@available(iOS 14.0, *)) {
    204             [self.navigationController pushViewController:[[GBPaletteEditor alloc] initForPalette:[self.tableView cellForRowAtIndexPath:indexPath].textLabel.text]
    205                                                  animated:true];
    206         }
    207         return nil;
    208     }
    209     if (indexPath.section == 2) {
    210         switch (indexPath.row) {
    211             case 0: {
    212                 if (@available(iOS 14.0, *)) {
    213                     NSString *name = [self.class makeUnique:@"Untitled Palette"];
    214                     NSMutableDictionary *dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBThemes"].mutableCopy;
    215                     srandom(time(NULL));
    216                     dict[name] = @{
    217                         @"BrightnessBias": @((random() & 0xFFF) / (double)0xFFF * 2 - 1),
    218                         @"Colors": @[@((random() & 0x3f3f3f) | 0xFF000000), @0, @0, @0, @((random() | 0xffc0c0c0))],
    219                         @"DisabledLCDColor": @YES,
    220                         @"HueBias": @((random() & 0xFFF) / (double)0xFFF),
    221                         @"HueBiasStrength": @((random() & 0xFFF) / (double)0xFFF),
    222                         @"Manual": @NO,
    223                     };
    224                     [[NSUserDefaults standardUserDefaults] setObject:dict forKey:@"GBThemes"];
    225                     [self.navigationController pushViewController:[[GBPaletteEditor alloc] initForPalette:name]
    226                                                          animated:true];
    227                 }
    228                 else {
    229                     UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Palette Editor Unavailable"
    230                                                                                              message:@"The palette editor requires iOS 14 or newer."
    231                                                                                       preferredStyle:UIAlertControllerStyleAlert];
    232                     [alertController addAction:[UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]];
    233                     [self presentViewController:alertController animated:true completion:nil];
    234                 }
    235                 break;
    236             }
    237             case 1: {
    238                 [self setEditing:false animated:true];
    239                 NSString *sbpUTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)@"sbp", NULL);
    240                 
    241                 
    242                 UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[sbpUTI]
    243                                                                                                                 inMode:UIDocumentPickerModeImport];
    244                 if (@available(iOS 13.0, *)) {
    245                     picker.shouldShowFileExtensions = true;
    246                 }
    247                 picker.delegate = self;
    248                 [self presentViewController:picker animated:true completion:nil];
    249                 break;
    250             }
    251             case 2: {
    252                 UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Restore default palettes?"
    253                                                                                message: @"The defaults palettes will be restored, changes will be reverted, and created or imported palettes will be deleted. This change cannot be undone."
    254                                                                         preferredStyle:UIAlertControllerStyleAlert];
    255                 [alert addAction:[UIAlertAction actionWithTitle:@"Restore"
    256                                                           style:UIAlertActionStyleDestructive
    257                                                         handler:^(UIAlertAction *action) {
    258                     [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"GBCurrentTheme"];
    259                     [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"GBThemes"];
    260                     [self.tableView reloadData];
    261                 }]];
    262                 [alert addAction:[UIAlertAction actionWithTitle:@"Cancel"
    263                                                           style:UIAlertActionStyleCancel
    264                                                         handler:nil]];
    265                 [self presentViewController:alert animated:true completion:nil];
    266                 break;
    267             }
    268         }
    269         return nil;
    270     }
    271     [[NSUserDefaults standardUserDefaults] setObject:[self.tableView cellForRowAtIndexPath:indexPath].textLabel.text
    272                                               forKey:@"GBCurrentTheme"];
    273     [self.tableView reloadData];
    274     return nil;
    275 }
    276 
    277 
    278 - (NSString *)title
    279 {
    280     return @"Monochrome Palette";
    281 }
    282 
    283 - (void)renameRow:(NSIndexPath *)indexPath
    284 {
    285     if (indexPath.section != 1) return;
    286     
    287     UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
    288     CGRect frame = cell.textLabel.frame;
    289     frame.size.width = cell.textLabel.superview.frame.size.width - 8 - frame.origin.x;
    290     UITextField *field = [[UITextField alloc] initWithFrame:frame];
    291     field.font = cell.textLabel.font;
    292     field.text = cell.textLabel.text;
    293     cell.textLabel.textColor = [UIColor clearColor];
    294     [[cell.textLabel superview] addSubview:field];
    295     [field becomeFirstResponder];
    296     [field selectAll:nil];
    297     _renamingPath = indexPath;
    298     [field addTarget:self action:@selector(doneRename:) forControlEvents:UIControlEventEditingDidEnd | UIControlEventEditingDidEndOnExit];
    299 }
    300 
    301 - (void)doneRename:(UITextField *)sender
    302 {
    303     if (!_renamingPath) return;
    304     NSString *newName = sender.text;
    305     NSString *oldName = [self.tableView cellForRowAtIndexPath:_renamingPath].textLabel.text;
    306     
    307     _renamingPath = nil;
    308     if ([newName isEqualToString:oldName]) {
    309         [self.tableView reloadData];
    310         return;
    311     }
    312     
    313     NSMutableDictionary *dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBThemes"].mutableCopy;
    314     newName = [self.class makeUnique:newName];
    315     
    316     dict[newName] = dict[oldName];
    317     [dict removeObjectForKey:oldName];
    318     [[NSUserDefaults standardUserDefaults] setObject:dict forKey:@"GBThemes"];
    319     if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"] isEqual:oldName]) {
    320         [[NSUserDefaults standardUserDefaults] setObject:newName forKey:@"GBCurrentTheme"];
    321     }
    322     [self.tableView reloadData];
    323     _renamingPath = nil;
    324 }
    325 
    326 
    327 - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
    328 {
    329     if (indexPath.section != 1) return;
    330     
    331     if (editingStyle != UITableViewCellEditingStyleDelete) return;
    332     NSString *rom = [self.tableView cellForRowAtIndexPath:indexPath].textLabel.text;
    333     UIAlertController *alert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"Delete “%@”?", rom]
    334                                                                    message: @"This change cannot be undone."
    335                                                             preferredStyle:UIAlertControllerStyleAlert];
    336     [alert addAction:[UIAlertAction actionWithTitle:@"Delete"
    337                                               style:UIAlertActionStyleDestructive
    338                                             handler:^(UIAlertAction *action) {
    339         NSMutableDictionary *dict = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBThemes"].mutableCopy;
    340         NSString *name = _cacheNames[indexPath.row];
    341         [dict removeObjectForKey:name];
    342         [[NSUserDefaults standardUserDefaults] setObject:dict forKey:@"GBThemes"];
    343         [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
    344 
    345         if ([[[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"] isEqual:name]) {
    346             [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"GBCurrentTheme"];
    347             [self.tableView reloadRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:1 inSection:0]] withRowAnimation:UITableViewRowAnimationFade];
    348         }
    349     }]];
    350     [alert addAction:[UIAlertAction actionWithTitle:@"Cancel"
    351                                               style:UIAlertActionStyleCancel
    352                                             handler:nil]];
    353     [self presentViewController:alert animated:true completion:nil];
    354 }
    355 
    356 - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
    357 {
    358     return indexPath.section == 1;
    359 }
    360 
    361 - (NSString *)exportTheme:(NSString *)name
    362 {
    363     theme_t theme = {0,};
    364     theme.magic = MAGIC;
    365     
    366     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    367     NSDictionary *themeDict = [defaults dictionaryForKey:@"GBThemes"][name];
    368     NSArray *colors = themeDict[@"Colors"];
    369     if (colors.count <= 5) {
    370         unsigned i = 0;
    371         for (NSNumber *color in colors) {
    372             uint32_t c = [color unsignedIntValue];
    373             theme.colors[i++] = (struct GB_color_s){
    374                 (c & 0xFF),
    375                 ((c >> 8) & 0xFF),
    376                 ((c >> 16) & 0xFF)};
    377         }
    378     }
    379     
    380     theme.manual = [themeDict[@"Manual"] boolValue];
    381     theme.disabled_lcd_color = [themeDict[@"DisabledLCDColor"] boolValue];
    382 
    383     theme.brightness_bias = ([themeDict[@"BrightnessBias"] doubleValue] * 0x40000000);
    384     theme.hue_bias = round([themeDict[@"HueBias"] doubleValue] * 0x80000000);
    385     theme.hue_bias_strength = round([themeDict[@"HueBiasStrength"] doubleValue] * 0x80000000);
    386     
    387     size_t size = sizeof(theme);
    388     if (theme.manual) {
    389         size = theme.disabled_lcd_color? 5 + 5 * sizeof(theme.colors[0]) : 5 + 4 * sizeof(theme.colors[0]);
    390     }
    391     
    392     NSString *path = [[NSTemporaryDirectory() stringByAppendingPathComponent:name] stringByAppendingPathExtension:@"sbp"];
    393     [[NSData dataWithBytes:&theme length:size] writeToFile:path atomically:false];
    394     [_tempFiles addObject:path];
    395     return path;
    396 }
    397 
    398 + (bool)importPalette:(NSString *)path
    399 {
    400     NSData *data = [NSData dataWithContentsOfFile:path];
    401     theme_t theme = {0,};
    402     memcpy(&theme, data.bytes, MIN(sizeof(theme), data.length));
    403     if (theme.magic != MAGIC) {
    404         return false;
    405     }
    406     
    407     NSMutableDictionary *themeDict = [NSMutableDictionary dictionary];
    408     themeDict[@"Manual"] = theme.manual? @YES : @NO;
    409     themeDict[@"DisabledLCDColor"] = theme.disabled_lcd_color? @YES : @NO;
    410     
    411 #define COLOR_TO_INT(color) ((color.r << 0) | (color.g << 8) | (color.b << 16) | 0xFF000000)
    412     themeDict[@"Colors"] = @[
    413         @(COLOR_TO_INT(theme.colors[0])),
    414         @(COLOR_TO_INT(theme.colors[1])),
    415         @(COLOR_TO_INT(theme.colors[2])),
    416         @(COLOR_TO_INT(theme.colors[3])),
    417         @(COLOR_TO_INT(theme.colors[theme.disabled_lcd_color? 4 : 3])),
    418     ];
    419 
    420     
    421     themeDict[@"BrightnessBias"] = @(theme.brightness_bias / (double)0x40000000);
    422     themeDict[@"HueBias"] = @(theme.hue_bias / (double)0x80000000);
    423     themeDict[@"HueBiasStrength"] = @(theme.hue_bias_strength / (double)0x80000000);
    424         
    425     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    426     NSMutableDictionary *themes = [defaults dictionaryForKey:@"GBThemes"].mutableCopy;
    427     NSString *baseName = path.lastPathComponent.stringByDeletingPathExtension;
    428     NSString *newName = [self.class makeUnique:baseName];
    429     themes[newName] = themeDict;
    430     [defaults setObject:themes forKey:@"GBThemes"];
    431     [defaults setObject:newName forKey:@"GBCurrentTheme"];
    432     return true;
    433 }
    434 
    435 - (UIContextMenuConfiguration *)tableView:(UITableView *)tableView
    436 contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
    437                                     point:(CGPoint)point API_AVAILABLE(ios(13.0))
    438 {
    439     if (indexPath.section != 1) return nil;
    440     
    441     return [UIContextMenuConfiguration configurationWithIdentifier:nil
    442                                                    previewProvider:nil
    443                                                     actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
    444         UIAction *deleteAction = [UIAction actionWithTitle:@"Delete"
    445                                                      image:[UIImage systemImageNamed:@"trash"]
    446                                                 identifier:nil
    447                                                    handler:^(UIAction *action) {
    448             [self tableView:tableView
    449          commitEditingStyle:UITableViewCellEditingStyleDelete
    450           forRowAtIndexPath:indexPath];
    451         }];
    452         
    453         deleteAction.attributes = UIMenuElementAttributesDestructive;
    454         return [UIMenu menuWithTitle:nil children:@[
    455             [UIAction actionWithTitle:@"Edit"
    456                                 image:[UIImage systemImageNamed:@"paintbrush"]
    457                            identifier:nil
    458                               handler:^(__kindof UIAction *action) {
    459                 if (@available(iOS 14.0, *)) {
    460                     [self.navigationController pushViewController:[[GBPaletteEditor alloc] initForPalette:[self.tableView cellForRowAtIndexPath:indexPath].textLabel.text]
    461                                                          animated:true];
    462                 }
    463                 else {
    464                     UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Palette Editor Unavailable"
    465                                                                                              message:@"The palette editor requires iOS 14 or newer."
    466                                                                                       preferredStyle:UIAlertControllerStyleAlert];
    467                     [alertController addAction:[UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]];
    468                     [self presentViewController:alertController animated:true completion:nil];
    469                     return;
    470                 }
    471             }],
    472             [UIAction actionWithTitle:@"Share"
    473                                 image:[UIImage systemImageNamed:@"square.and.arrow.up"]
    474                            identifier:nil
    475                               handler:^(__kindof UIAction *action) {
    476                 [self setEditing:false animated:true];
    477                 UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
    478                 NSString *file = [self exportTheme:cell.textLabel.text];
    479                 NSURL *url = [NSURL fileURLWithPath:file];
    480                 UIActivityViewController *controller = [[UIActivityViewController alloc] initWithActivityItems:@[url]
    481                                                                                          applicationActivities:nil];
    482                 
    483                 if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
    484                     controller.popoverPresentationController.sourceView = cell.contentView;
    485                 }
    486                 
    487                 [self presentViewController:controller
    488                                    animated:true
    489                                  completion:nil];
    490             }],
    491             [UIAction actionWithTitle:@"Rename"
    492                                 image:[UIImage systemImageNamed:@"pencil"]
    493                            identifier:nil
    494                               handler:^(__kindof UIAction *action) {
    495                 [self renameRow:indexPath];
    496             }],
    497             deleteAction,
    498         ]];
    499     }];
    500 }
    501 
    502 - (void)dealloc
    503 {
    504     for (NSString *file in _tempFiles) {
    505         [[NSFileManager defaultManager] removeItemAtPath:file error:nil];
    506     }
    507 }
    508 
    509 - (void)viewWillAppear:(BOOL)animated
    510 {
    511     [self.tableView reloadData];
    512     [super viewWillAppear:animated];
    513 }
    514 
    515 @end

This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.