Files
RyukGram/src/Settings/SCISettingsViewController.m
T
faroukbmiled 86eaa95019 [release] RyukGram v1.2.0
### Features
- **Open Instagram links in app (Safari extension)** — bundled Safari web extension (sideload IPA only). Enable in Safari → Extensions; instagram.com links open in the app.
- **Localization** — every user-facing string flows through a central translation layer. Globe button in Settings; missing keys fall back to English. Ships English only — see the "Translating RyukGram" section in the README to add more.
- **Action buttons** — context-aware menus on feed, reels, and stories (expand, repost, download, copy caption, etc.) with per-context default tap action and carousel/multi-story bulk download
- **Enhanced HD downloads** — up to 1080p via DASH + FFmpegKit with quality picker, preview playback, encoding-speed options, and 720p fallback
- **Repost**, **media viewer**, **media zoom** (long-press), **download pill** (frosted glass, stacks concurrent downloads)
- **Fake location** — overrides CoreLocation app-wide, map picker + saved presets, optional quick-toggle button on the Friends Map
- **Messages-only mode** — strips every tab except DM inbox + profile
- **Launch tab** — pick which tab the app opens to
- Full last active date in DMs — show full date instead of "Active 2h ago"
- Custom date format — 12 formats with per-surface toggles (feed, notes/comments/stories, DMs)
- Send files in DMs (experimental)
- View story mentions
- Hide suggested stories
- Story tray long-press actions — view HD profile picture from the tray menu
- Advance on story reply — auto-skip to next story after sending a reply or reaction
- Mark story as seen on reply or emoji reaction
- Hide metrics (likes, comments, shares counts)
- Hide messages tab
- Hide voice/video call buttons in DM thread header (independent toggles)
- Disable app haptics
- Disable reels tab refresh
- Disable disappearing messages mode in DMs
- Follow indicator — shows whether the profile user follows you
- Copy note text on long press
- Zoom profile photo — long press opens full-screen viewer
- Notes actions — copy text, download GIF/audio from notes long-press menu
- Confirm unfollow
- Feed refresh controls — disable background refresh, home button refresh, and home button scroll

### Improvements
- Default tap action: added copy URL, repost, and view mentions options; dynamic menu generation per context
- Settings pages reordered: General → Feed → Stories → Reels → Messages → Profile → Navigation → Saving → Confirmations
- Fake location picker: native Apple Maps-style UI (search, long-press to drop pin, current location)
- Liquid glass floating tab bar + dynamic sizing
- Upload audio: FFmpegKit re-encode + trim for any audio/video input
- Settings reorganized with per-context action button config; new Profile page
- Highlight cover: full-screen viewer replaces direct download
- Switched HD encoder to `h264_videotoolbox` (hardware) — no GPL FFmpegKit required
- Legacy long-press download deprecated (off by default), replaced by action buttons

### Fixes
- Hide suggested stories no longer removes followed users' stories on scroll
- Settings search bar transparency with liquid glass off; auto-deactivates on push
- HD download cancel: tapping pill aborts in-flight downloads + FFmpeg sessions cleanly
- Download pill stuck state on background/foreground, progress reset per download
- Disappearing messages mode confirmation not firing on swipe
- Detailed color picker not working on story draw `†`
- DM seen toggle menu not updating after tap
- Reel refresh confirmation appearing on first app launch `†`
- Reels action button displacing profile pictures on photo reels
- Disappearing DM media download (expand, share, save to Photos with progress pill)
- Carousel "Download all" not showing item count in feed
- Encoding speed setting being ignored for HD downloads
- Various upstream SCInsta merges (Meta AI hiding, suggested chats hiding, notes tray) — marked `†`

