diff --git a/Animated Banner/README.md b/Animated Banner/README.md new file mode 100644 index 0000000..03618c7 --- /dev/null +++ b/Animated Banner/README.md @@ -0,0 +1,89 @@ +# Animated Banner +Great-looking animated banner for your preferences + +## Preview +
+ Showcase +
Showcase of the banner
+
+ +## Features +- Simple but nice +- Looks good in both light and dark mode +- Endlessly customizable with animations and gradient +- Stays always vertically centered to fit the space +- Dynamically enabled/disabled by a specifier + +## Usage +### Basic implementation +```objective-c +- (void)viewDidLoad { + [super viewDidLoad]; + + NSArray *myUIColorArray = @[ + [UIColor redColor], + [UIColor blueColor] + ]; + CGFloat navbarOffset = 64; // default navigation bar height for non-notched devices. Required for scroll animation. Can be obtain with: + // self.view.window.windowScene.statusBarManager.statusBarFrame.size.height + self.realNavigationController.navigationBar.frame.size.height + + self.table.tableHeaderView = [[XXXAnimatedBanner alloc] initWithTitle:@"Title" subtitle:@"v1.0" colors:myUIColorArray defaultOffset:-navbarOffset]; +} +``` + +### Animate on scroll +```objective-c +- (void)scrollViewDidScroll:(UIScrollView *)scrollView { + if ([self.table.tableHeaderView isKindOfClass:[SPDAnimatedBanner class]]) { + [(SPDAnimatedBanner *)self.table.tableHeaderView adjustStackPositionToScrollOffset:scrollView.contentOffset.y]; + } +} +``` + +### Tip 1: dynamically enable/disable +```objective-c +- (void)setPreferenceValue:(id)value specifier:(PSSpecifier *)specifier { + [super setPreferenceValue:value specifier:specifier]; + + if ([specifier.properties[@"key"] isEqualToString:@"enabled"] && [self.table.tableHeaderView isKindOfClass:[SPDAnimatedBanner class]]) { + ((SPDAnimatedBanner *)self.table.tableHeaderView).activateGradient = [value boolValue]; + } +} +``` + +### Tip 2: relaunch animations when they get stopped by iOS +```objective-c +- (void)viewDidLoad { + [super viewDidLoad]; + + self.table.tableHeaderView = [[XXXAnimatedBanner alloc] initWithTitle:...]; + [self refreshBanner]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshBanner) name:UIApplicationDidBecomeActiveNotification object:nil]; // revive after app closed and reopened +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + if (!self.isMovingToParentViewController) [self refreshBanner]; // revive after going in sub pages +} + +- (void)refreshBanner { + if ([self.table.tableHeaderView isKindOfClass:[SPDAnimatedBanner class]]) { + ((SPDAnimatedBanner *)self.table.tableHeaderView).activateGradient = yourSwitchValue; + } +} +``` + +## Gallery +
+ Animatable +
Animatable
+
+
+ Light and Dark modes +
Gorgeous in light or dark
+
+
+ Togglable +
Can be controlled from a specifier
+
diff --git a/Animated Banner/XXXAnimatedBanner.h b/Animated Banner/XXXAnimatedBanner.h new file mode 100644 index 0000000..efdcf8b --- /dev/null +++ b/Animated Banner/XXXAnimatedBanner.h @@ -0,0 +1,15 @@ +#import + +@interface XXXAnimatedBanner : UIView +@property(nonatomic, strong) UILabel *titleLabel; +@property(nonatomic, strong) UIView *gradientView; +@property(nonatomic, strong) UILabel *subtitleLabel; +@property(nonatomic, strong) CAGradientLayer *gradient; +@property(nonatomic, strong) NSMutableArray *animations; +@property(nonatomic, strong) NSMutableArray *cgcolors; +@property(nonatomic) CGFloat defaultOffset; +@property(nonatomic, strong) NSLayoutConstraint *stackCenter; +@property(nonatomic, assign, getter=isGradientActivated) BOOL activateGradient; +- (instancetype)initWithTitle:(NSString *)title subtitle:(NSString *)subtitle colors:(NSArray<__kindof UIColor *> *)colors defaultOffset:(CGFloat)offset; +- (void)adjustStackPositionToScrollOffset:(CGFloat)offset; +@end diff --git a/Animated Banner/XXXAnimatedBanner.m b/Animated Banner/XXXAnimatedBanner.m new file mode 100644 index 0000000..4e265c4 --- /dev/null +++ b/Animated Banner/XXXAnimatedBanner.m @@ -0,0 +1,127 @@ +#import "XXXAnimatedBanner.h" + +@implementation XXXAnimatedBanner + +- (instancetype)initWithTitle:(NSString *)title subtitle:(NSString *)subtitle colors:(NSArray<__kindof UIColor *> *)colors defaultOffset:(CGFloat)defaultOffset { + if (self = [super initWithFrame:CGRectMake(0, 0, self.superview.frame.size.width, 200)]) { + // Labels + self.titleLabel = [[UILabel alloc] init]; + self.titleLabel.text = title; + self.titleLabel.font = [UIFont boldSystemFontOfSize:42.f]; + self.titleLabel.lineBreakMode = NSLineBreakByWordWrapping; + [self.titleLabel sizeToFit]; + // CGPoint titleFrameAnchor = CGPointMake(CGRectGetMinX(self.titleLabel.frame), CGRectGetMaxY(self.titleLabel.frame)); + // self.titleLabel.layer.anchorPoint = CGPointMake(0, 1); + // self.titleLabel.layer.position = titleFrameAnchor; // ~ top center + + self.subtitleLabel = [[UILabel alloc] init]; + self.subtitleLabel.text = subtitle; + self.subtitleLabel.font = [UIFont systemFontOfSize:24.f weight:UIFontWeightMedium]; + if (@available(iOS 13.0, *)) { + self.subtitleLabel.textColor = [UIColor secondaryLabelColor]; + } else { + self.subtitleLabel.textColor = [UIColor systemGrayColor]; + } + self.subtitleLabel.lineBreakMode = NSLineBreakByWordWrapping; + [self.subtitleLabel sizeToFit]; + // CGPoint subtitleFrameAnchor = CGPointMake(CGRectGetMinX(self.subtitleLabel.frame), CGRectGetMinY(self.subtitleLabel.frame)); + // self.subtitleLabel.layer.anchorPoint = CGPointMake(0, 0); + // self.subtitleLabel.layer.position = subtitleFrameAnchor; // bottom center + + // Create gradient + self.gradient = [CAGradientLayer layer]; + self.gradient.frame = self.titleLabel.frame; + self.cgcolors = [NSMutableArray new]; + for (int i = 0; i < colors.count; i++) { + self.cgcolors[i] = (id)colors[i].CGColor; + } + self.gradient.colors = self.cgcolors; + self.gradient.locations = @[@0, @1]; + + self.gradientView = [[UIView alloc] init]; + [self.gradientView.layer addSublayer:self.gradient]; + self.gradientView.maskView = self.titleLabel; + // CGPoint gradientFrameAnchor = CGPointMake(CGRectGetMinX(self.gradientView.frame), CGRectGetMaxY(self.gradientView.frame)); + // self.gradientView.layer.anchorPoint = CGPointMake(0, 1); + // self.gradientView.layer.position = gradientFrameAnchor; + + // Animations + self.animations = [NSMutableArray new]; + + CABasicAnimation *locations = [CABasicAnimation animationWithKeyPath:@"locations"]; + locations.fromValue = @[@0, @0]; + locations.toValue = @[@0, @1]; + locations.autoreverses = YES; + locations.repeatCount = FLT_MAX; + locations.duration = 5.0; + [self.animations addObject:locations]; + + CABasicAnimation *opacity = [CABasicAnimation animationWithKeyPath:@"opacity"]; + opacity.fromValue = @.75; + opacity.toValue = @1; + opacity.autoreverses = YES; + opacity.repeatCount = FLT_MAX; + opacity.duration = 3.0; + [self.animations addObject:opacity]; + + // Stack + UIStackView *stackView = [[UIStackView alloc] initWithArrangedSubviews:@[self.gradientView, self.subtitleLabel]]; + stackView.alignment = UIStackViewAlignmentLeading; + stackView.axis = UILayoutConstraintAxisVertical; + stackView.distribution = UIStackViewDistributionEqualCentering; + stackView.spacing = 0; + stackView.translatesAutoresizingMaskIntoConstraints = NO; + [self addSubview:stackView]; + + [NSLayoutConstraint activateConstraints:@[ + [stackView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor constant:25], + self.stackCenter = [stackView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor constant:0], + [stackView.heightAnchor constraintEqualToConstant:self.titleLabel.frame.size.height + self.subtitleLabel.frame.size.height], + + [self.gradientView.widthAnchor constraintEqualToConstant:self.titleLabel.frame.size.width], + [self.gradientView.heightAnchor constraintEqualToConstant:self.titleLabel.frame.size.height] + ]]; + + self.defaultOffset = defaultOffset; + } + + return self; +} + +- (void)setActivateGradient:(BOOL)activated { + _activateGradient = activated; + + // I would like to animate transition between activated and not but nvm + if (activated) { + self.gradient.colors = self.cgcolors; + for (CAAnimation *animation in self.animations) { + [self.gradient addAnimation:animation forKey:nil]; + } + } else { + if (@available(iOS 13.0, *)) { + self.gradient.colors = @[(id)[UIColor labelColor].CGColor, (id)[UIColor labelColor].CGColor]; + } else { + self.gradient.colors = @[(id)[UIColor blackColor].CGColor, (id)[UIColor blackColor].CGColor]; + } + [self.gradient removeAllAnimations]; + } +} + +- (void)adjustStackPositionToScrollOffset:(CGFloat)offset { + // Smooth scroll effect + if (offset < self.defaultOffset) { + CGFloat space = self.defaultOffset - offset; + self.stackCenter.constant = -space / 2; + // This + comments above are to make the text a bit bigger on scrolling + // pretty much like the stock iOS behavior with big titles but I can't + // manage to make it look good so I gave up + /* if (space < 100) { + CGFloat titleScale = 1 + (space / 750); + CGFloat subtitleScale = 1 + (space / 1000); + self.titleLabel.transform = CGAffineTransformMakeScale(titleScale, titleScale); + self.subtitleLabel.transform = CGAffineTransformMakeScale(subtitleScale, subtitleScale); + } */ + } +} + +@end diff --git a/Animated Banner/images/animated.gif b/Animated Banner/images/animated.gif new file mode 100644 index 0000000..1f17896 Binary files /dev/null and b/Animated Banner/images/animated.gif differ diff --git a/Animated Banner/images/light_and_dark.gif b/Animated Banner/images/light_and_dark.gif new file mode 100644 index 0000000..f3c61d8 Binary files /dev/null and b/Animated Banner/images/light_and_dark.gif differ diff --git a/Animated Banner/images/showcase.gif b/Animated Banner/images/showcase.gif new file mode 100644 index 0000000..2a5c4de Binary files /dev/null and b/Animated Banner/images/showcase.gif differ diff --git a/Animated Banner/images/togglable.gif b/Animated Banner/images/togglable.gif new file mode 100644 index 0000000..e0d6ece Binary files /dev/null and b/Animated Banner/images/togglable.gif differ diff --git a/Animated Title View/XXXAnimatedTitleView.h b/Animated Title View/XXXAnimatedTitleView.h index a4d4321..64332f2 100644 --- a/Animated Title View/XXXAnimatedTitleView.h +++ b/Animated Title View/XXXAnimatedTitleView.h @@ -1,6 +1,13 @@ #import -@interface LBMAnimatedTitleView : UIView --(instancetype)initWithTitle:(NSString *)title minimumScrollOffsetRequired:(CGFloat)minimumOffset; --(void)adjustLabelPositionToScrollOffset:(CGFloat)offset; +@interface XXXAnimatedTitleView : UIView { + NSLayoutConstraint *labelYConstraint; + NSLayoutConstraint *iconYConstraint; + CGFloat minimumOffsetRequired; +} +@property (nonatomic, strong, readonly) UIImageView *iconImageView; +@property (nonatomic, strong, readonly) UILabel *titleLabel; +- (instancetype)initWithTitle:(NSString *)title minimumScrollOffsetRequired:(CGFloat)minimumOffset; +- (instancetype)initWithTitle:(NSString *)title image:(_Nullable UIImage *)image minimumScrollOffsetRequired:(CGFloat)minimumOffset; +- (void)adjustItemsPositionToScrollOffset:(CGFloat)offset; @end diff --git a/Animated Title View/XXXAnimatedTitleView.m b/Animated Title View/XXXAnimatedTitleView.m index 69b8e66..152f3bd 100644 --- a/Animated Title View/XXXAnimatedTitleView.m +++ b/Animated Title View/XXXAnimatedTitleView.m @@ -1,50 +1,60 @@ #import "XXXAnimatedTitleView.h" -@implementation XXXAnimatedTitleView { - UILabel *_titleLabel; - NSLayoutConstraint *_yConstraint; - CGFloat _minimumOffsetRequired; +@implementation XXXAnimatedTitleView + +- (instancetype)initWithTitle:(NSString *)title minimumScrollOffsetRequired:(CGFloat)minimumOffset { + return [self initWithTitle:title image:nil minimumScrollOffsetRequired:minimumOffset]; +} + +- (instancetype)initWithTitle:(NSString *)title image:(_Nullable UIImage *)image minimumScrollOffsetRequired:(CGFloat)minimumOffset { + if (self = [super init]) { + self.superview.clipsToBounds = YES; + + _titleLabel = [[UILabel alloc] init]; + _titleLabel.text = title; + _titleLabel.textAlignment = NSTextAlignmentCenter; + if (@available(iOS 13.0, *)) { + _titleLabel.textColor = [UIColor labelColor]; + } else { + _titleLabel.textColor = [UIColor blackColor]; + } + _titleLabel.font = [UIFont systemFontOfSize:[UIFont labelFontSize] weight:UIFontWeightSemibold]; + _titleLabel.translatesAutoresizingMaskIntoConstraints = NO; + [_titleLabel sizeToFit]; + + _iconImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; + _iconImageView.image = image; + _iconImageView.contentMode = UIViewContentModeScaleAspectFit; + _iconImageView.translatesAutoresizingMaskIntoConstraints = NO; + + [self addSubview:_titleLabel]; + [self addSubview:_iconImageView]; + + [NSLayoutConstraint activateConstraints:@[ + [self.widthAnchor constraintEqualToAnchor:_titleLabel.widthAnchor], + [self.heightAnchor constraintEqualToAnchor:_titleLabel.heightAnchor], + [_titleLabel.centerXAnchor constraintEqualToAnchor:self.centerXAnchor], + labelYConstraint = [_titleLabel.centerYAnchor constraintEqualToAnchor:self.centerYAnchor constant:50], + + [_iconImageView.centerXAnchor constraintEqualToAnchor:self.centerXAnchor], + iconYConstraint = [_iconImageView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor constant:0] + ]]; + + minimumOffsetRequired = minimumOffset; + } + + return self; } - -(instancetype)initWithTitle:(NSString *)title minimumScrollOffsetRequired:(CGFloat)minimumOffset { - if([super init]) { - self.superview.clipsToBounds = YES; - - _titleLabel = [[UILabel alloc] init]; - _titleLabel.text = title; - _titleLabel.textAlignment = NSTextAlignmentCenter; - _titleLabel.textColor = [UIColor whiteColor]; - _titleLabel.font = [UIFont systemFontOfSize:17 weight:UIFontWeightHeavy]; - _titleLabel.translatesAutoresizingMaskIntoConstraints = NO; - [_titleLabel sizeToFit]; - - [self addSubview:_titleLabel]; - - [NSLayoutConstraint activateConstraints:@[ - [self.widthAnchor constraintEqualToAnchor:_titleLabel.widthAnchor], - [self.heightAnchor constraintEqualToAnchor:_titleLabel.heightAnchor], - - [_titleLabel.centerXAnchor constraintEqualToAnchor:self.centerXAnchor], - _yConstraint = [_titleLabel.centerYAnchor constraintEqualToAnchor:self.centerYAnchor constant:100], - ]]; - - _minimumOffsetRequired = minimumOffset; - } - - return self; - } - - -(void)adjustLabelPositionToScrollOffset:(CGFloat)offset { - CGFloat adjustment = 100 - (offset - _minimumOffsetRequired); - if(offset >= _minimumOffsetRequired) { - if(adjustment <= 0) { - _yConstraint.constant = 0; - } else { - _yConstraint.constant = adjustment; - } - - } else { - _yConstraint.constant = -100; - } - } +- (void)adjustItemsPositionToScrollOffset:(CGFloat)offset { + CGFloat adjustment = 50 - (offset - minimumOffsetRequired); + if (offset >= minimumOffsetRequired) { + labelYConstraint.constant = adjustment <= 0 ? 0 : adjustment; + iconYConstraint.constant = adjustment <= 40 ? adjustment <= -10 ? -50 : adjustment - 40 : 0; // don't ask me i don't know + } else { + labelYConstraint.constant = 50; + iconYConstraint.constant = 0; + } + _iconImageView.alpha = labelYConstraint.constant / 50; +} @end diff --git a/Animated Title View/XXXRootListController.m b/Animated Title View/XXXRootListController.m index 89439ad..f298dc4 100644 --- a/Animated Title View/XXXRootListController.m +++ b/Animated Title View/XXXRootListController.m @@ -1,18 +1,17 @@ --(void)viewDidAppear:(BOOL)animated { +- (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; - //Create view and set as titleView of your navigation bar - //Set the title and the minimum scroll offset before starting the animation - if(!self.navigationItem.titleView) { + // Create view and set as titleView of your navigation bar + // Set the title and the minimum scroll offset before starting the animation + if (!self.navigationItem.titleView) { LBMAnimatedTitleView *titleView = [[LBMAnimatedTitleView alloc] initWithTitle:@"Title" minimumScrollOffsetRequired:100]; self.navigationItem.titleView = titleView; } } --(void)scrollViewDidScroll:(UIScrollView *)scrollView { - - //Send scroll offset updates to view - if([self.navigationItem.titleView respondsToSelector:@selector(adjustLabelPositionToScrollOffset:)]) { +- (void)scrollViewDidScroll:(UIScrollView *)scrollView { + // Send scroll offset updates to view + if ([self.navigationItem.titleView respondsToSelector:@selector(adjustLabelPositionToScrollOffset:)]) { [(XXXAnimatedTitleView *)self.navigationItem.titleView adjustLabelPositionToScrollOffset:scrollView.contentOffset.y]; } } diff --git a/Animated Title View/with_icon.gif b/Animated Title View/with_icon.gif new file mode 100644 index 0000000..9b07a00 Binary files /dev/null and b/Animated Title View/with_icon.gif differ diff --git a/Group Cell Header with Button/README.md b/Group Cell Header with Button/README.md new file mode 100644 index 0000000..6f761f5 --- /dev/null +++ b/Group Cell Header with Button/README.md @@ -0,0 +1,43 @@ +# Group Cell Header With Button +`PSGroupCell` with a button at the opposite of the header title + +## Preview +
+ Preview +
Preview of the PSGroupCell
+
+ +## Features +- Native look +- Works with RTL +- Easily usable in a `.plist` + +## Usage +```plist + + cell + PSGroupCell + headerCellClass + XXXRefreshableHeaderCell + label + My label + actionLabel + Refresh + action + buttonAction: + +``` +You need to implement the method called by the button: +```objective-c +// MyController.m + +- (void)buttonAction:(id)sender { + // Actions after button triggering + // Note that the ':(id)sender' part + // isn't mandatory if you omit the ':' + // when declaring the action in the plist +} +``` + +## Known issues +- The cell will disappear if the table or one of its section is reloaded. It's an issue affecting every subclass of `PSTableCell `. See issue [here](https://github.com/hbang/libcephei/issues/53). diff --git a/Group Cell Header with Button/XXXRefreshableHeaderCell.h b/Group Cell Header with Button/XXXRefreshableHeaderCell.h new file mode 100644 index 0000000..fa7b97d --- /dev/null +++ b/Group Cell Header with Button/XXXRefreshableHeaderCell.h @@ -0,0 +1,12 @@ +#import +#import +#import + +@interface UIColor (Private) ++ (id)_groupTableHeaderFooterTextColor; +@end + +@interface XXXRefreshableHeaderCell : PSTableCell +@property (nonatomic, strong) UILabel *label; +@property (nonatomic, strong) UIButton *actionButton; +@end diff --git a/Group Cell Header with Button/XXXRefreshableHeaderCell.m b/Group Cell Header with Button/XXXRefreshableHeaderCell.m new file mode 100644 index 0000000..d36c910 --- /dev/null +++ b/Group Cell Header with Button/XXXRefreshableHeaderCell.m @@ -0,0 +1,42 @@ +#import "XXXRefreshableHeaderCell.h" + +@implementation XXXRefreshableHeaderCell + +- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier specifier:(PSSpecifier *)specifier { + if (self = [super initWithStyle:style reuseIdentifier:nil specifier:specifier]) { + // Recreate main label + self.titleLabel.text = [localize(specifier.properties[@"label"], @"MoreSub") uppercaseString]; + [self.titleLabel sizeToFit]; + self.titleLabel.textColor = [UIColor _groupTableHeaderFooterTextColor]; + self.titleLabel.font = [UIFont systemFontOfSize:13.f]; + self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO; + + // Button + self.actionButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [self.actionButton setTitle:[specifier.properties[@"actionLabel"] uppercaseString] forState:UIControlStateNormal]; + [self.actionButton setTitleColor:[self.actionButton.tintColor colorWithAlphaComponent:.5] forState:(UIControlStateHighlighted | UIControlStateSelected)]; + self.actionButton.titleLabel.font = [self.actionButton.titleLabel.font fontWithSize:13.f]; + [self.actionButton addTarget:specifier.target action:NSSelectorFromString(specifier.properties[@"action"]) forControlEvents:UIControlEventTouchUpInside]; + self.actionButton.translatesAutoresizingMaskIntoConstraints = NO; + [self.contentView addSubview:self.actionButton]; + + // Constraints + [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[label]-[action]-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:@{ @"label" : self.titleLabel, @"action" : self.actionButton }]]; + [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[label]-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:@{ @"label" : self.titleLabel }]]; + // For a reason the constraint needs to be inverted for the button + [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[action]" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:@{ @"action" : self.actionButton }]]; + } + return self; +} + +- (instancetype)initWithSpecifier:(PSSpecifier *)specifier { + return self = [self initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil specifier:specifier]; +} + +#pragma mark - PSHeaderFooterView + +- (CGFloat)preferredHeightForWidth:(CGFloat)width { + return 38.f; // default height +} + +@end diff --git a/Group Cell Header with Button/preview.jpeg b/Group Cell Header with Button/preview.jpeg new file mode 100644 index 0000000..66cb8ac Binary files /dev/null and b/Group Cell Header with Button/preview.jpeg differ diff --git a/README.md b/README.md index 6071138..ae6c901 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,28 @@ # Preference Cell Examples -*Custom preference cells among other things to improve your preferences.* +> Custom preference cells among other things to improve your preferences. ## Table of Contents | Preference Cell Type | Description | Preview | | ------------------------------ | ----------- | ------- | -| [Color Picker Cell](https://github.com/LacertosusRepo/Preference-Cell-Examples/tree/main/Color%20Picker%20Cell) | Color picker using iOS 14's UIColorPickerViewController. | Preview | -| [Labeled Slider Cell](https://github.com/LacertosusRepo/Preference-Cell-Examples/tree/main/Labeled%20Slider%20Cell) | Slider with label centered above it, also allowing custom values to be entered by tapping the value displayed. | Preview | -| [Segment Table Image Cell](https://github.com/LacertosusRepo/Preference-Cell-Examples/tree/main/Segment%20Table%20Image%20Cell) | PSSegmentCell displaying images instead of strings. | Preview | -| [Style Picker Cell](https://github.com/LacertosusRepo/Preference-Cell-Examples/tree/main/Style%20Picker%20Cell) | Style picker for options, similar to Apple's light/dark mode selector. | Preview | -| [Switch with Info Cell](https://github.com/LacertosusRepo/Preference-Cell-Examples/tree/main/Switch%20with%20Info%20Cell) | Add info button next to switch to provide more in-depth info to the user. | Preview | -| [Time Interval Picker Cell](https://github.com/LacertosusRepo/Preference-Cell-Examples/tree/main/Time%20Interval%20Picker%20Cell) | Lets user select a number of hours, minutes, and seconds. | Preview | +| [Color Picker Cell](Color%20Picker%20Cell) | Color picker using iOS 14's UIColorPickerViewController. | Preview | +| [Labeled Slider Cell](Labeled%20Slider%20Cell) | Slider with label centered above it, also allowing custom values to be entered by tapping the value displayed. | Preview | +| [Segment Table Image Cell](Segment%20Table%20Image%20Cell) | PSSegmentCell displaying images instead of strings. | Preview | +| [Style Picker Cell](Style%20Picker%20Cell) | Style picker for options, similar to Apple's light/dark mode selector. | Preview | +| [Switch with Info Cell](Switch%20with%20Info%20Cell) | Add info button next to switch to provide more in-depth info to the user. | Preview | +| [Time Interval Picker Cell](Time%20Interval%20Picker%20Cell) | Lets user select a number of hours, minutes, and seconds. | Preview | - + ## Other Guides Included | Guide Type | Description | Preview | | --------------- | ----------- | ------- | -| [Animated Title View](https://github.com/LacertosusRepo/Preference-Cell-Examples/tree/main/Animated%20Title%20View) | Title label in the navigation bar that 'slides up' when the user scrolls down. | Preview | -| [Dynamic Specifiers](https://github.com/LacertosusRepo/Preference-Cell-Examples/tree/main/Dynamic%20Specifiers) | Remove or insert specifiers based on other preference values. | Preview | -| [Footer Hyperlink](https://github.com/LacertosusRepo/Preference-Cell-Examples/tree/main/Footer%20Hyperlink) | Insert a hyperlink in your footer text. | Preview | +| [Animated Title View](Animated%20Title%20View) | Title label in the navigation bar that 'slides up' when the user scrolls down. | Preview | +| [Animated Title View With Icon](Animated%20Title%20View) | Title label and icon in the navigation bar that 'slide up' when the user scrolls down. | Preview | +| [Animated Banner](Animated%20Banner) | Pretty animated banner with a bunch of features. | Preview | +| [Dynamic Specifiers](Dynamic%20Specifiers) | Remove or insert specifiers based on other preference values. | Preview | +| [Footer Hyperlink](Footer%20Hyperlink) | Insert a hyperlink in your footer text. | Preview | +| [Group Cell Header with Button](Group%20Cell%20Header%20with%20Button) | Add a button for your PSGroupCell header. | Preview | - + diff --git a/SF Symbols Link Cell/README.md b/SF Symbols Link Cell/README.md new file mode 100644 index 0000000..bfe97c5 --- /dev/null +++ b/SF Symbols Link Cell/README.md @@ -0,0 +1,80 @@ +# SFSymbols Link Cell +Allow you to use `PSLinkCell`s that have an [SFSymbol](https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/) as icon. (In theory usable with any specifier that supports `icon`) + +## Availability +Already available on Cephei ([with my contribution]()!) since v1.1, so it's not needed if you do use it. + +Available for iOS 13+. Will fallback to an image with the same name for versions below, as an usual `PSLinkCell`. Make sure to import images with the same name as the symbol (x1/@2x/@3x) in the same folder as the `plist` file if you want to support older versions. + +You can find out which Symbols are available by using [this Mac app](https://developer.apple.com/sf-symbols/) or [downloading this app](https://github.com/MTACS/Symbols) on your jailbroken device. + +## How to use +- Import the `XXXSymbolsLinkCell` files here +- Use it in your `.plist` file as below: +```plist + + + + cell + PSLinkCell + cellClass + XXXSymbolsLinkCell + icon + YOUR_ICON_NAME + label + YOUR_LABEL + detail + YOUR_CONTROLLER + YOUR_PREFIX + PLIST_TO_LOAD + + + + + cell + PSLinkCell + cellClass + ABCSymbolsLinkCell + icon + circle.lefthalf.fill + label + Light and Dark Mode + detail + ABCSubListController + ABCSub + LightDarkSub + +``` + +
+ Example demo +
Render of the example
+
+ +## Available options +Customize your cell icon with these premade options. + +key | type | values | default +:---:|:---:|:---:|:---: +weight | string | `ultrathin`, `light`, `thin`, `regular`, `medium`, `semibold`, `bold`, `heavy` or `black` | `regular` +size | real | - | `29` (PSLinkCell default) +color | string | an hex value (e.g.: `#ab76e9`) | `#007aff` (system blue) +||| a [system color name](https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/color/#system-colors) (`blue`, `green`, `indigo`, [...], `gray6`) | `blue` +darkColor* | string | an hex value (e.g.: `#ab76e9`) | `#0a84ff` (system 'dark' blue) +||| a [system color name](https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/color/#system-colors) (`blue`, `green`, `indigo`, [...], `gray6`) | `blue` + +> *note that `darkColor` is used when the system **is in dark mode**. It will be ignored if `color` isn't specified. + +## Preview +
+ Default settings +
Cell with default settings
+
+
+ Customized demo +
Cell with several options
+
+
+ Dark and light mode +
Support for dynamic colors
+
diff --git a/SF Symbols Link Cell/XXXSymbolsLinkCell.h b/SF Symbols Link Cell/XXXSymbolsLinkCell.h new file mode 100644 index 0000000..f456fea --- /dev/null +++ b/SF Symbols Link Cell/XXXSymbolsLinkCell.h @@ -0,0 +1,14 @@ +#import + +@interface UIColor (Private) ++ (UIColor *)_systemColorWithName:(NSString *)arg1; +@end + +@interface UIColor (SymbolsLinkCell) ++ (UIColor *)systemColorFromString:(NSString *)colorString; ++ (UIColor *)colorFromHexString:(NSString *)hex; +@end + +@interface XXXSymbolsLinkCell : PSTableCell +@property (nonatomic, strong) UIImage *symbolsImage; +@end diff --git a/SF Symbols Link Cell/XXXSymbolsLinkCell.m b/SF Symbols Link Cell/XXXSymbolsLinkCell.m new file mode 100644 index 0000000..fcf61fd --- /dev/null +++ b/SF Symbols Link Cell/XXXSymbolsLinkCell.m @@ -0,0 +1,134 @@ +#import "XXXSymbolsLinkCell.h" + +@implementation UIColor (SymbolsLinkCell) + ++ (UIColor *)systemColorFromString:(NSString *)colorString { + NSString *finalColorString = [NSString stringWithFormat:@"system%@Color", [colorString capitalizedString]]; + + if ([UIColor respondsToSelector:@selector(_systemColorWithName:)]) { + return [UIColor _systemColorWithName:finalColorString]; + } else { + SEL colorSelector = NSSelectorFromString(finalColorString); + if ([UIColor respondsToSelector:colorSelector]) { + return [UIColor performSelector:colorSelector]; + } + } + + return nil; +} + +// https://stackoverflow.com/a/12397366/12070367 ++ (UIColor *)colorFromHexString:(NSString *)hex { + hex = [hex stringByReplacingOccurrencesOfString:@"#" withString:@""]; + NSCharacterSet *hexChars = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789ABCDEF"] invertedSet]; + if ([[hex uppercaseString] rangeOfCharacterFromSet:hexChars].location != NSNotFound) { + NSLog(@"[%@] Argument is not a valid hexadecimal color, returning", NSStringFromSelector(_cmd)); + return nil; + } + + unsigned intValue = 0; + [[NSScanner scannerWithString:hex] scanHexInt:&intValue]; + return [UIColor colorWithRed:((intValue & 0xFF0000) >> 16) / 255.0 + green:((intValue & 0xFF00) >> 8) / 255.0 + blue:(intValue & 0xFF) / 255.0 + alpha:1.0]; +} + +@end + +@implementation XXXSymbolsLinkCell + +- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier specifier:(PSSpecifier *)specifier { + if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier specifier:specifier]) { + if (@available(iOS 13.0, *)) { + // Checking availability and presence of icon property + if (!specifier.properties[@"icon"]) { + NSLog(@"[%@ %p] No icon found, falling back to default behavior", NSStringFromClass(self.class), self); + return self; + } + + // Applying SF Symbol + if ((self.symbolsImage = [UIImage systemImageNamed:specifier.properties[@"icon"]])) { + NSLog(@"[%@ %p] Applied SF Symbol: %@", NSStringFromClass(self.class), self, specifier.properties[@"icon"]); + } else { + NSLog(@"[%@ %p] SF Symbol \"%@\" not found, falling back to default behavior", NSStringFromClass(self.class), self, specifier.properties[@"icon"]); + return self; + } + + CGFloat pointSize = specifier.properties[@"size"] ? [specifier.properties[@"size"] floatValue] : 29.f; + + // Setting custom weight if requested + if (specifier.properties[@"weight"]) { + NSString *weightString = [specifier.properties[@"weight"] lowercaseString]; + UIImageSymbolWeight weight; + if ([weightString isEqualToString:@"ultralight"]) { + weight = UIImageSymbolWeightUltraLight; + } else if ([weightString isEqualToString:@"thin"]) { + weight = UIImageSymbolWeightThin; + } else if ([weightString isEqualToString:@"light"]) { + weight = UIImageSymbolWeightLight; + } else if ([weightString isEqualToString:@"regular"]) { + weight = UIImageSymbolWeightRegular; + } else if ([weightString isEqualToString:@"medium"]) { + weight = UIImageSymbolWeightMedium; + } else if ([weightString isEqualToString:@"semibold"]) { + weight = UIImageSymbolWeightSemibold; + } else if ([weightString isEqualToString:@"bold"]) { + weight = UIImageSymbolWeightBold; + } else if ([weightString isEqualToString:@"heavy"]) { + weight = UIImageSymbolWeightHeavy; + } else if ([weightString isEqualToString:@"black"]) { + weight = UIImageSymbolWeightBlack; + } else { + weight = UIImageSymbolWeightUnspecified; + } + + UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:pointSize weight:weight scale:UIImageSymbolScaleSmall]; + self.symbolsImage = [self.symbolsImage imageWithConfiguration:config]; + NSLog(@"[%@ %p] Applied weight configuration from weight \"%@\" and size %.f", NSStringFromClass(self.class), self, weightString, pointSize); + } else { + UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:pointSize weight:UIImageSymbolWeightUnspecified scale:UIImageSymbolScaleSmall]; + self.symbolsImage = [self.symbolsImage imageWithConfiguration:config]; + NSLog(@"[%@ %p] Applied size configuration with size %.f", NSStringFromClass(self.class), self, pointSize); + } + + // Setting custom color if requested + if (specifier.properties[@"color"]) { + UIColor *finalColor = nil; + + NSString *stringColor = [specifier.properties[@"color"] lowercaseString]; + UIColor *color = [UIColor systemColorFromString:stringColor] ?: [UIColor colorFromHexString:stringColor]; + if (color) { + if (specifier.properties[@"darkColor"]) { + NSString *stringDarkColor = [specifier.properties[@"darkColor"] lowercaseString]; + UIColor *darkColor = [UIColor systemColorFromString:stringDarkColor] ?: [UIColor colorFromHexString:stringDarkColor]; + finalColor = [UIColor colorWithDynamicProvider:^UIColor * (UITraitCollection *traits) { + return traits.userInterfaceStyle == UIUserInterfaceStyleLight ? color : darkColor; + }]; + } else { + finalColor = color; + } + } + + if (finalColor) { + self.symbolsImage = [self.symbolsImage imageWithTintColor:finalColor renderingMode:UIImageRenderingModeAlwaysOriginal]; + NSLog(@"[%@ %p] Color changed, now %@", NSStringFromClass(self.class), self, [[CIColor colorWithCGColor:finalColor.CGColor] stringRepresentation]); + } else { + NSLog(@"[%@ %p] Error when changing color (color: %@, darkColor: %@)", NSStringFromClass(self.class), self, color, specifier.properties[@"darkColor"]); + } + } + } + } + + return self; +} + +- (void)refreshCellContentsWithSpecifier:(PSSpecifier *)specifier { + [super refreshCellContentsWithSpecifier:specifier]; + + if (self.imageView && self.symbolsImage && self.imageView.image != self.symbolsImage) { + self.imageView.image = self.symbolsImage; + } +} + +@end diff --git a/SF Symbols Link Cell/images/custom.jpeg b/SF Symbols Link Cell/images/custom.jpeg new file mode 100644 index 0000000..21360c1 Binary files /dev/null and b/SF Symbols Link Cell/images/custom.jpeg differ diff --git a/SF Symbols Link Cell/images/default.JPEG b/SF Symbols Link Cell/images/default.JPEG new file mode 100644 index 0000000..bed2dc5 Binary files /dev/null and b/SF Symbols Link Cell/images/default.JPEG differ diff --git a/SF Symbols Link Cell/images/example.JPEG b/SF Symbols Link Cell/images/example.JPEG new file mode 100644 index 0000000..458f739 Binary files /dev/null and b/SF Symbols Link Cell/images/example.JPEG differ diff --git a/SF Symbols Link Cell/images/light_dark.GIF b/SF Symbols Link Cell/images/light_dark.GIF new file mode 100644 index 0000000..ef01c6f Binary files /dev/null and b/SF Symbols Link Cell/images/light_dark.GIF differ