This commit is contained in:
Đỗ Thành
2025-06-12 19:26:00 +07:00
parent ee63a3566b
commit 449ea66c10
12 changed files with 921 additions and 1 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

BIN
CTDOWelcomeFX/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,70 @@
// CTDOWelcomeFX.h
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/**
* Represents a feature item displayed in the ctdowelcomefx screen's feature list.
*/
@interface CTDOWelcomeFXFeature : NSObject
@property (nonatomic, strong, readonly) UIImage *icon;
@property (nonatomic, strong, readonly) NSString *title;
@property (nonatomic, strong, readonly) NSString *subtitle;
- (instancetype)initWithIcon:(UIImage *)icon title:(NSString *)title subtitle:(NSString *)subtitle;
@end
/**
* Configuration object containing all customizable data for the ctdowelcomefx screen.
*/
@interface CTDOWelcomeFXConfiguration : NSObject
// Header Configuration
@property (nonatomic, strong) UIImage *appIcon;
@property (nonatomic, copy) NSString *welcomeTitle;
@property (nonatomic, copy) NSString *appName;
@property (nonatomic, strong) UIColor *appNameColor;
// Content Configuration
@property (nonatomic, strong) NSArray<CTDOWelcomeFXFeature *> *features;
// Bottom Bar Configuration
@property (nonatomic, copy) NSString *descriptionText;
@property (nonatomic, copy) NSString *linkText;
@property (nonatomic, strong) NSURL *linkURL;
@property (nonatomic, copy) NSString *continueButtonText;
// Display Settings
@property (nonatomic, copy) NSString *userDefaultsKey;
@property (nonatomic, assign) BOOL showEveryLaunch;
+ (instancetype)defaultConfiguration;
@end
/**
* Main view controller for the ctdowelcomefx screen.
* Use the class method `showctdowelcomefxIfNeeded...` to display the ctdowelcomefx screen.
*/
@interface CTDOWelcomeFXViewController : UIViewController
/**
* Displays the ctdowelcomefx screen if it hasn't been shown before.
* Checks NSUserDefaults using the configuration's userDefaultsKey.
* If ctdowelcomefx was never shown, it presents the screen on presentingViewController.
* If already shown, it immediately calls the completion block.
*
* @param configuration Configuration object containing display data
* @param presentingVC View controller to present ctdowelcomefx from. If nil, uses top-most view controller
* @param completion Block called after user taps continue and ctdowelcomefx is dismissed
*/
+ (void)showctdowelcomefxIfNeededWithConfiguration:(CTDOWelcomeFXConfiguration *)configuration
inViewController:(nullable UIViewController *)presentingVC
completion:(nullable void (^)(void))completion;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,518 @@
//
// CTDOWelcomeFX.m
// CTDOWelcomeFX
//
// Created by Đ Trung Thành on 9/6/25.
//
#import "CTDOWelcomeFX.h"
#pragma mark - CTDOWelcomeFXFeature Implementation
@implementation CTDOWelcomeFXFeature
- (instancetype)initWithIcon:(UIImage *)icon title:(NSString *)title subtitle:(NSString *)subtitle {
self = [super init];
if (self) {
_icon = icon;
_title = title;
_subtitle = subtitle;
}
return self;
}
@end
#pragma mark - CTDOWelcomeFXConfiguration Implementation
@implementation CTDOWelcomeFXConfiguration
+ (instancetype)defaultConfiguration {
CTDOWelcomeFXConfiguration *config = [[CTDOWelcomeFXConfiguration alloc] init];
UIImage *appIcon = [UIImage imageNamed:@"app_icon_ctdo"];
if (!appIcon) {
appIcon = [UIImage systemImageNamed:@"app.gift.fill"];
}
config.appIcon = appIcon;
config.welcomeTitle = @"Welcome to";
config.appName = @"CTDOTECH";
config.appNameColor = [UIColor colorWithRed:0.72 green:0.66 blue:0.51 alpha:1.0];
NSMutableArray<CTDOWelcomeFXFeature *> *defaultFeatures = [NSMutableArray array];
NSArray *iconNames = @[@"feature_icon_1", @"feature_icon_2", @"feature_icon_3"];
NSArray *sfSymbolNames = @[@"star.fill", @"lock.shield.fill", @"paintbrush.pointed.fill"];
NSArray *titles = @[@"Feature 1", @"Feature 2", @"Feature 3"];
NSArray *subtitles = @[@"Description for feature 1\nand more text\nand more text", @"Description for feature 2", @"Description for feature 3"];
for (int i = 0; i < titles.count; i++) {
UIImage *icon = [UIImage imageNamed:iconNames[i]];
if (!icon) {
icon = [UIImage systemImageNamed:sfSymbolNames[i]];
}
[defaultFeatures addObject:[[CTDOWelcomeFXFeature alloc] initWithIcon:icon title:titles[i] subtitle:subtitles[i]]];
}
config.features = [defaultFeatures copy];
config.descriptionText = @"Your app description here. Learn more...";
config.linkText = @"Learn more...";
config.linkURL = [NSURL URLWithString:@"https://ctdo.net"];
config.continueButtonText = @"Continue";
config.userDefaultsKey = @"hasShownctdowelcomefx";
return config;
}
@end
#pragma mark - CTDOWelcomeFXViewController Interface
@interface CTDOWelcomeFXViewController () <UITextViewDelegate, UIScrollViewDelegate>
// Configuration
@property (nonatomic, strong) CTDOWelcomeFXConfiguration *configuration;
@property (nonatomic, copy) void (^ctdowelcomefxCompletionHandler)(void);
// UI Components
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) UILayoutGuide *contentLayoutGuide;
@property (nonatomic, strong) UIView *bottomBarContainer;
@property (nonatomic, strong) UIVisualEffectView *blurEffectView;
@property (nonatomic, strong) NSLayoutConstraint *bottomBarBottomConstraint;
@property (nonatomic, assign) BOOL isBottomBarVisible;
@property (nonatomic, assign) CGFloat lastScrollViewOffset;
// Responsive Metrics
@property (nonatomic, assign) CGFloat titleFontSize;
@property (nonatomic, assign) CGFloat horizontalPadding;
@property (nonatomic, assign) CGFloat iconSize;
@property (nonatomic, assign) CGFloat featureTitleSize;
@property (nonatomic, assign) CGFloat featureSubtitleSize;
@property (nonatomic, assign) CGFloat maxContentWidth;
// UI Elements
@property (nonatomic, strong) UIImageView *logoImageView;
@property (nonatomic, strong) UILabel *welcomeTitleLabel;
@property (nonatomic, strong) UIStackView *featuresStackView;
@property (nonatomic, strong) UITextView *descriptionTextView;
@property (nonatomic, strong) UIButton *continueButton;
// Animation Constraints
@property (nonatomic, strong) NSArray<NSLayoutConstraint *> *welcomeGroupCenteredConstraints;
@property (nonatomic, strong) NSArray<NSLayoutConstraint *> *welcomeGroupTopConstraints;
@end
#pragma mark - CTDOWelcomeFXViewController Implementation
@implementation CTDOWelcomeFXViewController
#pragma mark - Initialization and Lifecycle
- (instancetype)initWithConfiguration:(CTDOWelcomeFXConfiguration *)configuration {
self = [super init];
if (self) {
_configuration = configuration;
self.modalPresentationStyle = UIModalPresentationFullScreen;
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor systemBackgroundColor];
[self configureMetricsForCurrentDevice];
[self setupctdowelcomefxUI];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (self.logoImageView.alpha == 0.0) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self startctdowelcomefxAnimationSequence];
});
}
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[self updateBlurEffectVisibility];
}
#pragma mark - Public Class Method for Presentation
+ (void)showctdowelcomefxIfNeededWithConfiguration:(CTDOWelcomeFXConfiguration *)configuration
inViewController:(UIViewController *)presentingVC
completion:(void (^)(void))completion {
if (!configuration.showEveryLaunch) {
BOOL hasShownctdowelcomefx = [[NSUserDefaults standardUserDefaults] boolForKey:configuration.userDefaultsKey];
if (hasShownctdowelcomefx) {
if (completion) {
completion();
}
return;
}
}
CTDOWelcomeFXViewController *ctdowelcomefxVC = [[CTDOWelcomeFXViewController alloc] initWithConfiguration:configuration];
ctdowelcomefxVC.ctdowelcomefxCompletionHandler = completion;
ctdowelcomefxVC.modalPresentationStyle = UIModalPresentationFullScreen;
ctdowelcomefxVC.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
UIViewController *vcToPresentOn = presentingVC;
if (!vcToPresentOn) {
vcToPresentOn = [self topMostViewController];
}
if (vcToPresentOn.presentedViewController) {
[vcToPresentOn.presentedViewController dismissViewControllerAnimated:NO completion:^{
[vcToPresentOn presentViewController:ctdowelcomefxVC animated:YES completion:nil];
}];
} else {
[vcToPresentOn presentViewController:ctdowelcomefxVC animated:YES completion:nil];
}
}
+ (UIViewController *)topMostViewController {
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (topController.presentedViewController) {
topController = topController.presentedViewController;
}
return topController;
}
#pragma mark - UI Setup
- (void)configureMetricsForCurrentDevice {
if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) {
self.titleFontSize = 70.0; self.horizontalPadding = 60.0; self.iconSize = 50.0;
self.featureTitleSize = 22.0; self.featureSubtitleSize = 19.0; self.maxContentWidth = 700.0;
} else {
self.titleFontSize = 50.0; self.horizontalPadding = 35.0; self.iconSize = 40.0;
self.featureTitleSize = 17.0; self.featureSubtitleSize = 15.0; self.maxContentWidth = 580.0;
}
}
- (void)setupctdowelcomefxUI {
// Setup ScrollView
self.scrollView = [[UIScrollView alloc] init];
self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;
self.scrollView.delegate = self;
self.scrollView.showsVerticalScrollIndicator = NO;
[self.view addSubview:self.scrollView];
UIView *contentView = [[UIView alloc] init];
contentView.translatesAutoresizingMaskIntoConstraints = NO;
[self.scrollView addSubview:contentView];
self.contentLayoutGuide = [[UILayoutGuide alloc] init];
[self.view addLayoutGuide:self.contentLayoutGuide];
[self setupContentLayoutGuideConstraints:self.contentLayoutGuide];
// Setup Bottom Bar
self.bottomBarContainer = [[UIView alloc] init];
self.bottomBarContainer.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.bottomBarContainer];
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleSystemMaterial];
self.blurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
self.blurEffectView.translatesAutoresizingMaskIntoConstraints = NO;
self.blurEffectView.alpha = 0.0;
[self.bottomBarContainer addSubview:self.blurEffectView];
[self.bottomBarContainer sendSubviewToBack:self.blurEffectView];
// Create UI Components
self.logoImageView = [[UIImageView alloc] initWithImage:self.configuration.appIcon];
self.welcomeTitleLabel = [[UILabel alloc] init];
NSString *fullTitle = [NSString stringWithFormat:@"%@\n%@", self.configuration.welcomeTitle, self.configuration.appName];
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithString:fullTitle];
NSRange welcomeRange = [fullTitle rangeOfString:self.configuration.welcomeTitle];
NSRange appNameRange = [fullTitle rangeOfString:self.configuration.appName];
[attrStr addAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:self.titleFontSize weight:UIFontWeightBold], NSForegroundColorAttributeName: [UIColor labelColor]} range:welcomeRange];
if (appNameRange.location != NSNotFound) {
[attrStr addAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:self.titleFontSize weight:UIFontWeightBold], NSForegroundColorAttributeName: self.configuration.appNameColor} range:appNameRange];
}
self.welcomeTitleLabel.attributedText = attrStr;
self.welcomeTitleLabel.numberOfLines = 0;
self.welcomeTitleLabel.textAlignment = NSTextAlignmentLeft;
// Create Features StackView
NSMutableArray *featureViews = [NSMutableArray array];
for (CTDOWelcomeFXFeature *feature in self.configuration.features) {
UIImageView *iconView = [[UIImageView alloc] initWithImage:feature.icon];
[featureViews addObject:[self createFeatureViewWithIconView:iconView title:feature.title subtitle:feature.subtitle]];
}
self.featuresStackView = [[UIStackView alloc] initWithArrangedSubviews:featureViews];
self.featuresStackView.axis = UILayoutConstraintAxisVertical;
self.featuresStackView.spacing = 25.0;
self.featuresStackView.alignment = UIStackViewAlignmentLeading;
self.descriptionTextView = [[UITextView alloc] init];
self.descriptionTextView.delegate = self;
self.descriptionTextView.backgroundColor = [UIColor clearColor];
self.descriptionTextView.editable = NO;
self.descriptionTextView.scrollEnabled = NO;
self.descriptionTextView.textContainerInset = UIEdgeInsetsZero;
self.descriptionTextView.textContainer.lineFragmentPadding = 0;
self.descriptionTextView.textAlignment = NSTextAlignmentCenter;
NSMutableAttributedString *descriptionAttrStr = [[NSMutableAttributedString alloc] initWithString:self.configuration.descriptionText];
CGFloat descriptionFontSize = (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) ? 16 : 13;
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.alignment = NSTextAlignmentCenter;
[descriptionAttrStr addAttributes:@{
NSFontAttributeName: [UIFont systemFontOfSize:descriptionFontSize],
NSForegroundColorAttributeName: [UIColor systemGrayColor],
NSParagraphStyleAttributeName: paragraphStyle
} range:NSMakeRange(0, self.configuration.descriptionText.length)];
NSRange linkRange = [self.configuration.descriptionText rangeOfString:self.configuration.linkText];
if (linkRange.location != NSNotFound) {
[descriptionAttrStr addAttributes:@{ NSLinkAttributeName: self.configuration.linkURL, NSForegroundColorAttributeName: self.configuration.appNameColor } range:linkRange];
}
self.descriptionTextView.attributedText = descriptionAttrStr;
self.continueButton = [UIButton buttonWithType:UIButtonTypeSystem];
[self.continueButton setTitle:self.configuration.continueButtonText forState:UIControlStateNormal];
[self.continueButton setBackgroundColor:self.configuration.appNameColor];
[self.continueButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
self.continueButton.titleLabel.font = [UIFont systemFontOfSize:17 weight:UIFontWeightBold];
self.continueButton.layer.cornerRadius = 14;
[self.continueButton addTarget:self action:@selector(continueButtonTapped) forControlEvents:UIControlEventTouchUpInside];
// Set initial alpha for animation
self.logoImageView.alpha = 0.0;
self.welcomeTitleLabel.alpha = 0.0;
for (UIView *view in self.featuresStackView.arrangedSubviews) { view.alpha = 0.0; }
self.descriptionTextView.alpha = 0.0;
self.continueButton.alpha = 0.0;
// Add views and setup constraints
for (UIView *view in @[self.logoImageView, self.welcomeTitleLabel, self.featuresStackView]) {
view.translatesAutoresizingMaskIntoConstraints = NO;
[contentView addSubview:view];
}
for (UIView *view in @[self.descriptionTextView, self.continueButton]) {
view.translatesAutoresizingMaskIntoConstraints = NO;
[self.bottomBarContainer addSubview:view];
}
UILayoutGuide *safeArea = self.view.safeAreaLayoutGuide;
self.welcomeGroupCenteredConstraints = @[
[self.logoImageView.bottomAnchor constraintEqualToAnchor:self.welcomeTitleLabel.topAnchor constant:-20],
[self.logoImageView.leadingAnchor constraintEqualToAnchor:self.contentLayoutGuide.leadingAnchor],
[self.welcomeTitleLabel.leadingAnchor constraintEqualToAnchor:self.contentLayoutGuide.leadingAnchor],
[self.welcomeTitleLabel.trailingAnchor constraintEqualToAnchor:self.contentLayoutGuide.trailingAnchor],
[self.welcomeTitleLabel.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor]
];
self.welcomeGroupTopConstraints = @[
[self.logoImageView.topAnchor constraintEqualToAnchor:contentView.topAnchor constant:-10],
[self.logoImageView.leadingAnchor constraintEqualToAnchor:self.contentLayoutGuide.leadingAnchor],
[self.welcomeTitleLabel.topAnchor constraintEqualToAnchor:self.logoImageView.bottomAnchor constant:15],
[self.welcomeTitleLabel.leadingAnchor constraintEqualToAnchor:self.contentLayoutGuide.leadingAnchor],
[self.welcomeTitleLabel.trailingAnchor constraintEqualToAnchor:self.contentLayoutGuide.trailingAnchor],
];
CGFloat logoSize = self.iconSize + 20;
[NSLayoutConstraint activateConstraints:@[
[self.scrollView.frameLayoutGuide.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.scrollView.frameLayoutGuide.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[self.scrollView.frameLayoutGuide.topAnchor constraintEqualToAnchor:self.view.topAnchor],
[self.scrollView.frameLayoutGuide.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
[contentView.leadingAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.leadingAnchor],
[contentView.trailingAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.trailingAnchor],
[contentView.topAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.topAnchor],
[contentView.bottomAnchor constraintEqualToAnchor:self.scrollView.contentLayoutGuide.bottomAnchor],
[contentView.widthAnchor constraintEqualToAnchor:self.scrollView.frameLayoutGuide.widthAnchor],
]];
[NSLayoutConstraint activateConstraints:@[
[self.logoImageView.widthAnchor constraintEqualToConstant:logoSize],
[self.logoImageView.heightAnchor constraintEqualToConstant:logoSize],
[self.featuresStackView.topAnchor constraintEqualToAnchor:self.welcomeTitleLabel.bottomAnchor constant:40],
[self.featuresStackView.leadingAnchor constraintEqualToAnchor:self.contentLayoutGuide.leadingAnchor],
[self.featuresStackView.trailingAnchor constraintEqualToAnchor:self.contentLayoutGuide.trailingAnchor],
[self.featuresStackView.bottomAnchor constraintEqualToAnchor:contentView.bottomAnchor constant:-150],
]];
self.bottomBarBottomConstraint = [self.bottomBarContainer.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor];
[NSLayoutConstraint activateConstraints:@[
[self.bottomBarContainer.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[self.bottomBarContainer.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
self.bottomBarBottomConstraint,
[self.blurEffectView.topAnchor constraintEqualToAnchor:self.bottomBarContainer.topAnchor],
[self.blurEffectView.bottomAnchor constraintEqualToAnchor:self.bottomBarContainer.bottomAnchor],
[self.blurEffectView.leadingAnchor constraintEqualToAnchor:self.bottomBarContainer.leadingAnchor],
[self.blurEffectView.trailingAnchor constraintEqualToAnchor:self.bottomBarContainer.trailingAnchor],
[self.descriptionTextView.topAnchor constraintEqualToAnchor:self.bottomBarContainer.topAnchor constant:20],
[self.descriptionTextView.leadingAnchor constraintEqualToAnchor:self.contentLayoutGuide.leadingAnchor],
[self.descriptionTextView.trailingAnchor constraintEqualToAnchor:self.contentLayoutGuide.trailingAnchor],
[self.continueButton.topAnchor constraintEqualToAnchor:self.descriptionTextView.bottomAnchor constant:15],
[self.continueButton.heightAnchor constraintEqualToConstant:(UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) ? 60 : 50],
[self.continueButton.leadingAnchor constraintEqualToAnchor:self.contentLayoutGuide.leadingAnchor],
[self.continueButton.trailingAnchor constraintEqualToAnchor:self.contentLayoutGuide.trailingAnchor],
[self.continueButton.bottomAnchor constraintEqualToAnchor:safeArea.bottomAnchor constant:-20]
]];
[NSLayoutConstraint activateConstraints:self.welcomeGroupCenteredConstraints];
self.isBottomBarVisible = YES;
}
#pragma mark - Actions & Delegate
- (void)continueButtonTapped {
[self animateOutWithCompletion:^{
if (!self.configuration.showEveryLaunch) {
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:self.configuration.userDefaultsKey];
}
[self dismissViewControllerAnimated:NO completion:^{
if (self.ctdowelcomefxCompletionHandler) {
self.ctdowelcomefxCompletionHandler();
}
}];
}];
}
- (void)animateOutWithCompletion:(void (^)(void))completion {
[UIView animateWithDuration:0.4 delay:0.0 options:UIViewAnimationOptionCurveEaseIn animations:^{
self.scrollView.alpha = 0.0;
self.bottomBarContainer.alpha = 0.0;
} completion:^(BOOL finished) {
if (completion) {
completion();
}
}];
}
- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange interaction:(UITextItemInteraction)interaction {
[[UIApplication sharedApplication] openURL:URL options:@{} completionHandler:nil];
return NO;
}
#pragma mark - Animations and UI Updates
- (void)updateBlurEffectVisibility {
if (!self.featuresStackView.window) return;
CGRect featuresFrameInView = [self.featuresStackView.superview convertRect:self.featuresStackView.frame toView:self.view];
CGFloat featuresBottomY = CGRectGetMaxY(featuresFrameInView);
CGFloat barTopY = self.bottomBarContainer.frame.origin.y;
BOOL shouldBeVisible = featuresBottomY > barTopY;
if ((shouldBeVisible && self.blurEffectView.alpha == 0) || (!shouldBeVisible && self.blurEffectView.alpha == 1)) {
[UIView animateWithDuration:0.3 animations:^{
self.blurEffectView.alpha = shouldBeVisible ? 1.0 : 0.0;
}];
}
}
- (void)hideBottomBar {
if (!self.isBottomBarVisible) return;
self.isBottomBarVisible = NO;
CGFloat barHeight = self.bottomBarContainer.frame.size.height;
self.bottomBarBottomConstraint.constant = barHeight;
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
[self.view layoutIfNeeded];
} completion:nil];
}
- (void)showBottomBar {
if (self.isBottomBarVisible) return;
self.isBottomBarVisible = YES;
self.bottomBarBottomConstraint.constant = 0;
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
[self.view layoutIfNeeded];
} completion:nil];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat currentOffset = scrollView.contentOffset.y;
CGFloat contentHeight = scrollView.contentSize.height;
CGFloat scrollViewHeight = scrollView.frame.size.height;
if (contentHeight <= scrollViewHeight) { [self showBottomBar]; return; }
if (currentOffset > self.lastScrollViewOffset && currentOffset > 0) { [self hideBottomBar]; }
else if (currentOffset < self.lastScrollViewOffset || currentOffset <= 0) { [self showBottomBar]; }
self.lastScrollViewOffset = currentOffset;
[self updateBlurEffectVisibility];
}
- (void)startctdowelcomefxAnimationSequence {
[UIView animateWithDuration:1.2 delay:0.2 options:UIViewAnimationOptionCurveEaseIn animations:^{
self.logoImageView.alpha = 1.0;
self.welcomeTitleLabel.alpha = 1.0;
} completion:^(BOOL finished) {
if (!finished) return;
[self animateToTopPositionAndRevealFeatures];
}];
}
- (void)animateToTopPositionAndRevealFeatures {
[NSLayoutConstraint deactivateConstraints:self.welcomeGroupCenteredConstraints];
[NSLayoutConstraint activateConstraints:self.welcomeGroupTopConstraints];
[UIView animateWithDuration:0.8 delay:0.3 options:UIViewAnimationOptionCurveEaseInOut animations:^{
[self.view layoutIfNeeded];
self.logoImageView.alpha = 0.0;
self.descriptionTextView.alpha = 1.0;
self.continueButton.alpha = 1.0;
} completion:^(BOOL finished) {
if (!finished) return;
[self animateFeaturesStaggered];
[self updateBlurEffectVisibility];
}];
}
- (void)animateFeaturesStaggered {
NSTimeInterval delay = 0.0;
NSTimeInterval increment = 0.15;
for (UIView *featureView in self.featuresStackView.arrangedSubviews) {
featureView.transform = CGAffineTransformMakeTranslation(0, 20);
[UIView animateWithDuration:0.4 delay:delay options:UIViewAnimationOptionCurveEaseOut animations:^{
featureView.alpha = 1.0;
featureView.transform = CGAffineTransformIdentity;
} completion:nil];
delay += increment;
}
}
- (void)setupContentLayoutGuideConstraints:(UILayoutGuide *)guide {
UILayoutGuide *safeArea = self.view.safeAreaLayoutGuide;
NSLayoutConstraint *widthConstraint = [guide.widthAnchor constraintEqualToAnchor:safeArea.widthAnchor constant: -2 * self.horizontalPadding];
NSLayoutConstraint *maxWidthConstraint = [guide.widthAnchor constraintLessThanOrEqualToConstant:self.maxContentWidth];
widthConstraint.priority = UILayoutPriorityDefaultHigh;
[NSLayoutConstraint activateConstraints:@[
[guide.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
[guide.topAnchor constraintEqualToAnchor:self.view.topAnchor],
[guide.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
widthConstraint,
maxWidthConstraint
]];
}
- (UIView *)createFeatureViewWithIconView:(UIView *)iconView title:(NSString *)title subtitle:(NSString *)subtitle {
iconView.contentMode = UIViewContentModeScaleAspectFit;
UILabel *titleLabel = [[UILabel alloc] init];
titleLabel.text = title;
titleLabel.textColor = [UIColor labelColor];
titleLabel.font = [UIFont systemFontOfSize:self.featureTitleSize weight:UIFontWeightBold];
UILabel *subtitleLabel = [[UILabel alloc] init];
subtitleLabel.text = subtitle;
subtitleLabel.textColor = [UIColor systemGrayColor];
subtitleLabel.font = [UIFont systemFontOfSize:self.featureSubtitleSize];
subtitleLabel.numberOfLines = 0;
UIStackView *textStackView = [[UIStackView alloc] initWithArrangedSubviews:@[titleLabel, subtitleLabel]];
textStackView.axis = UILayoutConstraintAxisVertical;
textStackView.spacing = 4;
UIStackView *mainStackView = [[UIStackView alloc] initWithArrangedSubviews:@[iconView, textStackView]];
mainStackView.axis = UILayoutConstraintAxisHorizontal;
mainStackView.spacing = 15;
mainStackView.alignment = UIStackViewAlignmentTop;
[iconView.widthAnchor constraintEqualToConstant:self.iconSize].active = YES;
[iconView.heightAnchor constraintEqualToConstant:self.iconSize].active = YES;
return mainStackView;
}
@end

167
README.md
View File

@@ -1,2 +1,167 @@
# CTDOWelcomeFX
# 🚀 CTDOWelcomeFX
<div align="right">
<a href="README_VI.md">🇻🇳 Tiếng Việt</a>
</div>
<div align="center">
![Version](https://img.shields.io/badge/version-1.0.0-blue.svg)
![License](https://img.shields.io/badge/license-MIT-green.svg)
![Platform](https://img.shields.io/badge/platform-iOS-lightgrey.svg)
![iOS](https://img.shields.io/badge/iOS-13.0+-blue.svg)
</div>
<div align="center">
<img src="https://raw.githubusercontent.com/thanhdo1110/CTDOWelcomeFX/main/Resources/demo.gif" alt="Dark Mode Demo" width="300"/>
<img src="https://raw.githubusercontent.com/thanhdo1110/CTDOWelcomeFX/main/Resources/demo1.gif" alt="Light Mode Demo" width="300"/>
</div>
## 📱 Introduction
A beautiful, customizable WelcomeFX experience library for iOS applications. This library provides a modern and engaging way to introduce your app's features to new users.
## ✨ Features
- 🎨 Modern and clean UI design
- 📱 Supports both iPhone and iPad
- 🔄 Smooth animations and transitions
- 🎯 Customizable content and styling
- 🔒 Optional one-time or every-launch display
- 🌐 Link support in description text
- 🖼️ SF Symbols fallback for missing images
## 🛠 Installation
### Git Clone
```bash
git clone https://github.com/thanhdo1110/CTDOWelcomeFX.git
```
### Manual
Simply add `CTDOWelcomeFX.h` and `CTDOWelcomeFX.m` to your project.
## 📖 Usage
### Basic Implementation
```obj-c++
#import "CTDOWelcomeFX/CTDOWelcomeFX.h"
#import "CTDOWelcomeFX/CTDOWelcomeFXImages.h"
@interface CTDOWelcomeFXTweak : NSObject
+ (void)load;
@end
@implementation CTDOWelcomeFXTweak
+ (void)load {
@autoreleasepool {
// --- 1. Khởi tạo cấu hình ---
CTDOWelcomeFXConfiguration *config = [CTDOWelcomeFXConfiguration defaultConfiguration];
// --- 2. Config tuỳ chỉnh ---
config.appIcon = [CTDOWelcomeFXImages appIconImage];
config.appName = @"ctdotech";
config.welcomeTitle = @"Welcome to";
config.continueButtonText = @"continue";
config.descriptionText = @"Please join my community here...";
config.linkText = @"here...";
config.linkURL = [NSURL URLWithString:@"https://ctdo.net"];
config.userDefaultsKey = @"hasShownMyTweakctdowelcomefx";
config.showEveryLaunch = YES;
config.appNameColor = [UIColor colorWithRed:0.0 green:0.7137 blue:0.7255 alpha:1.0]; // Màu xanh dương
// config.appNameColor = [UIColor colorWithRed:0.0/255.0 green:201.0/255.0 blue:167.0/255.0 alpha:1.0];
// Tạo các features của bạn
CTDOWelcomeFXFeature *feature1 = [[CTDOWelcomeFXFeature alloc]
initWithIcon:[CTDOWelcomeFXImages feature1Image]
title:@"Privacy policy"
subtitle:@"We do not collect any of your information.\nYour security is guaranteed."];
CTDOWelcomeFXFeature *feature2 = [[CTDOWelcomeFXFeature alloc]
initWithIcon:[CTDOWelcomeFXImages feature2Image]
title:@"Interface"
subtitle:@"Smooth, easy, and friendly to use."];
CTDOWelcomeFXFeature *feature3 = [[CTDOWelcomeFXFeature alloc]
initWithIcon:[CTDOWelcomeFXImages feature3Image]
title:@"Features"
subtitle:@"Diverse and innovative for a better experience."];
config.features = @[feature1, feature2, feature3];
// --- 3. Gọi để hiển thị ---
dispatch_async(dispatch_get_main_queue(), ^{
[CTDOWelcomeFXViewController showctdowelcomefxIfNeededWithConfiguration:config
inViewController:nil
completion:^{
NSLog(@"MyTweak by ctdoteam || @dothanh1110");
}];
});
}
}
@end
```
## 🎨 Image Guidelines
### 1. App Icon
- Recommended size: 1024x1024px
- Format: PNG
- Can be loaded from Assets.xcassets or PNG file
- SF Symbols fallback available
### 2. Feature Icons
- Recommended size: 60x60px
- Format: PNG
- Can be loaded from Assets.xcassets or PNG file
- SF Symbols fallback available
### 3. SF Symbols
- Available as fallback when images are missing
- Automatically scales for different screen sizes
- Supports dynamic colors and dark mode
- Example: "star.fill", "lock.shield.fill", "paintbrush.pointed.fill"
## ⚙️ Configuration
| Property | Description |
|----------|-------------|
| `appIcon` | Your app's icon (UIImage or SF Symbol) |
| `welcomeTitle` | Welcome message (e.g., "Welcome to") |
| `appName` | Your app's name |
| `appNameColor` | Color for app name |
| `features` | Array of features to display |
| `descriptionText` | Bottom description text |
| `linkText` | Text to be linked |
| `linkURL` | URL for the link |
| `continueButtonText` | Text for continue button |
| `userDefaultsKey` | Key for storing display state |
| `showEveryLaunch` | Whether to show on every launch |
## 📋 Requirements
- iOS 13.0+
- Xcode 11.0+/Theos
- Objective-C/C++
## 📄 License
This project is available under the MIT license. See the [LICENSE](LICENSE) file for more info.
## 👥 Author
<div align="center">
<img src="https://raw.githubusercontent.com/thanhdo1110/CTDOWelcomeFX/main/Resources/logo.png" alt="CTDOTECH Logo" width="200"/>
**CTDOTECH Team** - [@thanhdo1110](https://github.com/thanhdo1110)
</div>
---
<div align="center">
<sub>Built with ❤️ by <a href="https://github.com/thanhdo1110">CTDOTECH Team</a></sub>
</div>

167
README_VI.md Normal file
View File

@@ -0,0 +1,167 @@
# 🚀 CTDOWelcomeFX
<div align="right">
<a href="README.md">🇬🇧 English</a>
</div>
<div align="center">
![Version](https://img.shields.io/badge/version-1.0.0-blue.svg)
![License](https://img.shields.io/badge/license-MIT-green.svg)
![Platform](https://img.shields.io/badge/platform-iOS-lightgrey.svg)
![iOS](https://img.shields.io/badge/iOS-13.0+-blue.svg)
</div>
<div align="center">
<img src="https://raw.githubusercontent.com/thanhdo1110/CTDOWelcomeFX/main/Resources/demo.gif" alt="Demo Chế Độ Tối" width="300"/>
<img src="https://raw.githubusercontent.com/thanhdo1110/CTDOWelcomeFX/main/Resources/demo1.gif" alt="Demo Chế Độ Sáng" width="300"/>
</div>
## 📱 Giới Thiệu
Thư viện tạo trải nghiệm WelcomeFX đẹp mắt và có thể tùy chỉnh cho các ứng dụng iOS. Thư viện này cung cấp cách hiện đại và hấp dẫn để giới thiệu các tính năng của ứng dụng cho người dùng mới.
## ✨ Tính Năng
- 🎨 Thiết kế UI hiện đại và sạch sẽ
- 📱 Hỗ trợ cả iPhone và iPad
- 🔄 Hiệu ứng và chuyển động mượt mà
- 🎯 Nội dung và giao diện có thể tùy chỉnh
- 🔒 Tùy chọn hiển thị một lần hoặc mỗi lần khởi động
- 🌐 Hỗ trợ liên kết trong văn bản mô tả
- 🖼️ Tự động sử dụng SF Symbols khi không có hình ảnh
## 🛠 Cài Đặt
### Git Clone
```bash
git clone https://github.com/thanhdo1110/CTDOWelcomeFX.git
```
### Thủ Công
Chỉ cần thêm `CTDOWelcomeFX.h``CTDOWelcomeFX.m` vào dự án của bạn.
## 📖 Cách Sử Dụng
### Triển Khai Cơ Bản
```obj-c++
#import "CTDOWelcomeFX/CTDOWelcomeFX.h"
#import "CTDOWelcomeFX/CTDOWelcomeFXImages.h"
@interface CTDOWelcomeFXTweak : NSObject
+ (void)load;
@end
@implementation CTDOWelcomeFXTweak
+ (void)load {
@autoreleasepool {
// --- 1. Khởi tạo cấu hình ---
CTDOWelcomeFXConfiguration *config = [CTDOWelcomeFXConfiguration defaultConfiguration];
// --- 2. Config tuỳ chỉnh ---
config.appIcon = [CTDOWelcomeFXImages appIconImage];
config.appName = @"ctdotech";
config.welcomeTitle = @"Welcome to";
config.continueButtonText = @"continue";
config.descriptionText = @"Please join my community here...";
config.linkText = @"here...";
config.linkURL = [NSURL URLWithString:@"https://ctdo.net"];
config.userDefaultsKey = @"hasShownMyTweakctdowelcomefx";
config.showEveryLaunch = YES;
config.appNameColor = [UIColor colorWithRed:0.0 green:0.7137 blue:0.7255 alpha:1.0]; // Màu xanh dương
// config.appNameColor = [UIColor colorWithRed:0.0/255.0 green:201.0/255.0 blue:167.0/255.0 alpha:1.0];
// Tạo các features của bạn
CTDOWelcomeFXFeature *feature1 = [[CTDOWelcomeFXFeature alloc]
initWithIcon:[CTDOWelcomeFXImages feature1Image]
title:@"Privacy policy"
subtitle:@"We do not collect any of your information.\nYour security is guaranteed."];
CTDOWelcomeFXFeature *feature2 = [[CTDOWelcomeFXFeature alloc]
initWithIcon:[CTDOWelcomeFXImages feature2Image]
title:@"Interface"
subtitle:@"Smooth, easy, and friendly to use."];
CTDOWelcomeFXFeature *feature3 = [[CTDOWelcomeFXFeature alloc]
initWithIcon:[CTDOWelcomeFXImages feature3Image]
title:@"Features"
subtitle:@"Diverse and innovative for a better experience."];
config.features = @[feature1, feature2, feature3];
// --- 3. Gọi để hiển thị ---
dispatch_async(dispatch_get_main_queue(), ^{
[CTDOWelcomeFXViewController showctdowelcomefxIfNeededWithConfiguration:config
inViewController:nil
completion:^{
NSLog(@"MyTweak by ctdoteam || @dothanh1110");
}];
});
}
}
@end
```
## 🎨 Hướng Dẫn Sử Dụng Ảnh
### 1. App Icon
- Kích thước khuyến nghị: 1024x1024px
- Định dạng: PNG
- Có thể tải từ Assets.xcassets hoặc file PNG
- Có sẵn SF Symbols làm fallback
### 2. Feature Icons
- Kích thước khuyến nghị: 60x60px
- Định dạng: PNG
- Có thể tải từ Assets.xcassets hoặc file PNG
- Có sẵn SF Symbols làm fallback
### 3. SF Symbols
- Sẵn sàng làm fallback khi không có ảnh
- Tự động điều chỉnh kích thước cho các màn hình khác nhau
- Hỗ trợ màu động và chế độ tối
- Ví dụ: "star.fill", "lock.shield.fill", "paintbrush.pointed.fill"
## ⚙️ Cấu Hình
| Thuộc tính | Mô tả |
|------------|--------|
| `appIcon` | Icon của ứng dụng (UIImage hoặc SF Symbol) |
| `welcomeTitle` | Lời chào (ví dụ: "Welcome to") |
| `appName` | Tên ứng dụng |
| `appNameColor` | Màu sắc cho tên ứng dụng |
| `features` | Mảng các tính năng cần hiển thị |
| `descriptionText` | Văn bản mô tả ở dưới cùng |
| `linkText` | Văn bản cần liên kết |
| `linkURL` | URL cho liên kết |
| `continueButtonText` | Văn bản cho nút tiếp tục |
| `userDefaultsKey` | Khóa lưu trạng thái hiển thị |
| `showEveryLaunch` | Có hiển thị mỗi lần khởi động hay không |
## 📋 Yêu Cầu
- iOS 13.0+
- Xcode 11.0+/Theos
- Objective-C/C++
## 📄 Giấy Phép
Dự án này được cung cấp dưới giấy phép MIT. Xem file [LICENSE](LICENSE) để biết thêm chi tiết.
## 👥 Tác Giả
<div align="center">
<img src="https://raw.githubusercontent.com/thanhdo1110/CTDOWelcomeFX/main/Resources/logo.png" alt="CTDOTECH Logo" width="200"/>
**CTDOTECH Team** - [@thanhdo1110](https://github.com/thanhdo1110)
</div>
---
<div align="center">
<sub>Built with ❤️ by <a href="https://github.com/thanhdo1110">CTDOTECH Team</a></sub>
</div>

BIN
Resources/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
Resources/demo1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
Resources/feature1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
Resources/feature2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
Resources/feature3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
Resources/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB