Prechádzať zdrojové kódy

Add XCBCMultiTabPageViewController

xingcheng02 2 rokov pred
rodič
commit
b141239baf

+ 5 - 3
XCBaseContainer.podspec

@@ -21,16 +21,18 @@ Pod::Spec.new do |s|
 TODO: Add long description of the pod here.
                        DESC
 
-  s.homepage         = 'https://github.com/xingcheng02/XCBaseContainer'
+  s.homepage         = 'https://git.forgetive.org/XCAppCollection/XCBaseContainer.git'
   # s.screenshots     = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
   s.license          = { :type => 'MIT', :file => 'LICENSE' }
   s.author           = { 'xingcheng02' => 'xingcheng02@baidu.com' }
-  s.source           = { :git => 'https://github.com/xingcheng02/XCBaseContainer.git', :tag => s.version.to_s }
+  s.source           = { :git => 'https://git.forgetive.org/XCAppCollection/XCBaseContainer.git', :tag => s.version.to_s }
   # s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
 
-  s.ios.deployment_target = '10.0'
+  s.ios.deployment_target = '11.0'
 
   s.source_files = 'XCBaseContainer/Classes/**/*'
+  s.dependency 'Masonry'
+  s.dependency 'XCTreeLang'
   
   # s.resource_bundles = {
   #   'XCBaseContainer' => ['XCBaseContainer/Assets/*.png']

+ 22 - 0
XCBaseContainer/Classes/MultiTab/View/XCBCCategorySelectView.h

@@ -0,0 +1,22 @@
+//
+//  XCComCategorySelectView.h
+//  notebook
+//
+//  Created by 邢铖 on 2023/6/6.
+//
+
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface XCBCCategorySelectView : UIView
+
+@property (nonatomic, copy) NSArray<NSString *> *categoryNames;
+@property (nonatomic, assign) CGFloat selectedIndex;
+@property (nonatomic, copy) void(^categoryClickedBlock)(NSInteger newIndex);
+
+@property (nonatomic, assign) CGFloat selectorHeight;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 250 - 0
XCBaseContainer/Classes/MultiTab/View/XCBCCategorySelectView.m