> `†` Merged from upstream [SCInsta](https://github.com/SoCuul/SCInsta) by SoCuul

### Credits
- Thanks to [@erupts0](https://github.com/erupts0) (John) for testing and feature suggestions
- Thanks to [@euoradan](https://t.me/euoradan) (Radan) for experimental Instagram feature flag research
- Safari extension forked/cleaned from [BillyCurtis/OpenInstagramSafariExtension](https://github.com/BillyCurtis/OpenInstagramSafariExtension)

### Known Issues
- Preserved unsent messages can't be removed via "Delete for you"; pull-to-refresh clears them (warning available in settings)
- "Delete for you" detection uses a ~2s window after the local action — a real unsend landing in that window may be missed (rare)
2026-04-16 03:03:30 +01:00

564 lines
23 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#import "SCISettingsViewController.h"
#import "SCISearchBarStyler.h"
static char rowStaticRef[] = "row";
@interface SCISettingsViewController () <UITableViewDataSource, UITableViewDelegate, UISearchResultsUpdating, UISearchControllerDelegate>
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, copy) NSArray *sections;
@property (nonatomic) BOOL reduceMargin;
@property (nonatomic, strong) UISearchController *searchController;
@property (nonatomic, copy) NSArray<NSDictionary *> *searchResults;
@property (nonatomic) BOOL isRoot;
@end
///
@implementation SCISettingsViewController
- (instancetype)initWithTitle:(NSString *)title sections:(NSArray *)sections reduceMargin:(BOOL)reduceMargin {
self = [super init];
if (self) {
self.title = title;
self.reduceMargin = reduceMargin;
self.isRoot = reduceMargin; // root call uses reduceMargin=YES
// Exclude development cells from release builds
NSMutableArray *mutableSections = [sections mutableCopy];
[mutableSections enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(NSDictionary *section, NSUInteger index, BOOL *stop) {
if ([section[@"header"] hasPrefix:@"_"] && [section[@"footer"] hasPrefix:@"_"]) {
if (![[SCIUtils IGVersionString] isEqualToString:@"0.0.0"]) {
[mutableSections removeObjectAtIndex:index];
}
}
else if ([section[@"header"] isEqualToString:@"Experimental"]) {
if (![[SCIUtils IGVersionString] hasSuffix:@"-dev"]) {
[mutableSections removeObjectAtIndex:index];
}
}
}];
self.sections = [mutableSections copy];
}
return self;
}
- (instancetype)init {
return [self initWithTitle:[SCITweakSettings title] sections:[SCITweakSettings sections] reduceMargin:YES];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationController.navigationBar.prefersLargeTitles = NO;
self.view.backgroundColor = UIColor.systemBackgroundColor;
self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStyleInsetGrouped];
self.tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.tableView.dataSource = self;
self.tableView.contentInset = UIEdgeInsetsMake(self.reduceMargin ? -30 : -10, 0, 0, 0);
self.tableView.delegate = self;
[self.view addSubview:self.tableView];
if (self.isRoot) {
UISearchController *sc = [[UISearchController alloc] initWithSearchResultsController:nil];
sc.searchResultsUpdater = self;
sc.delegate = self;
sc.obscuresBackgroundDuringPresentation = NO;
sc.searchBar.placeholder = SCILocalized(@"settings.search.placeholder");
self.navigationItem.searchController = sc;
self.navigationItem.hidesSearchBarWhenScrolling = NO;
if (![SCIUtils getBoolPref:@"liquid_glass_buttons"]) {
self.definesPresentationContext = YES;
}
self.searchController = sc;
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemClose
target:self action:@selector(sciDismissSettings)];
// Compact globe button — English is the only shipped language for now,
// so the tap shows an info alert instead of a picker. Re-enable the
// menu below once additional translations land.
UIImage *globe = [UIImage systemImageNamed:@"globe"];
UIBarButtonItem *langItem = [[UIBarButtonItem alloc] initWithImage:globe
style:UIBarButtonItemStylePlain
target:self
action:@selector(sciShowLanguageInfo)];
self.navigationItem.rightBarButtonItem = langItem;
}
}
- (void)sciShowLanguageInfo {
UIAlertController *alert = [UIAlertController
alertControllerWithTitle:SCILocalized(@"settings.language.title")
message:SCILocalized(@"settings.language.english_only")
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"settings.language.ok") style:UIAlertActionStyleCancel handler:nil]];
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"settings.language.help_translate") style:UIAlertActionStyleDefault
handler:^(__unused UIAlertAction *a) {
NSURL *url = [NSURL URLWithString:@"https://github.com/faroukbmiled/RyukGram#translating-ryukgram"];
if (url) [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
}]];
[self presentViewController:alert animated:YES completion:nil];
}
- (UIMenu *)sciBuildLanguageMenu {
NSString *current = [[NSUserDefaults standardUserDefaults] stringForKey:SCILanguagePrefKey] ?: @"system";
NSMutableArray<UIAction *> *actions = [NSMutableArray array];
for (NSDictionary<NSString *, NSString *> *lang in SCIAvailableLanguages()) {
NSString *code = lang[@"code"];
NSString *title = [code isEqualToString:@"system"]
? SCILocalized(@"settings.language.system")
: lang[@"native"];
UIAction *action = [UIAction actionWithTitle:title
image:nil
identifier:nil
handler:^(UIAction * _Nonnull a) {
NSString *prev = [[NSUserDefaults standardUserDefaults] stringForKey:SCILanguagePrefKey] ?: @"system";
if ([prev isEqualToString:code]) return;
[[NSUserDefaults standardUserDefaults] setObject:code forKey:SCILanguagePrefKey];
SCILocalizationReset();
[self sciApplyLanguageChange];
// Most IG-side hooks cache their labels at load time, so a full
// restart is the only way to flip every menu/button cleanly.
[SCIUtils showRestartConfirmation];
}];
action.state = [code isEqualToString:current] ? UIMenuElementStateOn : UIMenuElementStateOff;
[actions addObject:action];
}
return [UIMenu menuWithTitle:SCILocalized(@"settings.language.title") children:actions];
}
- (void)sciApplyLanguageChange {
// Root title + search placeholder reflect the new language immediately.
self.title = SCILocalized(@"settings.title");
self.searchController.searchBar.placeholder = SCILocalized(@"settings.search.placeholder");
if (self.navigationItem.rightBarButtonItem.menu) {
self.navigationItem.rightBarButtonItem.menu = [self sciBuildLanguageMenu];
}
[self.tableView reloadData];
// Features watching for runtime label refreshes (IG menu items, overlay
// buttons, toasts) can subscribe to this to re-read their strings.
[[NSNotificationCenter defaultCenter] postNotificationName:@"SCILanguageDidChange" object:nil];
}
- (void)sciDismissSettings {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.tableView reloadData];
[self sciStyleSearchBar];
}
- (void)sciStyleSearchBar { [SCISearchBarStyler styleSearchBar:self.searchController.searchBar]; }
- (void)willPresentSearchController:(UISearchController *)searchController { [self sciStyleSearchBar]; }
- (void)didPresentSearchController:(UISearchController *)searchController {
[self sciStyleSearchBar];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self sciStyleSearchBar];
});
}
#pragma mark - Search
- (BOOL)isSearching {
return self.searchController.isActive && self.searchController.searchBar.text.length > 0;
}
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
NSString *q = searchController.searchBar.text ?: @"";
if (q.length == 0) {
self.searchResults = @[];
} else {
NSMutableArray *out = [NSMutableArray array];
[self collectMatchingFromSections:self.sections breadcrumb:@"" query:q into:out];
self.searchResults = out;
}
[self.tableView reloadData];
}
- (void)collectMatchingFromSections:(NSArray *)sections
breadcrumb:(NSString *)breadcrumb
query:(NSString *)q
into:(NSMutableArray *)out
{
for (id sectionObj in sections) {
if (![sectionObj isKindOfClass:[NSDictionary class]]) continue;
NSDictionary *section = sectionObj;
NSString *header = section[@"header"] ?: @"";
NSArray *rows = section[@"rows"];
for (id rowObj in rows) {
if (![rowObj isKindOfClass:[SCISetting class]]) continue;
SCISetting *row = rowObj;
NSString *titleHay = row.title ?: @"";
NSString *subHay = row.subtitle ?: @"";
BOOL matches = [titleHay rangeOfString:q options:NSCaseInsensitiveSearch].location != NSNotFound
|| [subHay rangeOfString:q options:NSCaseInsensitiveSearch].location != NSNotFound;
if (matches) {
NSMutableString *crumb = [NSMutableString string];
if (breadcrumb.length) [crumb appendString:breadcrumb];
if (header.length) {
if (crumb.length) [crumb appendString:@" "];
[crumb appendString:header];
}
[out addObject:@{ @"setting": row, @"breadcrumb": crumb ?: @"" }];
}
if (row.navSections) {
NSString *child = breadcrumb.length
? [NSString stringWithFormat:@"%@ %@", breadcrumb, row.title ?: @""]
: (row.title ?: @"");
[self collectMatchingFromSections:row.navSections breadcrumb:child query:q into:out];
}
}
}
}
- (SCISetting *)settingForIndexPath:(NSIndexPath *)indexPath breadcrumbOut:(NSString **)outCrumb {
if ([self isSearching]) {
if (indexPath.row >= (NSInteger)self.searchResults.count) return nil;
NSDictionary *entry = self.searchResults[indexPath.row];
if (outCrumb) *outCrumb = entry[@"breadcrumb"];
return entry[@"setting"];
}
return self.sections[indexPath.section][@"rows"][indexPath.row];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// Without this the search bar strands itself as a floating bar on return.
if (![SCIUtils getBoolPref:@"liquid_glass_buttons"] && self.searchController.isActive) {
self.searchController.active = NO;
}
if (![[[NSUserDefaults standardUserDefaults] objectForKey:@"SCInstaFirstRun"] isEqualToString:SCIVersionString]) {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:SCILocalized(@"settings.firstrun.title")
message:SCILocalized(@"settings.firstrun.message")
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:SCILocalized(@"settings.firstrun.ok")
style:UIAlertActionStyleDefault
handler:nil]];
UIViewController *presenter = self.presentingViewController;
[presenter presentViewController:alert animated:YES completion:nil];
// Done with first-time setup for this version
[[NSUserDefaults standardUserDefaults] setValue:SCIVersionString forKey:@"SCInstaFirstRun"];
}
}
// MARK: - UITableViewDataSource
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *searchBreadcrumb = nil;
SCISetting *row = [self settingForIndexPath:indexPath breadcrumbOut:&searchBreadcrumb];
if (!row) return nil;
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
UIListContentConfiguration *cellContentConfig = cell.defaultContentConfiguration;
cellContentConfig.text = row.dynamicTitle ? row.dynamicTitle() : row.title;
// While searching, show the breadcrumb path instead of the row subtitle.
NSString *displaySubtitle = [self isSearching] && searchBreadcrumb.length ? searchBreadcrumb : row.subtitle;
if (displaySubtitle.length) {
cellContentConfig.secondaryText = displaySubtitle;
cellContentConfig.textToSecondaryTextVerticalPadding = 4.5;
}
// Icon
if (row.icon != nil) {
cellContentConfig.image = [row.icon image];
cellContentConfig.imageProperties.tintColor = row.icon.color;
}
// Image url
if (row.imageUrl != nil) {
[self loadImageFromURL:row.imageUrl atIndexPath:indexPath forTableView:tableView];
cellContentConfig.imageToTextPadding = 14;
}
switch (row.type) {
case SCITableCellStatic: {
cell.selectionStyle = UITableViewCellSelectionStyleNone;
break;
}
case SCITableCellLink: {
cellContentConfig.textProperties.color = [UIColor systemBlueColor];
cellContentConfig.textProperties.font = [UIFont systemFontOfSize:[UIFont preferredFontForTextStyle:UIFontTextStyleBody].pointSize
weight:UIFontWeightMedium];
cell.selectionStyle = UITableViewCellSelectionStyleDefault;
UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage systemImageNamed:@"safari"]];
imageView.tintColor = [UIColor systemGray3Color];
cell.accessoryView = imageView;
break;
}
case SCITableCellSwitch: {
UISwitch *toggle = [UISwitch new];
toggle.on = row.disabled ? NO : [[NSUserDefaults standardUserDefaults] boolForKey:row.defaultsKey];
toggle.onTintColor = [SCIUtils SCIColor_Primary];
toggle.enabled = !row.disabled;
objc_setAssociatedObject(toggle, rowStaticRef, row, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[toggle addTarget:self action:@selector(switchChanged:) forControlEvents:UIControlEventValueChanged];
cell.accessoryView = toggle;
cell.selectionStyle = UITableViewCellSelectionStyleNone;
if (row.disabled) {
cell.contentView.alpha = 0.4;
}
break;
}
case SCITableCellStepper: {
UIStepper *stepper = [UIStepper new];
stepper.minimumValue = row.min;
stepper.maximumValue = row.max;
stepper.stepValue = row.step;
stepper.value = [[NSUserDefaults standardUserDefaults] doubleForKey:row.defaultsKey];
objc_setAssociatedObject(stepper, rowStaticRef, row, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[stepper addTarget:self
action:@selector(stepperChanged:)
forControlEvents:UIControlEventValueChanged];
// Template subtitle
if (row.subtitle.length) {
cellContentConfig.secondaryText = [self formatString:row.subtitle withValue:stepper.value label:row.label singularLabel:row.singularLabel];
}
cell.accessoryView = stepper;
cell.selectionStyle = UITableViewCellSelectionStyleNone;
break;
}
case SCITableCellButton: {
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
break;
}
case SCITableCellMenu: {
UIButton *menuButton = [UIButton buttonWithType:UIButtonTypeSystem];
[menuButton setTitle:@"•••" forState:UIControlStateNormal];
menuButton.menu = [row menuForButton:menuButton];
menuButton.showsMenuAsPrimaryAction = YES;
menuButton.titleLabel.font = [UIFont systemFontOfSize:[UIFont preferredFontForTextStyle:UIFontTextStyleBody].pointSize
weight:UIFontWeightMedium];
UIButtonConfiguration *config = menuButton.configuration ?: [UIButtonConfiguration plainButtonConfiguration];
menuButton.configuration.contentInsets = NSDirectionalEdgeInsetsMake(8, 8, 8, 8);
menuButton.configuration = config;
[menuButton sizeToFit];
cell.accessoryView = menuButton;
cell.selectionStyle = UITableViewCellSelectionStyleNone;
if (row.disabled) {
menuButton.enabled = NO;
cell.contentView.alpha = 0.4;
}
break;
}
case SCITableCellNavigation: {
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
break;
}
}
cell.contentConfiguration = cellContentConfig;
return cell;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if ([self isSearching]) return self.searchResults.count;
return [self.sections[section][@"rows"] count];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
if ([self isSearching]) {
NSUInteger n = self.searchResults.count;
if (n == 0) return SCILocalized(@"settings.results.none");
NSString *fmt = n == 1 ? SCILocalized(@"settings.results.one") : SCILocalized(@"settings.results.many");
return [NSString stringWithFormat:fmt, (unsigned long)n];
}
return self.sections[section][@"header"];
}
- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section {
if ([self isSearching]) return nil;
return self.sections[section][@"footer"];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
if ([self isSearching]) return 1;
return self.sections.count;
}
// MARK: - UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
SCISetting *row = [self settingForIndexPath:indexPath breadcrumbOut:NULL];
if (!row) return;
if (row.type == SCITableCellLink) {
[[UIApplication sharedApplication] openURL:row.url options:@{} completionHandler:nil];
}
else if (row.type == SCITableCellButton) {
if (row.action != nil) {
row.action();
}
}
else if (row.type == SCITableCellNavigation) {
if (row.navSections.count > 0) {
UIViewController *vc = [[SCISettingsViewController alloc] initWithTitle:row.title sections:row.navSections reduceMargin:NO];
vc.title = row.title;
[self.navigationController pushViewController:vc animated:YES];
}
else if (row.navViewController) {
[self.navigationController pushViewController:row.navViewController animated:YES];
}
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
// MARK: - Actions
- (void)switchChanged:(UISwitch *)sender {
SCISetting *row = objc_getAssociatedObject(sender, rowStaticRef);
[[NSUserDefaults standardUserDefaults] setBool:sender.isOn forKey:row.defaultsKey];
NSLog(@"Switch changed: %@", sender.isOn ? @"ON" : @"OFF");
if (row.requiresRestart) {
[SCIUtils showRestartConfirmation];
}
if ([row.defaultsKey isEqualToString:@"hide_suggested_stories"]) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"SCISuggestedStoriesReload" object:nil];
}
}
- (void)stepperChanged:(UIStepper *)sender {
SCISetting *row = objc_getAssociatedObject(sender, rowStaticRef);
[[NSUserDefaults standardUserDefaults] setDouble:sender.value forKey:row.defaultsKey];
NSLog(@"Stepper changed: %f", sender.value);
[self reloadCellForView:sender];
}
- (void)menuChanged:(UICommand *)command {
NSDictionary *properties = command.propertyList;
[[NSUserDefaults standardUserDefaults] setValue:properties[@"value"] forKey:properties[@"defaultsKey"]];
NSLog(@"Menu changed: %@", command.propertyList[@"value"]);
[self reloadCellForView:command.sender animated:YES];
[self.tableView reloadData];
if (properties[@"requiresRestart"]) {
[SCIUtils showRestartConfirmation];
}
}
// MARK: - Helper
- (NSString *)formatString:(NSString *)template withValue:(double)value label:(NSString *)label singularLabel:(NSString *)singularLabel {
// Singular or plural labels
NSString *applicableLabel = fabs(value - 1.0) < 0.00001 ? singularLabel : label;
// Force value to 0 to prevent it being -0
if (fabs(value) < 0.00001) {
value = 0.0;
}
// Get correct decimal value based on step value
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
formatter.numberStyle = NSNumberFormatterDecimalStyle;
formatter.minimumFractionDigits = 0;
formatter.maximumFractionDigits = [SCIUtils decimalPlacesInDouble:value];
NSString *stringValue = [formatter stringFromNumber:@(value)];
return [NSString stringWithFormat:template, stringValue, applicableLabel];
}
- (void)reloadCellForView:(UIView *)view animated:(BOOL)animated {
UITableViewCell *cell = (UITableViewCell *)view.superview;
while (cell && ![cell isKindOfClass:[UITableViewCell class]]) {
cell = (UITableViewCell *)cell.superview;
}
if (!cell) return;
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
if (!indexPath) return;
[self.tableView reloadRowsAtIndexPaths:@[indexPath]
withRowAnimation:animated ? UITableViewRowAnimationAutomatic : UITableViewRowAnimationNone];
}
- (void)reloadCellForView:(UIView *)view {
[self reloadCellForView:view animated:NO];
}
- (void)loadImageFromURL:(NSURL *)url atIndexPath:(NSIndexPath *)indexPath forTableView:(UITableView *)tableView
{
if (!url) return;
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
{
if (!data || error) return;
UIImage *image = [UIImage imageWithData:data];
if (!image) return;
dispatch_async(dispatch_get_main_queue(), ^{
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
if (!cell) return;
UIListContentConfiguration *config = (UIListContentConfiguration *)cell.contentConfiguration;
config.image = image;
config.imageProperties.maximumSize = CGSizeMake(45, 45);
cell.contentConfiguration = config;
});
}];
[task resume];
}
@end