diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..19f5ffa Binary files /dev/null and b/.DS_Store differ diff --git a/CTDOWelcomeFX/.DS_Store b/CTDOWelcomeFX/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/CTDOWelcomeFX/.DS_Store differ diff --git a/CTDOWelcomeFX/CTDOWelcomeFX.h b/CTDOWelcomeFX/CTDOWelcomeFX.h new file mode 100644 index 0000000..49f6bbb --- /dev/null +++ b/CTDOWelcomeFX/CTDOWelcomeFX.h @@ -0,0 +1,70 @@ +// CTDOWelcomeFX.h + +#import + +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 *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 \ No newline at end of file diff --git a/CTDOWelcomeFX/CTDOWelcomeFX.m b/CTDOWelcomeFX/CTDOWelcomeFX.m new file mode 100644 index 0000000..226fc92 --- /dev/null +++ b/CTDOWelcomeFX/CTDOWelcomeFX.m @@ -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 *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 () + +// 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 *welcomeGroupCenteredConstraints; +@property (nonatomic, strong) NSArray *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 \ No newline at end of file diff --git a/README.md b/README.md index f027525..ab63d81 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,167 @@ -# CTDOWelcomeFX +# 🚀 CTDOWelcomeFX + + +
+ +![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) + +
+ +
+ Dark Mode Demo + Light Mode Demo +
+ +## 📱 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 + +
+ CTDOTECH Logo + + **CTDOTECH Team** - [@thanhdo1110](https://github.com/thanhdo1110) + +
+ +--- +
+ Built with ❤️ by CTDOTECH Team +
\ No newline at end of file diff --git a/README_VI.md b/README_VI.md new file mode 100644 index 0000000..a0364a8 --- /dev/null +++ b/README_VI.md @@ -0,0 +1,167 @@ +# 🚀 CTDOWelcomeFX + + + +
+ +![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) + +
+ +
+ Demo Chế Độ Tối + Demo Chế Độ Sáng +
+ +## 📱 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` và `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ả + +
+ CTDOTECH Logo + + **CTDOTECH Team** - [@thanhdo1110](https://github.com/thanhdo1110) + +
+ +--- +
+ Built with ❤️ by CTDOTECH Team +
\ No newline at end of file diff --git a/Resources/demo.gif b/Resources/demo.gif new file mode 100644 index 0000000..b53fe8c Binary files /dev/null and b/Resources/demo.gif differ diff --git a/Resources/demo1.gif b/Resources/demo1.gif new file mode 100644 index 0000000..c98b286 Binary files /dev/null and b/Resources/demo1.gif differ diff --git a/Resources/feature1.png b/Resources/feature1.png new file mode 100644 index 0000000..74c7e66 Binary files /dev/null and b/Resources/feature1.png differ diff --git a/Resources/feature2.png b/Resources/feature2.png new file mode 100644 index 0000000..424595a Binary files /dev/null and b/Resources/feature2.png differ diff --git a/Resources/feature3.png b/Resources/feature3.png new file mode 100644 index 0000000..f8c5762 Binary files /dev/null and b/Resources/feature3.png differ diff --git a/Resources/logo.png b/Resources/logo.png new file mode 100644 index 0000000..d8a72d5 Binary files /dev/null and b/Resources/logo.png differ