@@ -0,0 +1,250 @@
+//
+//  XCComCategorySelectView.m
+//  notebook
+//
+//  Created by 邢铖 on 2023/6/6.
+//
+
+#import "XCComCategorySelectView.h"
+#import <XCPublicModule/XCPublicModule.h>
+#import <Masonry/Masonry.h>
+#import "OCUtils.h"
+
+static char kXCBCCategorySelectTitleViewKVOKey[] = "kXCBCCategorySelectTitleViewKVOKey";
+
+@interface XCBCCategorySelectTitleView : UIButton
+
+@property (nonatomic, strong) NSString *categoryTitle;
+@property (nonatomic, copy) void(^touchBlock)(void);
+@property (nonatomic, assign) CGFloat selectPrecentage;
+
+@end
+
+@implementation XCBCCategorySelectTitleView
+
+- (instancetype)init {
+    self = [super init];
+    [self addTarget:self
+             action:@selector(onTouchUpInside)
+   forControlEvents:UIControlEventTouchUpInside];
+    [self addObserver:self
+           forKeyPath:@"categoryTitle"
+              options:NSKeyValueObservingOptionNew
+              context:kXCBCCategorySelectTitleViewKVOKey];
+    [self addObserver:self
+           forKeyPath:@"selectPrecentage"
+              options:NSKeyValueObservingOptionNew
+              context:kXCBCCategorySelectTitleViewKVOKey];
+    return self;
+}
+
+- (void)onTouchUpInside {
+    if (self.touchBlock) {
+        self.touchBlock();
+    }
+}
+
+- (void)updateSelectable {
+    CGFloat maximumSize = 20;
+    CGFloat minimumSize = 15;
+    UIFontWeight maximumWeight = UIFontWeightHeavy;
+    UIFontWeight minimumWeight = UIFontWeightThin;
+#define clamp(t, min, max) ((min) + ((max) - (min)) * (t))
+    NSMutableAttributedString *attr = [[NSMutableAttributedString alloc] initWithString:self.categoryTitle];
+    NSRange allRange = NSMakeRange(0, attr.length);
+    [attr setAttributes:@{
+        NSForegroundColorAttributeName : [UIColor grayScaleWithDiff:[self clampValue:self.selectPrecentage
+                                                                       withBoundLeft:0.4
+                                                                       andBoundRight:1]
+                                                              alpha:1]
+//        NSFontAttributeName : [UIFont systemFontOfSize:clamp(self.selectPrecentage, minimumSize, maximumSize)]
+    } range:allRange];
+#undef clamp
+    [self setAttributedTitle:attr forState:UIControlStateNormal];
+    [self layoutSubviews];
+}
+
+- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
+    [super traitCollectionDidChange:previousTraitCollection];
+    [self updateSelectable];
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
+    if (context == kXCBCCategorySelectTitleViewKVOKey) {
+        [self updateSelectable];
+    } else {
+        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
+    }
+}
+
+- (void)dealloc {
+    [self removeObserver:self
+              forKeyPath:@"categoryTitle"
+                 context:kXCBCCategorySelectTitleViewKVOKey];
+    [self removeObserver:self
+              forKeyPath:@"selectPrecentage"
+                 context:kXCBCCategorySelectTitleViewKVOKey];
+}
+
+@end
+
+static char kXCBCCategorySelectViewKVOKey[] = "kXCBCCategorySelectViewKVOKey";
+
+@interface XCBCCategorySelectView ()
+
+@property (nonatomic, strong) UIStackView *stackView;
+@property (nonatomic, strong) UIView *indicatorView;
+
+@end
+
+@implementation XCBCCategorySelectView
+
+- (instancetype)init {
+    self = [super init];
+    self.selectorHeight = 3;
+    [self configureSubviews];
+    return self;
+}
+
+- (UIStackView *)stackView {
+    if (!_stackView) {
+        _stackView = [UIStackView new];
+        _stackView.axis = UILayoutConstraintAxisHorizontal;
+        _stackView.distribution = UIStackViewDistributionEqualSpacing;
+        _stackView.spacing = 10;
+    }
+    return _stackView;
+}
+
+- (UIView *)indicatorView {
+    if (!_indicatorView) {
+        _indicatorView = [UIView new];
+        _indicatorView.backgroundColor = UIColor.systemOrangeColor;
+        _indicatorView.layer.masksToBounds = true;
+        _indicatorView.layer.cornerRadius = 1.5;
+    }
+    return _indicatorView;
+}
+
+- (void)configureSubviews {
+    [self addSubview:self.stackView];
+    [self mas_makeConstraints:^(MASConstraintMaker *make) {
+        make.height.equalTo(@42);
+    }];
+    [self.stackView mas_makeConstraints:^(MASConstraintMaker *make) {
+        make.left.equalTo(self.mas_left);
+        make.right.equalTo(self.mas_right);
+        make.top.equalTo(self.mas_top);
+        make.bottom.equalTo(self.mas_bottom).with.offset(-4);
+    }];
+    [self addSubview:self.indicatorView];
+    [self.indicatorView mas_makeConstraints:^(MASConstraintMaker *make) {
+        make.height.equalTo(@(self.selectorHeight));
+        make.width.equalTo(@0);
+    }];
+    
+    [self addObserver:self
+           forKeyPath:@"categoryNames"
+              options:NSKeyValueObservingOptionNew
+              context:kXCBCCategorySelectViewKVOKey];
+    [self addObserver:self
+           forKeyPath:@"selectedIndex"
+              options:NSKeyValueObservingOptionNew
+              context:kXCBCCategorySelectViewKVOKey];
+}
+
+- (void)reconfigureStackView {
+    for (UIView *view in self.stackView.arrangedSubviews) {
+        [self.stackView removeArrangedSubview:view];
+        [view removeFromSuperview];
+    }
+    NSInteger index = 0;
+    for (NSString *categoryName in self.categoryNames) {
+        XCBCCategorySelectTitleView *button = [XCBCCategorySelectTitleView new];
+        button.categoryTitle = categoryName;
+        XC_WEAKIFY_SELF;
+        button.touchBlock = ^{
+            XC_STRONG_SELF;
+            [self userDidSelectCategoryAtIndex:index];
+        };
+        [self.stackView addArrangedSubview:button];
+        index++;
+    }
+}
+
+- (void)updateSelectedIndex {
+    _selectedIndex = MAX(MIN(self.selectedIndex, self.categoryNames.count - 1), 0);
+    if (self.stackView.arrangedSubviews.count < 2) {
+        
+    } else {
+        for (XCBCCategorySelectTitleView *view in self.stackView.arrangedSubviews) {
+            view.selectPrecentage = 0;
+        }
+        NSUInteger leftObjectIndex = floor(self.selectedIndex);
+        NSUInteger rightObjectIndex = ceil(self.selectedIndex);
+        if (leftObjectIndex == rightObjectIndex) {
+            XCBCCategorySelectTitleView *targetView = self.stackView.arrangedSubviews[leftObjectIndex];
+            [self.indicatorView mas_remakeConstraints:^(MASConstraintMaker *make) {
+                make.left.equalTo(self.mas_left).with.offset(targetView.frame.origin.x);
+                make.width.equalTo(@(targetView.frame.size.width));
+                make.height.equalTo(@(self.selectorHeight));
+                make.bottom.equalTo(self.mas_bottom).with.offset(-0.5);
+            }];
+            targetView.selectPrecentage = 1;
+        }
+        else {
+            CGFloat leftRightPercentageValue = self.selectedIndex - leftObjectIndex;
+            XCBCCategorySelectTitleView *leftView = self.stackView.arrangedSubviews[leftObjectIndex];
+            XCBCCategorySelectTitleView *rightView = self.stackView.arrangedSubviews[rightObjectIndex];
+            CGFloat selectorWidth = [self clampValue:leftRightPercentageValue
+                                       withBoundLeft:leftView.frame.size.width
+                                       andBoundRight:rightView.frame.size.width];
+            CGFloat selectorLeft = [self clampValue:leftRightPercentageValue
+                                      withBoundLeft:leftView.frame.origin.x
+                                      andBoundRight:rightView.frame.origin.x];
+            leftView.selectPrecentage = 1 - leftRightPercentageValue;
+            rightView.selectPrecentage = leftRightPercentageValue;
+            [self.indicatorView mas_remakeConstraints:^(MASConstraintMaker *make) {
+                make.left.equalTo(self.mas_left).with.offset(selectorLeft);
+                make.width.equalTo(@(selectorWidth));
+                make.height.equalTo(@(self.selectorHeight));
+                make.bottom.equalTo(self.mas_bottom).with.offset(-0.5);
+            }];
+        }
+    }
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
+    if (context == kXCBCCategorySelectViewKVOKey) {
+        if ([@"categoryNames" isEqualToString:keyPath]) {
+            [self reconfigureStackView];
+        }
+        if ([@"selectedIndex" isEqualToString:keyPath]) {
+            [self updateSelectedIndex];
+        }
+    } else {
+        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
+    }
+}
+
+- (void)layoutSubviews {
+    [self updateSelectedIndex];
+    [super layoutSubviews];
+}
+
+- (void)userDidSelectCategoryAtIndex:(NSInteger)index {
+    if (self.categoryClickedBlock) {
+        self.categoryClickedBlock(index);
+    }
+}
+
+- (void)dealloc {
+    [self removeObserver:self
+              forKeyPath:@"categoryNames"
+                 context:kXCBCCategorySelectViewKVOKey];
+    [self removeObserver:self
+              forKeyPath:@"selectedIndex"
+                 context:kXCBCCategorySelectViewKVOKey];
+}
+
+@end

+ 21 - 0
XCBaseContainer/Classes/MultiTab/XCBCMultiTabPageViewController.h

@@ -0,0 +1,21 @@
+//
+//  XCBCMultiTabPageViewController.h
+//  notebook
+//
+//  Created by 邢铖 on 2023/6/6.
+//
+
+#import <UIKit/UIKit.h>
+#import <XCPublicModule/XCPublicModule.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface XCBCMultiTabPageViewController : UIViewController
+
+@property (nonatomic, assign) NSUInteger defaultSelectionIndex;
+@property (nonatomic, copy) NSArray<UIViewController *> *managedViewControllers;
+@property (nonatomic, strong, readonly, nullable) UIViewController *selectedViewController;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 210 - 0
XCBaseContainer/Classes/MultiTab/XCBCMultiTabPageViewController.m

@@ -0,0 +1,210 @@
+//
+//  XCBCMultiTabPageViewController.m
+//  notebook
+//
+//  Created by 邢铖 on 2023/6/6.
+//
+
+#import "XCBCMultiTabPageViewController.h"
+#import "XCBCCategorySelectView.h"
+#import <Masonry/Masonry.h>
+#import <XCTreeLang/XCTreeLang.h>
+
+static char kXCComEntryTabPageViewControllerKVOKey[] = "kXCComEntryTabPageViewControllerKVOKey";
+
+@interface XCBCMultiTabPageViewController () <UIScrollViewDelegate>
+
+@property (nonatomic, strong) XCBCCategorySelectView *categorySelectView;
+@property (nonatomic, strong) UIScrollView *scrollView;
+@property (nonatomic, strong) UIStackView *stackView;
+
+@end
+
+@implementation XCBCMultiTabPageViewController
+
+- (instancetype)init {
+    self = [super init];
+    self.defaultSelectionIndex = 0;
+    [self addObserver:self
+           forKeyPath:@"managedViewControllers"
+              options:NSKeyValueObservingOptionNew
+              context:kXCComEntryTabPageViewControllerKVOKey];
+    return self;
+}
+
+- (NSArray<UIViewController *> *)managedViewControllers {
+    if (!_managedViewControllers) {
+        _managedViewControllers = [NSArray new];
+    }
+    return _managedViewControllers;
+}
+
+- (XCBCCategorySelectView *)categorySelectView {
+    if (!_categorySelectView) {
+        _categorySelectView = [XCBCCategorySelectView new];
+        XC_WEAKIFY_SELF;
+        _categorySelectView.categoryClickedBlock = ^(NSInteger newIndex) {
+            XC_STRONG_SELF;
+            [self.categorySelectView animateChangingBlock:^(CGFloat value) {
+                self.categorySelectView.selectedIndex = value;
+                CGPoint offset = self.scrollView.contentOffset;
+                offset.x = self.scrollView.frame.size.width * value;
+                self.scrollView.contentOffset = offset;
+            } from:self.categorySelectView.selectedIndex to:newIndex forTime:0.38];
+        };
+    }
+    return _categorySelectView;
+}
+
+- (UIScrollView *)scrollView {
+    if (!_scrollView) {
+        _scrollView = [UIScrollView new];
+        _scrollView.pagingEnabled = true;
+        _scrollView.showsVerticalScrollIndicator = false;
+        _scrollView.showsHorizontalScrollIndicator = false;
+        _scrollView.delegate = self;
+        _scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
+    }
+    return _scrollView;
+}
+
+- (UIStackView *)stackView {
+    if (!_stackView) {
+        _stackView = [UIStackView new];
+        _stackView.axis = UILayoutConstraintAxisHorizontal;
+        _stackView.distribution = UIStackViewDistributionFillEqually;
+        _stackView.spacing = 0;
+        _stackView.alignment = UIStackViewAlignmentCenter;
+    }
+    return _stackView;
+}
+
+- (void)configureSubviews {
+//    self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"RepeatBackground"]];
+    self.navigationItem.titleView = self.categorySelectView;
+    
+    [self.view addSubview:self.scrollView];
+    [self.scrollView addSubview:self.stackView];
+    [self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
+        make.left.equalTo(self.view.mas_safeAreaLayoutGuideLeft);
+        make.top.equalTo(self.view.mas_top);
+        make.right.equalTo(self.view.mas_right);
+        make.bottom.equalTo(self.view.mas_bottom);
+    }];
+    [self.stackView mas_makeConstraints:^(MASConstraintMaker *make) {
+        make.left.equalTo(self.scrollView.mas_left);
+        make.top.equalTo(self.scrollView.mas_top);
+        make.height.equalTo(self.scrollView.mas_height);
+    }];
+}
+
+- (void)viewDidLoad {
+    [self configureSubviews];
+    dispatch_async(dispatch_get_main_queue(), ^{
+        [super viewDidLoad]; // prepare for treelang
+    });
+}
+
+- (void)configureScrollContents {
+    for (UIView *view in self.stackView.arrangedSubviews) {
+        [self.stackView removeArrangedSubview:view];
+        [view.viewController willMoveToParentViewController:nil];
+        [view removeFromSuperview];
+        [view.viewController removeFromParentViewController];
+    }
+    for (UIViewController *controller in self.managedViewControllers) {
+        [controller willMoveToParentViewController:self];
+        [self addChildViewController:controller];
+        [self.stackView addArrangedSubview:controller.view];
+        [controller.view mas_updateConstraints:^(MASConstraintMaker *make) {
+            make.width.equalTo(self.scrollView.mas_width);
+            make.height.equalTo(self.scrollView.mas_height);
+        }];
+    }
+    [self.stackView mas_updateConstraints:^(MASConstraintMaker *make) {
+        make.width.equalTo(self.scrollView.mas_width).with.multipliedBy(self.managedViewControllers.count);
+    }];
+    self.scrollView.contentSize = CGSizeMake(self.scrollView.frame.size.width * self.managedViewControllers.count, self.scrollView.frame.size.height);
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
+    if (context == kXCComEntryTabPageViewControllerKVOKey) {
+        NSMutableArray *titles = [NSMutableArray new];
+        for (UIViewController *controller in self.managedViewControllers) {
+            [titles addObject:controller.title];
+        }
+        self.categorySelectView.categoryNames = titles;
+        self.categorySelectView.selectedIndex = self.defaultSelectionIndex;
+        [self configureScrollContents];
+        CGPoint offset = self.scrollView.contentOffset;
+        offset.x = self.scrollView.frame.size.width * self.categorySelectView.selectedIndex;
+        self.scrollView.contentOffset = offset;
+    } else {
+        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
+    }
+}
+
+- (nullable UIViewController *)selectedViewController {
+    if (!self.managedViewControllers.count) {
+        return nil;
+    }
+    CGFloat selectedIndex = self.categorySelectView.selectedIndex;
+    NSInteger selectedIndexReal = round(selectedIndex);
+    selectedIndexReal = MIN(MAX(selectedIndexReal, 0), self.managedViewControllers.count - 1);
+    return self.managedViewControllers[selectedIndexReal];
+}
+
+- (void)viewDidLayoutSubviews {
+    CGPoint offset = self.scrollView.contentOffset;
+    offset.x = self.scrollView.frame.size.width * self.categorySelectView.selectedIndex;
+    self.scrollView.contentOffset = offset;
+    [super viewDidLayoutSubviews];
+}
+
+- (void)dealloc {
+    [self removeObserver:self
+              forKeyPath:@"managedViewControllers"
+                 context:kXCComEntryTabPageViewControllerKVOKey];
+}
+
+
+
+@end
+
+@interface XCBCMultiTabPageViewController (Scrolling)
+
+@end
+
+@implementation XCBCMultiTabPageViewController (Scrolling)
+
+- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
+    CGPoint offset = scrollView.contentOffset;
+    offset.y = 0;
+    offset.x = MAX(MIN(offset.x, scrollView.frame.size.width * (self.managedViewControllers.count - 1)), 0);
+    scrollView.contentOffset = offset;
+    CGFloat value = offset.x / scrollView.frame.size.width;
+    self.categorySelectView.selectedIndex = value;
+}
+
+@end
+
+@interface XCBCMultiTabPageViewController (XCT) <XCTLGenerateProtocol>
+
+@end
+
+@implementation XCBCMultiTabPageViewController (XCT)
+
++ (NSObject *)initWithXCT:(NSArray *)arg error:(NSError * _Nullable __autoreleasing *)error {
+    XCBCMultiTabPageViewController *controller = [self new];
+    for (id obj in arg) {
+        if (![obj isKindOfClass:UIViewController.class]) {
+            @throw [NSException exceptionWithName:NSInvalidArgumentException
+                                           reason:@"XCT: Arg can only be UIViewController or __kindof UIViewController"
+                                         userInfo:nil];
+        }
+    }
+    controller.managedViewControllers = arg;
+    return controller;
+}
+
+@end

+ 0 - 0
XCBaseContainer/Classes/ReplaceMe.m