diff --git a/QMUIConfigurationTemplate/QMUIConfigurationTemplate.m b/QMUIConfigurationTemplate/QMUIConfigurationTemplate.m index fbcacfa4..89f71ef0 100644 --- a/QMUIConfigurationTemplate/QMUIConfigurationTemplate.m +++ b/QMUIConfigurationTemplate/QMUIConfigurationTemplate.m @@ -284,7 +284,6 @@ - (void)applyConfigurationTemplate { QMUICMI.shouldFixTabBarSafeAreaInsetsBug = NO; // ShouldFixTabBarSafeAreaInsetsBug : 是否要对 iOS 11 及以后的版本修复当存在 UITabBar 时,UIScrollView 的 inset.bottom 可能错误的 bug(issue #218 #934),默认为 YES QMUICMI.shouldFixSearchBarMaskViewLayoutBug = NO; // ShouldFixSearchBarMaskViewLayoutBug : 是否自动修复 UISearchController.searchBar 被当作 tableHeaderView 使用时可能出现的布局 bug(issue #950) QMUICMI.shouldPrintQMUIWarnLogToConsole = IS_DEBUG; // ShouldPrintQMUIWarnLogToConsole : 是否在出现 QMUILogWarn 时自动把这些 log 以 QMUIConsole 的方式显示到设备屏幕上 - QMUICMI.sendAnalyticsToQMUITeam = YES; // SendAnalyticsToQMUITeam : 是否允许在 DEBUG 模式下上报 Bundle Identifier 和 Display Name 给 QMUI 统计用 QMUICMI.dynamicPreferredValueForIPad = NO; // DynamicPreferredValueForIPad : 当 iPad 处于 Slide Over 或 Split View 分屏模式下,宏 `PreferredValueForXXX` 是否把 iPad 视为某种屏幕宽度近似的 iPhone 来取值。 QMUICMI.ignoreKVCAccessProhibited = NO; // IgnoreKVCAccessProhibited : 是否全局忽略 iOS 13 对 KVC 访问 UIKit 私有属性的限制 QMUICMI.adjustScrollIndicatorInsetsByContentInsetAdjustment = NO; // AdjustScrollIndicatorInsetsByContentInsetAdjustment : 当将 UIScrollView.contentInsetAdjustmentBehavior 设为 UIScrollViewContentInsetAdjustmentNever 时,是否自动将 UIScrollView.automaticallyAdjustsScrollIndicatorInsets 设为 NO,以保证原本在 iOS 12 下的代码不用修改就能在 iOS 13 下正常控制滚动条的位置。 diff --git a/QMUIKit.podspec b/QMUIKit.podspec index 9d3d49b9..f6fb647d 100644 --- a/QMUIKit.podspec +++ b/QMUIKit.podspec @@ -1,18 +1,18 @@ Pod::Spec.new do |s| s.name = "QMUIKit" - s.version = "4.6.3" + s.version = "4.7.0" s.summary = "致力于提高项目 UI 开发效率的解决方案" s.description = <<-DESC QMUI iOS 是一个致力于提高项目 UI 开发效率的解决方案,其设计目的是用于辅助快速搭建一个具备基本设计还原效果的 iOS 项目,同时利用自身提供的丰富控件及兼容处理, 让开发者能专注于业务需求而无需耗费精力在基础代码的设计上。不管是新项目的创建,或是已有项目的维护,均可使开发效率和项目质量得到大幅度提升。 DESC - s.homepage = "https://qmuiteam.com/ios" + s.homepage = "https://github.com/Tencent/QMUI_iOS" s.license = 'MIT' s.author = {"qmuiteam" => "contact@qmuiteam.com"} s.source = {:git => "https://github.com/Tencent/QMUI_iOS.git", :tag => s.version.to_s} #s.source = {:git => "https://github.com/Tencent/QMUI_iOS.git", :branch => 'master'} s.social_media_url = 'https://github.com/Tencent/QMUI_iOS' s.requires_arc = true - s.documentation_url = 'https://qmuiteam.com/ios/page/document.html' + s.documentation_url = 'https://github.com/Tencent/QMUI_iOS' s.screenshot = 'https://cloud.githubusercontent.com/assets/1190261/26751376/63f96538-486a-11e7-81cf-5bc83a945207.png' s.platform = :ios, '13.0' diff --git a/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m b/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m index 3eb407a9..d1bd78c5 100644 --- a/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m +++ b/QMUIKit/QMUIComponents/ImagePickerLibrary/QMUIImagePickerViewController.m @@ -122,6 +122,16 @@ - (void)viewWillAppear:(BOOL)animated { [self.collectionView reloadData]; } +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + + // 在 pop 回相簿列表时重置标志位以使下次进来 picker 时 collection 可以滚动到正确的初始位置 + // 但不能影响从 picker 进入大图的路径 + if (self.navigationController && ![self.navigationController.viewControllers containsObject:self]) { + self.hasScrollToInitialPosition = NO; + } +} + - (void)showEmptyView { [super showEmptyView]; self.emptyView.backgroundColor = self.view.backgroundColor; // 为了盖住背后的 collectionView,这里加个背景色(不盖住的话会看到 collectionView 先滚到列表顶部然后跳到列表底部) @@ -244,10 +254,6 @@ - (void)scrollToInitialPositionIfNeeded { } } -- (void)willPopInNavigationControllerWithAnimated:(BOOL)animated { - self.hasScrollToInitialPosition = NO; -} - #pragma mark - Getters & Setters @synthesize collectionViewLayout = _collectionViewLayout; diff --git a/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.h b/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.h index 94d0b067..a0b3b796 100644 --- a/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.h +++ b/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.h @@ -23,7 +23,13 @@ @interface _QMUITransitionNavigationBar : UINavigationBar +@property(nonatomic, weak) UIViewController *parentViewController; + // 建立假 bar 到真 bar 的关系,内部会通过 qmuinb_copyStylesToBar 同时设置真 bar 到假 bar 的关系 @property(nonatomic, weak) UINavigationBar *originalNavigationBar; + @property(nonatomic, assign) BOOL shouldPreventAppearance; + +// 根据当前的系统导航栏布局,刷新自身在 vc.view 上的布局 +- (void)updateLayout; @end diff --git a/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m b/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m index 9ea83284..3e1c5ea7 100644 --- a/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m +++ b/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationBar+Transition.m @@ -26,32 +26,63 @@ + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ -#ifdef IOS15_SDK_ALLOWED if (@available(iOS 15.0, *)) { - ExtendImplementationOfVoidMethodWithSingleArgument([UINavigationBar class], @selector(setStandardAppearance:), UINavigationBarAppearance *, ^(UINavigationBar *selfObject, UINavigationBarAppearance *appearance) { - if (selfObject.qmuinb_copyStylesToBar) { - selfObject.qmuinb_copyStylesToBar.standardAppearance = appearance; - } + + OverrideImplementation([UINavigationBar class], @selector(setStandardAppearance:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject, UINavigationBarAppearance *appearance) { + + // call super + void (*originSelectorIMP)(id, SEL, UINavigationBarAppearance *); + originSelectorIMP = (void (*)(id, SEL, UINavigationBarAppearance *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, appearance); + + if (selfObject.qmuinb_copyStylesToBar) { + selfObject.qmuinb_copyStylesToBar.standardAppearance = appearance; + } + }; }); - ExtendImplementationOfVoidMethodWithSingleArgument([UINavigationBar class], @selector(setScrollEdgeAppearance:), UINavigationBarAppearance *, ^(UINavigationBar *selfObject, UINavigationBarAppearance *appearance) { - if (selfObject.qmuinb_copyStylesToBar) { - selfObject.qmuinb_copyStylesToBar.scrollEdgeAppearance = appearance; - } + OverrideImplementation([UINavigationBar class], @selector(setScrollEdgeAppearance:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject, UINavigationBarAppearance *appearance) { + + // call super + void (*originSelectorIMP)(id, SEL, UINavigationBarAppearance *); + originSelectorIMP = (void (*)(id, SEL, UINavigationBarAppearance *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, appearance); + + if (selfObject.qmuinb_copyStylesToBar) { + selfObject.qmuinb_copyStylesToBar.standardAppearance = appearance; + } + }; }); } -#endif - ExtendImplementationOfVoidMethodWithSingleArgument([UINavigationBar class], @selector(setBarStyle:), UIBarStyle, ^(UINavigationBar *selfObject, UIBarStyle barStyle) { - if (selfObject.qmuinb_copyStylesToBar && selfObject.qmuinb_copyStylesToBar.barStyle != barStyle) { - selfObject.qmuinb_copyStylesToBar.barStyle = barStyle; - } + OverrideImplementation([UINavigationBar class], @selector(setBarStyle:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject, UIBarStyle barStyle) { + + // call super + void (*originSelectorIMP)(id, SEL, UIBarStyle); + originSelectorIMP = (void (*)(id, SEL, UIBarStyle))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, barStyle); + + if (selfObject.qmuinb_copyStylesToBar && selfObject.qmuinb_copyStylesToBar.barStyle != barStyle) { + selfObject.qmuinb_copyStylesToBar.barStyle = barStyle; + } + }; }); - ExtendImplementationOfVoidMethodWithSingleArgument([UINavigationBar class], @selector(setBarTintColor:), UIColor *, ^(UINavigationBar *selfObject, UIColor *barTintColor) { - if (selfObject.qmuinb_copyStylesToBar && ![selfObject.qmuinb_copyStylesToBar.barTintColor isEqual:barTintColor]) { - selfObject.qmuinb_copyStylesToBar.barTintColor = barTintColor; - } + OverrideImplementation([UINavigationBar class], @selector(setBarTintColor:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject, UIColor *barTintColor) { + + // call super + void (*originSelectorIMP)(id, SEL, UIColor *); + originSelectorIMP = (void (*)(id, SEL, UIColor *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, barTintColor); + + if (selfObject.qmuinb_copyStylesToBar && ![selfObject.qmuinb_copyStylesToBar.barTintColor isEqual:barTintColor]) { + selfObject.qmuinb_copyStylesToBar.barTintColor = barTintColor; + } + }; }); OverrideImplementation([UINavigationBar class], @selector(setBackgroundImage:forBarMetrics:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { @@ -68,10 +99,18 @@ + (void)load { }; }); - ExtendImplementationOfVoidMethodWithSingleArgument([UINavigationBar class], @selector(setShadowImage:), UIImage *, ^(UINavigationBar *selfObject, UIImage *firstArgv) { - if (selfObject.qmuinb_copyStylesToBar) { - selfObject.qmuinb_copyStylesToBar.shadowImage = firstArgv; - } + OverrideImplementation([UINavigationBar class], @selector(setShadowImage:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UINavigationBar *selfObject, UIImage *shadowImage) { + + // call super + void (*originSelectorIMP)(id, SEL, UIImage *); + originSelectorIMP = (void (*)(id, SEL, UIImage *))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, shadowImage); + + if (selfObject.qmuinb_copyStylesToBar) { + selfObject.qmuinb_copyStylesToBar.shadowImage = shadowImage; + } + }; }); OverrideImplementation([UINavigationBar class], @selector(setQmui_effect:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { @@ -211,6 +250,8 @@ - (void)setOriginalNavigationBar:(UINavigationBar *)originBar { // 只复制当前 originBar 的样式,所以复制完立马就清空 originBar.qmuinb_copyStylesToBar = self; originBar.qmuinb_copyStylesToBar = nil; + + [self updateLayout]; } - (void)layoutSubviews { @@ -228,4 +269,13 @@ - (void)didAddSubview:(UIView *)subview { } } +- (void)updateLayout { + if ([self.parentViewController isViewLoaded] && self.originalNavigationBar) { + [self.parentViewController.view bringSubviewToFront:self]; + UIView *backgroundView = self.originalNavigationBar.qmui_backgroundView; + CGRect rect = [backgroundView.superview convertRect:backgroundView.frame toView:self.parentViewController.view]; + self.frame = CGRectSetX(rect, 0);// push/pop 过程中系统的导航栏转换过来的 x 可能是 112、-112 + } +} + @end diff --git a/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m b/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m index 93c979c4..6d4a3456 100644 --- a/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m +++ b/QMUIKit/QMUIComponents/NavigationBarTransition/UINavigationController+NavigationBarTransition.m @@ -113,7 +113,7 @@ + (void)load { switch (action) { case QMUINavigationActionDidPush: case QMUINavigationActionWillPop: - case QMUINavigationActionWillSet: { + case QMUINavigationActionDidSet: { BOOL shouldCustomNavigationBarTransition = [weakNavigationController shouldCustomTransitionAutomaticallyForOperation:UINavigationControllerOperationPush firstViewController:disappearingViewController secondViewController:appearingViewController]; if (shouldCustomNavigationBarTransition) { @@ -173,9 +173,7 @@ + (void)load { OverrideImplementation([UIViewController class], @selector(viewWillLayoutSubviews), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UIViewController *selfObject) { - if (selfObject.transitionNavigationBar) { - [selfObject layoutTransitionNavigationBar]; - } + [selfObject.transitionNavigationBar updateLayout]; // call super void (*originSelectorIMP)(id, SEL); @@ -233,24 +231,26 @@ - (void)addTransitionNavigationBarAndBindNavigationBar:(BOOL)shouldBind { } _QMUITransitionNavigationBar *customBar = [[_QMUITransitionNavigationBar alloc] init]; + customBar.parentViewController = self; self.transitionNavigationBar = customBar; // iOS 15 里,假 bar 在 add 到界面上时会被强制同步为 UIAppearance 的值,不管你之前是否设置过自己的样式。而且在那个 runloop 内不管你后续怎么更新 standardAppearance,都会呈现出 UIAppearance 里的统一的值的样式。所以这里一方面屏蔽 didMoveToWindow,从而避免在这时候应用 UIAppearance,另一方面要保证先 add 到界面上再同步当前导航栏的样式。 - // 经测试只有 push 时需要这么处理,pop 没问题 + // 经测试只有 push 或 push 动画的 set 需要这么处理,pop 及 pop 动画的 set 没问题 // iOS 14 及以下没这种问题。 -#ifdef IOS15_SDK_ALLOWED + // https://github.com/Tencent/QMUI_iOS/issues/1501 if (@available(iOS 15.0, *)) { - if (self.navigationController.qmui_navigationAction == QMUINavigationActionDidPush) { + BOOL isPush = self.navigationController.qmui_navigationAction == QMUINavigationActionDidPush; + BOOL isSet = self.navigationController.qmui_navigationAction == QMUINavigationActionDidSet; + BOOL isPopAnimation = isSet && self.navigationController.qmui_lastOperation == UINavigationControllerOperationPop; + if (isPush || (isSet && !isPopAnimation)) { customBar.shouldPreventAppearance = YES; } } -#endif [self.view addSubview:customBar]; customBar.originalNavigationBar = self.navigationController.navigationBar;// 注意这里内部不会保留真 bar 和假 bar 的 copy 关系 if (shouldBind) { self.navigationController.navigationBar.qmuinb_copyStylesToBar = customBar; } - [self layoutTransitionNavigationBar]; } - (void)removeTransitionNavigationBar { @@ -264,15 +264,6 @@ - (void)removeTransitionNavigationBar { } } -- (void)layoutTransitionNavigationBar { - if (self.isViewLoaded && self.navigationController) { - UIView *backgroundView = self.navigationController.navigationBar.qmui_backgroundView; - CGRect rect = [backgroundView.superview convertRect:backgroundView.frame toView:self.view]; - self.transitionNavigationBar.frame = CGRectSetX(rect, 0);// push/pop 过程中系统的导航栏转换过来的 x 可能是 112、-112 - [self.view bringSubviewToFront:self.transitionNavigationBar];// 避免在后续被其他 subviews 盖住 - } -} - #pragma mark - 工具方法 // 根据当前的viewController,统一处理导航栏的显隐、样式 diff --git a/QMUIKit/QMUIComponents/QMUIAlertController.m b/QMUIKit/QMUIComponents/QMUIAlertController.m index c75b865e..9b9718c3 100644 --- a/QMUIKit/QMUIComponents/QMUIAlertController.m +++ b/QMUIKit/QMUIComponents/QMUIAlertController.m @@ -759,7 +759,7 @@ - (void)viewDidLayoutSubviews { } // 把上下的margin都加上用于跟整个屏幕的高度做比较 CGFloat contentHeight = contentOriginY + UIEdgeInsetsGetVerticalValue(self.sheetContentMargin); - CGFloat screenSpaceHeight = CGRectGetHeight(self.view.bounds); + CGFloat screenSpaceHeight = CGRectGetHeight(self.view.bounds) - SafeAreaInsetsConstantForDeviceWithNotch.top - (self.isExtendBottomLayout ? 0 : SafeAreaInsetsConstantForDeviceWithNotch.bottom); if (contentHeight > screenSpaceHeight) { CGFloat cancelButtonAreaHeight = (self.cancelAction ? (CGRectGetHeight(self.cancelAction.button.bounds) + self.sheetCancelButtonMarginTop) : 0); screenSpaceHeight = screenSpaceHeight - cancelButtonAreaHeight - UIEdgeInsetsGetVerticalValue(self.sheetContentMargin); @@ -789,7 +789,7 @@ - (void)viewDidLayoutSubviews { contentHeight -= self.sheetContentMargin.top; } - self.containerView.qmui_frameApplyTransform = CGRectMake((CGRectGetWidth(self.view.bounds) - CGRectGetWidth(self.containerView.frame)) / 2, screenSpaceHeight - contentHeight - SafeAreaInsetsConstantForDeviceWithNotch.bottom, CGRectGetWidth(self.containerView.frame), contentHeight + (self.isExtendBottomLayout ? SafeAreaInsetsConstantForDeviceWithNotch.bottom : 0)); + self.containerView.qmui_frameApplyTransform = CGRectMake((CGRectGetWidth(self.view.bounds) - CGRectGetWidth(self.containerView.frame)) / 2, SafeAreaInsetsConstantForDeviceWithNotch.top + screenSpaceHeight - contentHeight, CGRectGetWidth(self.containerView.frame), contentHeight + (self.isExtendBottomLayout ? SafeAreaInsetsConstantForDeviceWithNotch.bottom : 0)); self.extendLayer.frame = CGRectFlatMake(0, CGRectGetHeight(self.containerView.bounds) - SafeAreaInsetsConstantForDeviceWithNotch.bottom - 1, CGRectGetWidth(self.containerView.bounds), SafeAreaInsetsConstantForDeviceWithNotch.bottom + 1); } diff --git a/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.m b/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.m index 98bf2ad4..c6da9bee 100644 --- a/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.m +++ b/QMUIKit/QMUIComponents/QMUIBadge/UIView+QMUIBadge.m @@ -405,7 +405,8 @@ - (void)updateLayoutSubviewsBlockIfNeeded { } } -- (UIView *)findBarButtonImageViewIfOffsetByTopRight:(BOOL)offsetByTopRight { +// 不管 image 还是 text 的 UIBarButtonItem 都获取内部的 _UIModernBarButton 即可 +- (UIView *)findBarButtonContentViewIfOffsetByTopRight:(BOOL)offsetByTopRight { NSString *classString = NSStringFromClass(self.class); if ([classString isEqualToString:@"UITabBarButton"]) { // 特别的,对于 UITabBarItem,将 imageView 作为参考 view @@ -420,10 +421,7 @@ - (UIView *)findBarButtonImageViewIfOffsetByTopRight:(BOOL)offsetByTopRight { if ([classString isEqualToString:@"_UIButtonBarButton"]) { for (UIView *subview in self.subviews) { if ([subview isKindOfClass:UIButton.class]) { - UIView *imageView = ((UIButton *)subview).imageView; - if (imageView && !imageView.hidden) { - return imageView; - } + return subview; } } } @@ -437,9 +435,9 @@ - (void)qmuibdg_layoutSubviews { BOOL offsetByTopRight = !CGPointEqualToPoint(badgeView.offset, QMUIBadgeInvalidateOffset) || !CGPointEqualToPoint(badgeView.offsetLandscape, QMUIBadgeInvalidateOffset); CGPoint offset = IS_LANDSCAPE ? (offsetByTopRight ? badgeView.offsetLandscape : badgeView.centerOffsetLandscape) : (offsetByTopRight ? badgeView.offset : badgeView.centerOffset); - UIView *imageView = [view findBarButtonImageViewIfOffsetByTopRight:offsetByTopRight]; - if (imageView) { - CGRect imageViewFrame = [view convertRect:imageView.frame fromView:imageView.superview]; + UIView *contentView = [view findBarButtonContentViewIfOffsetByTopRight:offsetByTopRight]; + if (contentView) { + CGRect imageViewFrame = [view convertRect:contentView.frame fromView:contentView.superview]; if (offsetByTopRight) { badgeView.frame = CGRectSetXY(badgeView.frame, CGRectGetMaxX(imageViewFrame) + offset.x, CGRectGetMinY(imageViewFrame) - CGRectGetHeight(badgeView.frame) + offset.y); } else { diff --git a/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.m b/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.m index 61933b34..02983fb4 100644 --- a/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.m +++ b/QMUIKit/QMUIComponents/QMUIButton/QMUIButton.m @@ -501,10 +501,8 @@ - (void)setHighlighted:(BOOL)highlighted { - (void)setEnabled:(BOOL)enabled { [super setEnabled:enabled]; - if (!enabled && self.adjustsButtonWhenDisabled) { - self.alpha = ButtonDisabledAlpha; - } else { - self.alpha = 1; + if (self.adjustsButtonWhenDisabled) { + self.alpha = enabled ? 1 : ButtonDisabledAlpha; } } diff --git a/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m b/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m index 5779769d..26d8e2b0 100644 --- a/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m +++ b/QMUIKit/QMUIComponents/QMUIButton/QMUINavigationButton.m @@ -24,6 +24,8 @@ #import "NSString+QMUI.h" #import "UINavigationController+QMUI.h" #import "UINavigationItem+QMUI.h" +#import "UINavigationBar+QMUI.h" +#import "NSArray+QMUI.h" typedef NS_ENUM(NSInteger, QMUINavigationButtonPosition) { QMUINavigationButtonPositionNone = -1, // 不处于navigationBar最左(右)边的按钮,则使用None。用None则不会在alignmentRectInsets里调整位置 @@ -107,6 +109,7 @@ - (void)renderButtonStyle { } break; case QMUINavigationButtonTypeBack: { + self.qmui_outsideEdge = UIEdgeInsetsMake(-12, -12, -24, -24); UIImage *backIndicatorImage = UINavigationBar.qmui_appearanceConfigured.backIndicatorImage; if (!backIndicatorImage) { // 配置表没有自定义的图片,则按照系统的返回按钮图片样式创建一张,颜色按照 tintColor 来 @@ -362,7 +365,7 @@ + (void)load { for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); index++) { SEL originalSelector = selectors[index]; SEL swizzledSelector = NSSelectorFromString([@"qmui_" stringByAppendingString:NSStringFromSelector(originalSelector)]); - ExchangeImplementations([self class], originalSelector, swizzledSelector); + ExchangeImplementations([UINavigationItem class], originalSelector, swizzledSelector); } }); } @@ -477,6 +480,48 @@ + (void)load { }; }); + // 系统的 UIBarButtonItem 响应区域比较大,如果用 customView 则响应区域只有 customView.frame 的大小,这里专门扩大它 + // 对没用 customView 的不处理 + OverrideImplementation([UINavigationBar class], @selector(hitTest:withEvent:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIView *(UINavigationBar *selfObject, CGPoint firstArgv, UIEvent *secondArgv) { + + // call super + UIView * (*originSelectorIMP)(id, SEL, CGPoint, UIEvent *); + originSelectorIMP = (UIView * (*)(id, SEL, CGPoint, UIEvent *))originalIMPProvider(); + UIView * result = originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv); + + BOOL hitNothing = !result || result == selfObject.qmui_contentView || [NSStringFromClass(result.class) containsString:@"StackView"]; + if (!hitNothing) return result; + + NSMutableArray *customViews = [[NSMutableArray alloc] init]; + if (selfObject.topItem.titleView) { + [customViews addObject:selfObject.topItem.titleView]; + } + [customViews addObjectsFromArray:[selfObject.topItem.leftBarButtonItems qmui_compactMapWithBlock:^id _Nullable(UIBarButtonItem * _Nonnull item) { + return item.customView ?: nil; + }]]; + [customViews addObjectsFromArray:[selfObject.topItem.rightBarButtonItems qmui_compactMapWithBlock:^id _Nullable(UIBarButtonItem * _Nonnull item) { + return item.customView ?: nil; + }]]; + UIView *hitTestingView = [customViews qmui_firstMatchWithBlock:^BOOL(UIView * _Nonnull item) { + if (!CGRectIsEmpty(item.frame) && !item.hidden && item.alpha > 0.01 && item.window) { + if ([item isKindOfClass:UIControl.class] && !((UIControl *)item).enabled) { + return NO; + } + CGRect rect = [selfObject convertRect:item.bounds fromView:item]; + rect = CGRectInsetEdges(rect, item.qmui_outsideEdge); + if (CGRectContainsPoint(rect, firstArgv)) { + return YES; + } + } + return NO; + }]; + if (hitTestingView) { + return hitTestingView; + } + return result; + }; + }); }); } diff --git a/QMUIKit/QMUIComponents/QMUICellHeightCache.m b/QMUIKit/QMUIComponents/QMUICellHeightCache.m index 787ca5c2..d8d9db0c 100644 --- a/QMUIKit/QMUIComponents/QMUICellHeightCache.m +++ b/QMUIKit/QMUIComponents/QMUICellHeightCache.m @@ -222,7 +222,7 @@ + (void)load { for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); ++index) { SEL originalSelector = selectors[index]; SEL swizzledSelector = NSSelectorFromString([@"qmuiTableCache_" stringByAppendingString:NSStringFromSelector(originalSelector)]); - ExchangeImplementations([self class], originalSelector, swizzledSelector); + ExchangeImplementations([UITableView class], originalSelector, swizzledSelector); } }); } @@ -526,7 +526,7 @@ + (void)load { for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); index++) { SEL originalSelector = selectors[index]; SEL swizzledSelector = NSSelectorFromString([@"qmuiCollectionCache_" stringByAppendingString:NSStringFromSelector(originalSelector)]); - ExchangeImplementations([self class], originalSelector, swizzledSelector); + ExchangeImplementations([UICollectionView class], originalSelector, swizzledSelector); } }); } diff --git a/QMUIKit/QMUIComponents/QMUIKeyboardManager.m b/QMUIKit/QMUIComponents/QMUIKeyboardManager.m index ad1fb54a..2bb1aa2b 100644 --- a/QMUIKit/QMUIComponents/QMUIKeyboardManager.m +++ b/QMUIKit/QMUIComponents/QMUIKeyboardManager.m @@ -19,6 +19,7 @@ #import "QMUIAppearance.h" #import "QMUIMultipleDelegates.h" #import "NSArray+QMUI.h" +#import "UIView+QMUI.h" @class QMUIKeyboardViewFrameObserver; @protocol QMUIKeyboardViewFrameObserverDelegate @@ -935,7 +936,10 @@ + (CGRect)currentKeyboardFrame { + (CGFloat)visibleKeyboardHeight { UIView *keyboardView = [self keyboardView]; - UIWindow *keyboardWindow = keyboardView.window; + // iPad“侧拉”模式打开的 App,App Window 和键盘 Window 尺寸不同,如果以键盘 Window 为准则会认为键盘一直在屏幕上,从而出现误判,所以这里改为用 App Window。 + // iPhone、iPad 全屏/分屏/台前调度,都没这个问题 +// UIWindow *keyboardWindow = keyboardView.window; + UIWindow *keyboardWindow = UIApplication.sharedApplication.delegate.window; if (!keyboardView || !keyboardWindow) { return 0; } else { @@ -945,7 +949,8 @@ + (CGFloat)visibleKeyboardHeight { return 0; } - CGRect visibleRect = CGRectIntersection(CGRectFlatted(keyboardWindow.bounds), CGRectFlatted(keyboardView.frame)); + CGRect keyboardFrame = [keyboardWindow qmui_convertRect:keyboardView.bounds fromView:keyboardView]; + CGRect visibleRect = CGRectIntersection(keyboardWindow.bounds, keyboardFrame); if (CGRectIsValidated(visibleRect)) { return CGRectGetHeight(visibleRect); } diff --git a/QMUIKit/QMUIComponents/QMUISearchController.m b/QMUIKit/QMUIComponents/QMUISearchController.m index c9fec143..ef8c9a09 100644 --- a/QMUIKit/QMUIComponents/QMUISearchController.m +++ b/QMUIKit/QMUIComponents/QMUISearchController.m @@ -43,7 +43,12 @@ @implementation QMUISearchResultsTableViewController - (void)initTableView { [super initTableView]; + + // UISearchController.searchBar 作为 UITableView.tableHeaderView 时,进入搜索状态,搜索结果列表顶部有一大片空白 + // 不要让系统自适应了,否则在搜索结果(navigationBar 隐藏)push 进入下一级界面(navigationBar 显示)过程中系统自动调整的 contentInset 会跳来跳去 + // https://github.com/Tencent/QMUI_iOS/issues/1473 self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag; if ([self.delegate respondsToSelector:@selector(didLoadTableViewInSearchResultsTableViewController:)]) { [self.delegate didLoadTableViewInSearchResultsTableViewController:self]; diff --git a/QMUIKit/QMUIComponents/QMUITheme/UIView+QMUITheme.m b/QMUIKit/QMUIComponents/QMUITheme/UIView+QMUITheme.m index b258ee7d..32f91c15 100644 --- a/QMUIKit/QMUIComponents/QMUITheme/UIView+QMUITheme.m +++ b/QMUIKit/QMUIComponents/QMUITheme/UIView+QMUITheme.m @@ -152,16 +152,22 @@ - (void)qmui_themeDidChangeByManager:(QMUIThemeManager *)manager identifier:(__k } if ([self isKindOfClass:UITextView.class]) { - UITextView *textView = (UITextView *)self; +#ifdef IOS16_SDK_ALLOWED if (@available(iOS 16.0, *)) { - // iOS 16 里无法通过 setNeedsDisplay 去刷新文本颜色了,所以只能重新把 textColor 设置一遍 - // 测过 textColor 和 typingAttributes[NSForegroundColorAttributeName] 是互通的,所以只操作任意一个即可 - if (textView.textColor.qmui_isQMUIDynamicColor) { - textView.textColor = textView.textColor; + // iOS 16 里使用 TextKit 2 的输入框无法通过 setNeedsDisplay 去刷新文本颜色了,所以改为用这种方式去刷新 + // 以下语句对 iOS 16 里因为访问 UITextView.layoutManager 而回退到 TextKit 1 的输入框无效,但由于 TextKit 1 本来就可以正常刷新,所以没问题。 + // 注意要考虑输入框内可能存在多种颜色的富文本场景 + UITextView *textView = (UITextView *)self; + NSTextRange *textRange = textView.textLayoutManager.textContentManager.documentRange; + if (textRange) { + [textView.textLayoutManager invalidateLayoutForRange:textRange]; } } else { +#endif [self setNeedsDisplay]; +#ifdef IOS16_SDK_ALLOWED } +#endif } // 输入框、搜索框的键盘跟随主题变化 diff --git a/QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.m b/QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.m index 55a1ca55..a926adad 100644 --- a/QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.m +++ b/QMUIKit/QMUIComponents/QMUIWindowSizeMonitor.m @@ -18,7 +18,7 @@ @interface NSObject (QMUIWindowSizeMonitor_Private) -@property(nonatomic, readonly) NSMutableArray *qwsm_windowSizeChangeHandlers; +@property(nonatomic, readonly) NSMutableDictionary *qwsm_windowSizeChangeHandlers; @end @@ -58,21 +58,21 @@ - (void)qmui_addSizeObserverForWindow:(UIWindow *)window handler:(QMUIWindowSize }; void * blockFuncPtr = ((__bridge struct Block_literal *)handler)->__FuncPtr; - for (QMUIWindowSizeObserverHandler handler in self.qwsm_windowSizeChangeHandlers) { + for (QMUIWindowSizeObserverHandler handler in self.qwsm_windowSizeChangeHandlers.allKeys) { // 由于利用 block 的 __FuncPtr 指针来判断同一个实现的 block 过滤掉,防止重复添加监听 if (((__bridge struct Block_literal *)handler)->__FuncPtr == blockFuncPtr) { return; } } - [self.qwsm_windowSizeChangeHandlers addObject:handler]; + self.qwsm_windowSizeChangeHandlers[(id)handler] = [[QMUIWeakObjectContainer alloc] initWithObject:window]; [window qwsm_addSizeObserver:self]; } -- (NSMutableArray *)qwsm_windowSizeChangeHandlers { - NSMutableArray *_handlers = objc_getAssociatedObject(self, _cmd); +- (NSMutableDictionary *)qwsm_windowSizeChangeHandlers { + NSMutableDictionary *_handlers = objc_getAssociatedObject(self, _cmd); if (!_handlers) { - _handlers = [NSMutableArray array]; + _handlers = [[NSMutableDictionary alloc] init]; objc_setAssociatedObject(self, _cmd, _handlers, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return _handlers; @@ -158,10 +158,11 @@ - (void)qwsm_notifyWithNewSize:(CGSize)newSize { // notify sizeObservers for (NSUInteger i = 0, count = self.qwsm_sizeObservers.count; i < count; i++) { NSObject *object = [self.qwsm_sizeObservers pointerAtIndex:i]; - for (NSUInteger i = 0, count = object.qwsm_windowSizeChangeHandlers.count; i < count; i++) { - QMUIWindowSizeObserverHandler handler = object.qwsm_windowSizeChangeHandlers[i]; - handler(newSize); - } + [object.qwsm_windowSizeChangeHandlers enumerateKeysAndObjectsUsingBlock:^(QMUIWindowSizeObserverHandler _Nonnull key, QMUIWeakObjectContainer * _Nonnull obj, BOOL * _Nonnull stop) { + if (obj.object == self) { + key(newSize); + } + }]; } // send ‘windowDidTransitionToSize:’ to responders for (NSUInteger i = 0, count = self.qwsm_canReceiveWindowDidTransitionToSizeResponders.count; i < count; i++) { diff --git a/QMUIKit/QMUICore/QMUICommonDefines.h b/QMUIKit/QMUICore/QMUICommonDefines.h index 17b9e739..04139300 100644 --- a/QMUIKit/QMUICore/QMUICommonDefines.h +++ b/QMUIKit/QMUICore/QMUICommonDefines.h @@ -303,7 +303,7 @@ AddAccessibilityHint(NSObject *obj, NSString *hint) { /// 与 NSAssert 的差异在于,当你使用 NSAssert 时,整条语句默认不会出现在 Release 包里,但 QMUIAssert 依然会存在。 /// 用法:QMUIAssert(a != b, @"UIView (QMUI)", @"xxxx") /// 用法:QMUIAssert(a != b, @"UIView (QMUI)", @"%@, xxx", @"xxx") -#define QMUIAssert(_condition, _categoryName, ...) ({if (!(_condition)) {QMUILogWarn(_categoryName, __VA_ARGS__);if (QMUICMIActivated && !ShouldPrintQMUIWarnLogToConsole) {NSAssert(NO, __VA_ARGS__);}}}) +#define QMUIAssert(_condition, _categoryName, ...) ({if (!(_condition)) {QMUILogWarn(_categoryName, __VA_ARGS__);if (!QMUICMIActivated || !ShouldPrintQMUIWarnLogToConsole) {NSAssert(NO, __VA_ARGS__);}}}) #pragma mark - Selector diff --git a/QMUIKit/QMUICore/QMUIConfiguration.h b/QMUIKit/QMUICore/QMUIConfiguration.h index 7ad84adb..270b5f19 100644 --- a/QMUIKit/QMUICore/QMUIConfiguration.h +++ b/QMUIKit/QMUICore/QMUIConfiguration.h @@ -283,7 +283,6 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, assign) BOOL navigationBarHiddenInitially; @property(nonatomic, assign) BOOL shouldFixTabBarSafeAreaInsetsBug; @property(nonatomic, assign) BOOL shouldFixSearchBarMaskViewLayoutBug; -@property(nonatomic, assign) BOOL sendAnalyticsToQMUITeam; @property(nonatomic, assign) BOOL dynamicPreferredValueForIPad; @property(nonatomic, assign) BOOL ignoreKVCAccessProhibited API_AVAILABLE(ios(13.0)); @property(nonatomic, assign) BOOL adjustScrollIndicatorInsetsByContentInsetAdjustment API_AVAILABLE(ios(13.0)); diff --git a/QMUIKit/QMUICore/QMUIConfiguration.m b/QMUIKit/QMUICore/QMUIConfiguration.m index 8a320046..5c245699 100644 --- a/QMUIKit/QMUICore/QMUIConfiguration.m +++ b/QMUIKit/QMUICore/QMUIConfiguration.m @@ -119,31 +119,11 @@ - (void)applyInitialTemplate { } } - if (IS_DEBUG && self.sendAnalyticsToQMUITeam) { - [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification object:nil queue:[NSOperationQueue new] usingBlock:^(NSNotification * _Nonnull note) { - // 这里根据是否能成功获取到 classesref 来统计信息,以供后续确认对 classesref 为 nil 的保护是否真的必要 - [self sendAnalyticsWithQuery:classes ? @"findByObjc=true" : nil]; - }]; - } - if (classes) free(classes); QMUI_hasAppliedInitialTemplate = YES; } -- (void)sendAnalyticsWithQuery:(NSString *)query { - NSString *identifier = [NSBundle mainBundle].bundleIdentifier.qmui_stringByEncodingUserInputQuery; - NSString *displayName = ((NSString *)([NSBundle mainBundle].infoDictionary[@"CFBundleDisplayName"] ?: [NSBundle mainBundle].infoDictionary[@"CFBundleName"])).qmui_stringByEncodingUserInputQuery; - NSString *QMUIVersion = QMUI_VERSION.qmui_stringByEncodingUserInputQuery;// 如果不以 framework 方式引入 QMUI 的话,是无法通过 CFBundleShortVersionString 获取到 QMUI 所在的 bundle 的版本号的,所以这里改为用脚本生成的变量来获取 - NSString *queryString = [NSString stringWithFormat:@"appId=%@&appName=%@&version=%@&platform=iOS", identifier, displayName, QMUIVersion]; - if (query.length > 0) queryString = [NSString stringWithFormat:@"%@&%@", queryString, query]; - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://qmuiteam.com/analytics/usageReport"]]; - request.HTTPMethod = @"POST"; - request.HTTPBody = [queryString dataUsingEncoding:NSUTF8StringEncoding]; - NSURLSession *session = [NSURLSession sharedSession]; - [[session dataTaskWithRequest:request] resume]; -} - #pragma mark - Initialize default values - (void)initDefaultConfiguration { @@ -193,12 +173,16 @@ - (void)initDefaultConfiguration { self.navBarHighlightedAlpha = 0.2f; self.navBarDisabledAlpha = 0.2f; self.sizeNavBarBackIndicatorImageAutomatically = YES; - self.navBarCloseButtonImage = [UIImage qmui_imageWithShape:QMUIImageShapeNavClose size:CGSizeMake(16, 16) tintColor:self.navBarTintColor]; - self.navBarLoadingMarginRight = 3; self.navBarAccessoryViewMarginLeft = 5; self.navBarActivityIndicatorViewStyle = UIActivityIndicatorViewStyleGray; - self.navBarAccessoryViewTypeDisclosureIndicatorImage = [[UIImage qmui_imageWithShape:QMUIImageShapeTriangle size:CGSizeMake(8, 5) tintColor:self.navBarTitleColor] qmui_imageWithOrientation:UIImageOrientationDown]; + + // XCTest 会在 dispatch_once 里访问 UIScreen 引发死锁,所以屏蔽掉 + // https://github.com/Tencent/QMUI_iOS/issues/1479 + if (!IS_XCTEST) { + self.navBarCloseButtonImage = [UIImage qmui_imageWithShape:QMUIImageShapeNavClose size:CGSizeMake(16, 16) tintColor:self.navBarTintColor]; + self.navBarAccessoryViewTypeDisclosureIndicatorImage = [[UIImage qmui_imageWithShape:QMUIImageShapeTriangle size:CGSizeMake(8, 5) tintColor:self.navBarTitleColor] qmui_imageWithOrientation:UIImageOrientationDown]; + } #pragma mark - Toolbar @@ -277,7 +261,7 @@ - (void)initDefaultConfiguration { self.shouldPrintDefaultLog = YES; self.shouldPrintInfoLog = YES; self.shouldPrintWarnLog = YES; - self.shouldPrintQMUIWarnLogToConsole = IS_DEBUG; + self.shouldPrintQMUIWarnLogToConsole = IS_DEBUG && !IS_XCTEST; #pragma mark - QMUIBadge self.badgeOffset = QMUIBadgeInvalidateOffset; @@ -291,7 +275,6 @@ - (void)initDefaultConfiguration { self.needsBackBarButtonItemTitle = YES; self.preventConcurrentNavigationControllerTransitions = YES; self.shouldFixTabBarSafeAreaInsetsBug = YES; - self.sendAnalyticsToQMUITeam = YES; } #pragma mark - Switch Setter diff --git a/QMUIKit/QMUICore/QMUIConfigurationMacros.h b/QMUIKit/QMUICore/QMUIConfigurationMacros.h index 7fc368a3..9107b42f 100644 --- a/QMUIKit/QMUICore/QMUIConfigurationMacros.h +++ b/QMUIKit/QMUICore/QMUIConfigurationMacros.h @@ -272,7 +272,6 @@ #define NavigationBarHiddenInitially [QMUICMI navigationBarHiddenInitially] // preferredNavigationBarHidden 的初始值,默认为NO #define ShouldFixTabBarSafeAreaInsetsBug [QMUICMI shouldFixTabBarSafeAreaInsetsBug] // 是否要对 iOS 11 及以后的版本修复当存在 UITabBar 时,UIScrollView 的 inset.bottom 可能错误的 bug(issue #218 #934),默认为 YES #define ShouldFixSearchBarMaskViewLayoutBug [QMUICMI shouldFixSearchBarMaskViewLayoutBug] // 是否自动修复 UISearchController.searchBar 被当作 tableHeaderView 使用时可能出现的布局 bug(issue #950) -#define SendAnalyticsToQMUITeam [QMUICMI sendAnalyticsToQMUITeam] // 是否允许在 DEBUG 模式下上报 Bundle Identifier 和 Display Name 给 QMUI 统计用 #define DynamicPreferredValueForIPad [QMUICMI dynamicPreferredValueForIPad] // 当 iPad 处于 Slide Over 或 Split View 分屏模式下,宏 `PreferredValueForXXX` 是否把 iPad 视为某种屏幕宽度近似的 iPhone 来取值。 #define IgnoreKVCAccessProhibited [QMUICMI ignoreKVCAccessProhibited] // 是否全局忽略 iOS 13 对 KVC 访问 UIKit 私有属性的限制 #define AdjustScrollIndicatorInsetsByContentInsetAdjustment [QMUICMI adjustScrollIndicatorInsetsByContentInsetAdjustment] // 当将 UIScrollView.contentInsetAdjustmentBehavior 设为 UIScrollViewContentInsetAdjustmentNever 时,是否自动将 UIScrollView.automaticallyAdjustsScrollIndicatorInsets 设为 NO,以保证原本在 iOS 12 下的代码不用修改就能在 iOS 13 下正常控制滚动条的位置。 diff --git a/QMUIKit/QMUICore/QMUIHelper.h b/QMUIKit/QMUICore/QMUIHelper.h index 3bafb08c..1bdcced4 100644 --- a/QMUIKit/QMUICore/QMUIHelper.h +++ b/QMUIKit/QMUICore/QMUIHelper.h @@ -231,7 +231,7 @@ extern const CGPoint QMUIBadgeInvalidateOffset; @property(class, nonatomic, readonly) CGSize applicationSize; /** - 静态的导航栏高度,在导航栏不可见时也会根据机型返回导航栏的固定高度 + 静态的状态栏高度,在状态栏不可见时也会根据机型返回状态栏的固定高度 */ @property(class, nonatomic, readonly) CGFloat statusBarHeightConstant; diff --git a/QMUIKit/QMUIKit.h b/QMUIKit/QMUIKit.h index 918cdf61..42580c4f 100644 --- a/QMUIKit/QMUIKit.h +++ b/QMUIKit/QMUIKit.h @@ -13,7 +13,7 @@ #ifndef QMUIKit_h #define QMUIKit_h -static NSString * const QMUI_VERSION = @"4.6.3"; +static NSString * const QMUI_VERSION = @"4.7.0"; #if __has_include("CAAnimation+QMUI.h") #import "CAAnimation+QMUI.h" @@ -39,6 +39,10 @@ static NSString * const QMUI_VERSION = @"4.6.3"; #import "NSCharacterSet+QMUI.h" #endif +#if __has_include("NSDictionary+QMUI.h") +#import "NSDictionary+QMUI.h" +#endif + #if __has_include("NSMethodSignature+QMUI.h") #import "NSMethodSignature+QMUI.h" #endif @@ -655,4 +659,4 @@ static NSString * const QMUI_VERSION = @"4.6.3"; #import "UIWindow+QMUI.h" #endif -#endif /* QMUIKit_h */ \ No newline at end of file +#endif /* QMUIKit_h */ diff --git a/QMUIKit/QMUIMainFrame/QMUINavigationController.m b/QMUIKit/QMUIMainFrame/QMUINavigationController.m index c3b62ed7..5b32cc06 100644 --- a/QMUIKit/QMUIMainFrame/QMUINavigationController.m +++ b/QMUIKit/QMUIMainFrame/QMUINavigationController.m @@ -129,6 +129,7 @@ @implementation QMUINavigationController - (void)qmui_didInitialize { [super qmui_didInitialize]; + self.qmui_alwaysInvokeAppearanceMethods = YES; self.qmui_multipleDelegatesEnabled = YES; self.delegator = [[_QMUINavigationControllerDelegator alloc] init]; self.delegator.navigationController = self; @@ -313,16 +314,10 @@ - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)ani animated = NO; } - if (self.isViewLoaded) { - if (self.view.window) { - // 增加 self.view.window 作为判断条件是因为当 UINavigationController 不可见时(例如上面盖着一个 prenset 起来的 vc,或者 nav 所在的 tabBar 切到别的 tab 去了),pushViewController 会被执行,但 navigationController:didShowViewController:animated: 的 delegate 不会被触发,导致 isViewControllerTransiting 的标志位无法正确恢复,所以做个保护。 - // https://github.com/Tencent/QMUI_iOS/issues/261 - if (animated) { - self.isViewControllerTransiting = YES; - } - } else { - QMUILogWarn(NSStringFromClass(self.class), @"push 的时候 navigationController 不可见(例如上面盖着一个 prenset vc,或者切到别的 tab,可能导致一些 UINavigationControllerDelegate 不会被调用"); - } + // 增加 self.view.window 作为判断条件是因为当 UINavigationController 不可见时(例如上面盖着一个 present 起来的 vc,或者 nav 所在的 tabBar 切到别的 tab 去了),pushViewController 会被执行,但 navigationController:didShowViewController:animated: 的 delegate 不会被触发,导致 isViewControllerTransiting 的标志位无法正确恢复,所以做个保护。 + // https://github.com/Tencent/QMUI_iOS/issues/261 + if (animated && self.isViewLoaded && self.view.window) { + self.isViewControllerTransiting = YES; } // 在 push 前先设置好返回按钮的文字 diff --git a/QMUIKit/UIKitExtensions/CALayer+QMUI.h b/QMUIKit/UIKitExtensions/CALayer+QMUI.h index c05f5fad..6c3b8603 100644 --- a/QMUIKit/UIKitExtensions/CALayer+QMUI.h +++ b/QMUIKit/UIKitExtensions/CALayer+QMUI.h @@ -52,6 +52,18 @@ typedef NS_OPTIONS (NSUInteger, QMUICornerMask) { */ @property(nonatomic, strong, nullable) NSShadow *qmui_shadow; +/** + 只有当前 layer 里被返回的路径包裹住的内容才能被看到,路径之外的区域被裁剪掉。 + 该 block 会在 layer 大小发生变化时被调用,所以请根据 aLayer.bounds 计算实时的路径。 + */ +@property(nonatomic, copy, nullable) UIBezierPath * (^qmui_maskPathBlock)(__kindof CALayer *aLayer); + +/** + 与 qmui_maskPathBlock 相反,返回的路径会将当前 layer 的内容裁切掉,例如假设返回一个 layer 中间的矩形路径,则这个矩形会被挖空,其他区域正常显示。 + 该 block 会在 layer 大小发生变化时被调用,所以请根据 aLayer.bounds 计算实时的路径。 + */ +@property(nonatomic, copy, nullable) UIBezierPath * (^qmui_evenOddMaskPathBlock)(__kindof CALayer *aLayer); + /// 获取指定 name 值的 layer,包括 self 和 self.sublayers,会一直往 sublayers 查找直到找到目标 layer。 - (nullable __kindof CALayer *)qmui_layerWithName:(NSString *)name; diff --git a/QMUIKit/UIKitExtensions/CALayer+QMUI.m b/QMUIKit/UIKitExtensions/CALayer+QMUI.m index 225b2e68..8eff35fb 100644 --- a/QMUIKit/UIKitExtensions/CALayer+QMUI.m +++ b/QMUIKit/UIKitExtensions/CALayer+QMUI.m @@ -165,6 +165,65 @@ - (NSShadow *)qmui_shadow { return (NSShadow *)objc_getAssociatedObject(self, &kAssociatedObjectKey_shadow); } +static char kAssociatedObjectKey_maskPathBlock; +- (void)setQmui_maskPathBlock:(UIBezierPath * _Nonnull (^)(__kindof CALayer * _Nonnull))qmui_maskPathBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_maskPathBlock, qmui_maskPathBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (qmui_maskPathBlock) { + [CALayer qmui_hookMaskIfNeeded]; + CAShapeLayer *mask = CAShapeLayer.layer; + self.mask = mask; + [self setNeedsLayout]; + } else { + self.mask = nil; + } +} + +- (UIBezierPath * _Nonnull (^)(__kindof CALayer * _Nonnull))qmui_maskPathBlock { + return (UIBezierPath * _Nonnull (^)(__kindof CALayer * _Nonnull))objc_getAssociatedObject(self, &kAssociatedObjectKey_maskPathBlock); +} + +static char kAssociatedObjectKey_evenOddMaskPathBlock; +- (void)setQmui_evenOddMaskPathBlock:(UIBezierPath * _Nonnull (^)(__kindof CALayer * _Nonnull))qmui_evenOddMaskPathBlock { + objc_setAssociatedObject(self, &kAssociatedObjectKey_evenOddMaskPathBlock, qmui_evenOddMaskPathBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); + if (qmui_evenOddMaskPathBlock) { + [CALayer qmui_hookMaskIfNeeded]; + CAShapeLayer *mask = CAShapeLayer.layer; + mask.fillRule = kCAFillRuleEvenOdd; + self.mask = mask; + [self setNeedsLayout]; + } else { + self.mask = nil; + } +} + +- (UIBezierPath * _Nonnull (^)(__kindof CALayer * _Nonnull))qmui_evenOddMaskPathBlock { + return (UIBezierPath * _Nonnull (^)(__kindof CALayer * _Nonnull))objc_getAssociatedObject(self, &kAssociatedObjectKey_evenOddMaskPathBlock); +} + ++ (void)qmui_hookMaskIfNeeded { + [QMUIHelper executeBlock:^{ + OverrideImplementation([CALayer class], @selector(layoutSublayers), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(CALayer *selfObject) { + + // call super + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + + if (selfObject.qmui_maskPathBlock && [selfObject.mask isKindOfClass:CAShapeLayer.class]) { + ((CAShapeLayer *)selfObject.mask).path = selfObject.qmui_maskPathBlock(selfObject).CGPath; + } + if (selfObject.qmui_evenOddMaskPathBlock && [selfObject.mask isKindOfClass:CAShapeLayer.class]) { + UIBezierPath *path = [UIBezierPath bezierPathWithRect:selfObject.bounds]; + UIBezierPath *maskPath = selfObject.qmui_evenOddMaskPathBlock(selfObject); + [path appendPath:maskPath]; + ((CAShapeLayer *)selfObject.mask).path = path.CGPath; + } + }; + }); + } oncePerIdentifier:@"CALayer (QMUI) mask"]; +} + - (__kindof CALayer *)qmui_layerWithName:(NSString *)name { if ([self.name isEqualToString:name]) return self; for (CALayer *sublayer in self.sublayers) { diff --git a/QMUIKit/UIKitExtensions/NSArray+QMUI.h b/QMUIKit/UIKitExtensions/NSArray+QMUI.h index e3e2d30c..0e72afd2 100644 --- a/QMUIKit/UIKitExtensions/NSArray+QMUI.h +++ b/QMUIKit/UIKitExtensions/NSArray+QMUI.h @@ -48,10 +48,15 @@ NS_ASSUME_NONNULL_BEGIN - (ObjectType _Nullable)qmui_firstMatchWithBlock:(BOOL (NS_NOESCAPE^)(ObjectType item))block; /** -* 转换数组元素,将每个 item 都经过 block 转换成一遍 返回转换后的新数组 +* 转换数组元素,将每个 item 都经过 block 转换成一遍后返回一个等长的数组。 */ - (NSArray *)qmui_mapWithBlock:(id (NS_NOESCAPE^)(ObjectType item))block; +/** +* 转换数组元素,将每个 item 经过 block 转换为另一个元素,如果希望移除该 item,可返回 nil。当所有元素都被移除时,本方法返回空的容器。 +*/ +- (NSArray *)qmui_compactMapWithBlock:(id _Nullable (NS_NOESCAPE^)(ObjectType item))block; + @end NS_ASSUME_NONNULL_END diff --git a/QMUIKit/UIKitExtensions/NSArray+QMUI.m b/QMUIKit/UIKitExtensions/NSArray+QMUI.m index d32edc56..0bc394e0 100644 --- a/QMUIKit/UIKitExtensions/NSArray+QMUI.m +++ b/QMUIKit/UIKitExtensions/NSArray+QMUI.m @@ -108,4 +108,18 @@ - (NSArray *)qmui_mapWithBlock:(id (NS_NOESCAPE^)(id item))block { return [result copy]; } +- (NSArray *)qmui_compactMapWithBlock:(id _Nullable (NS_NOESCAPE^)(id _Nonnull))block { + if (!block) { + return self; + } + NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:self.count]; + for (NSInteger i = 0; i < self.count; i++) { + id item = block(self[i]); + if (item) { + [result addObject:item]; + } + } + return [result copy]; +} + @end diff --git a/QMUIKit/UIKitExtensions/NSDictionary+QMUI.h b/QMUIKit/UIKitExtensions/NSDictionary+QMUI.h new file mode 100644 index 00000000..74e6d175 --- /dev/null +++ b/QMUIKit/UIKitExtensions/NSDictionary+QMUI.h @@ -0,0 +1,38 @@ +/** + * Tencent is pleased to support the open source community by making QMUI_iOS available. + * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * http://opensource.org/licenses/MIT + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +// +// NSDictionary+QMUI.h +// QMUIKit +// +// Created by molice on 2023/7/21. +// Copyright © 2023 QMUI Team. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSDictionary (QMUI) + +/** +* 转换字典的元素,将每个 key-value 经过 block 转换为另一个 key-value,如果希望移除该 item,可返回 nil。当所有元素都被移除时,本方法返回空的容器。 + 对应 -[NSArray(QMUI) qmui_compactMapWithBlock],是觉得没必要区分 compact 和非 compact 了。 +*/ +- (NSDictionary * _Nullable)qmui_mapWithBlock:(NSDictionary * _Nullable (NS_NOESCAPE^)(KeyType key, ObjectType value))block; + +/** + 深度转换字典的元素,同 qmui_mapWithBlock:,但区别在于如果 object 是一个 NSDictionary,则它会递归再 map,最终把所有的 key-value 都转换一遍。 + + @warning 面对嵌套 dictionary 时,本方法的 block 里的参数 value 有可能会传 NSDictionary 类型,但实际上你对其转换后的返回值只有 key 会被使用,value 会被丢弃。 + */ +- (NSDictionary * _Nullable)qmui_deepMapWithBlock:(NSDictionary * _Nullable (NS_NOESCAPE^)(KeyType key, ObjectType value))block; + +@end + +NS_ASSUME_NONNULL_END diff --git a/QMUIKit/UIKitExtensions/NSDictionary+QMUI.m b/QMUIKit/UIKitExtensions/NSDictionary+QMUI.m new file mode 100644 index 00000000..72ff1974 --- /dev/null +++ b/QMUIKit/UIKitExtensions/NSDictionary+QMUI.m @@ -0,0 +1,65 @@ +/** + * Tencent is pleased to support the open source community by making QMUI_iOS available. + * Copyright (C) 2016-2021 THL A29 Limited, a Tencent company. All rights reserved. + * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + * http://opensource.org/licenses/MIT + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + */ + +// +// NSDictionary+QMUI.m +// QMUIKit +// +// Created by molice on 2023/7/21. +// Copyright © 2023 QMUI Team. All rights reserved. +// + +#import "NSDictionary+QMUI.h" + +@implementation NSDictionary (QMUI) + +- (NSDictionary *)qmui_mapWithBlock:(NSDictionary * _Nullable (NS_NOESCAPE^)(id _Nonnull, id _Nonnull))block { + if (!block) { + return self; + } + + NSMutableDictionary *temp = NSMutableDictionary.new; + [self enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { + NSDictionary *mapped = block(key, obj); + if (!mapped) { + return; + } + id k = mapped.allKeys.firstObject; + id o = mapped.allValues.firstObject; + temp[k] = o; + }]; + return temp.copy; +} + +- (NSDictionary *)qmui_deepMapWithBlock:(NSDictionary * _Nullable (NS_NOESCAPE^)(id _Nonnull, id _Nonnull))block { + if (!block) { + return self; + } + + NSMutableDictionary *temp = NSMutableDictionary.new; + [self enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { + if ([obj isKindOfClass:NSDictionary.class]) { + obj = [obj qmui_deepMapWithBlock:block]; + } + NSDictionary *mapped = block(key, obj); + if (!mapped) { + return; + } + id k = mapped.allKeys.firstObject; + id o = nil; + if ([obj isKindOfClass:NSDictionary.class]) { + o = obj;// 返回值 mapped.value 被丢弃了,实际上将 obj 作为 value + } else { + o = mapped.allValues.firstObject; + } + temp[k] = o; + }]; + return temp.copy; +} + +@end diff --git a/QMUIKit/UIKitExtensions/QMUIStringPrivate.m b/QMUIKit/UIKitExtensions/QMUIStringPrivate.m index 7b088422..ac452098 100644 --- a/QMUIKit/UIKitExtensions/QMUIStringPrivate.m +++ b/QMUIKit/UIKitExtensions/QMUIStringPrivate.m @@ -288,6 +288,23 @@ + (void)qmuisafety_NSString { return result; }; }); + + // 保护 -[NSMutableAttributedString appendAttributedString:] 遇到参数为 nil 时会命中系统 assert: nil argument 的场景 + // -[__NSCFString replaceCharactersInRange:withString:] + OverrideImplementation(NSClassFromString(@"__NSCFString"), @selector(replaceCharactersInRange:withString:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(NSString *selfObject, NSRange firstArgv, id secondArgv) { + + if (!secondArgv) { + QMUIAssert(NO, @"QMUIStringPrivate", @"replaceCharactersInRange:withString: 参数 nil 会命中系统 Assert 导致 crash"); + secondArgv = @""; + } + + // call super + void (*originSelectorIMP)(id, SEL, NSRange, id); + originSelectorIMP = (void (*)(id, SEL, NSRange, id))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, firstArgv, secondArgv); + }; + }); } + (void)qmuisafety_NSAttributedString { diff --git a/QMUIKit/UIKitExtensions/UILabel+QMUI.h b/QMUIKit/UIKitExtensions/UILabel+QMUI.h index da10c627..a4e2ce16 100644 --- a/QMUIKit/UIKitExtensions/UILabel+QMUI.h +++ b/QMUIKit/UIKitExtensions/UILabel+QMUI.h @@ -60,6 +60,12 @@ extern const CGFloat QMUILineHeightIdentity; */ @property(nonatomic, assign) CGFloat qmui_lineHeight; +/** + 获取当前 font.capHeight 的中心点在 label.bounds.size.height 里的y值(代表字符的中心点位置),从而令业务在试图将文本垂直居中时可以基于该属性的返回值去计算。 + @warning 仅对单行文本有意义 + */ +@property(nonatomic, assign, readonly) CGFloat qmui_centerOfCapHeight; + /** * 将目标UILabel的样式属性设置到当前UILabel上 * diff --git a/QMUIKit/UIKitExtensions/UILabel+QMUI.m b/QMUIKit/UIKitExtensions/UILabel+QMUI.m index e6d19b64..71e5ed93 100644 --- a/QMUIKit/UIKitExtensions/UILabel+QMUI.m +++ b/QMUIKit/UIKitExtensions/UILabel+QMUI.m @@ -42,11 +42,20 @@ + (void)load { for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); index++) { SEL originalSelector = selectors[index]; SEL swizzledSelector = NSSelectorFromString([@"qmuilb_" stringByAppendingString:NSStringFromSelector(originalSelector)]); - ExchangeImplementations([self class], originalSelector, swizzledSelector); + ExchangeImplementations([UILabel class], originalSelector, swizzledSelector); } }); } +- (instancetype)qmui_initWithFont:(UIFont *)font textColor:(UIColor *)textColor { + BeginIgnoreClangWarning(-Wunused-value) + [self init]; + EndIgnoreClangWarning + self.font = font; + self.textColor = textColor; + return self; +} + - (void)qmuilb_setText:(NSString *)text { if (!text) { [self qmuilb_setText:text]; @@ -240,13 +249,14 @@ - (BOOL)_hasSetQmuiLineHeight { return !!objc_getAssociatedObject(self, &kAssociatedObjectKey_lineHeight); } -- (instancetype)qmui_initWithFont:(UIFont *)font textColor:(UIColor *)textColor { - BeginIgnoreClangWarning(-Wunused-value) - [self init]; - EndIgnoreClangWarning - self.font = font; - self.textColor = textColor; - return self; +- (CGFloat)qmui_centerOfCapHeight { + NSRange range = NSMakeRange(0, self.attributedText.length); + UIFont *font = [self.attributedText attribute:NSFontAttributeName atIndex:0 effectiveRange:&range]; + if (!font) { + font = self.font; + } + CGFloat center = CGRectGetHeight(self.bounds) + font.descender - font.capHeight / 2; + return center; } - (void)qmui_setTheSameAppearanceAsLabel:(UILabel *)label { @@ -293,6 +303,7 @@ - (void)setQmui_showPrincipalLines:(BOOL)qmui_showPrincipalLines { if (!self.qmui_layoutSubviewsBlock) { self.qmui_layoutSubviewsBlock = ^(UILabel * _Nonnull label) { + if (!label.attributedText.length) return; if (!label.qmuilb_principalLineLayer || label.qmuilb_principalLineLayer.hidden) return; label.qmuilb_principalLineLayer.frame = label.bounds; diff --git a/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m b/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m index b0bde16f..8d82a777 100644 --- a/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m +++ b/QMUIKit/UIKitExtensions/UINavigationBar+QMUI.m @@ -341,7 +341,14 @@ + (void)load { originSelectorIMP(selfObject, originCMD, firstArgv); // 这里只希望识别 UINavigationController 自带的 navigationBar,不希望处理业务自己 new 的 bar,所以用 superview 是否为 UILayoutContainerView 来作为判断条件。 - if ([NSStringFromClass(selfObject.superview.class) hasPrefix:@"UILayoutContainer"] && !selfObject.window) { + BOOL isSystemBar = [NSStringFromClass(selfObject.superview.class) hasPrefix:@"UILayoutContainer"]; + BOOL alreadyMoveToWindow = !!selfObject.window; + BOOL isPresenting = NO; + if (!alreadyMoveToWindow) { + UINavigationController *nav = [selfObject.qmui_viewController isKindOfClass:UINavigationController.class] ? selfObject.qmui_viewController : nil; + isPresenting = nav && nav.presentedViewController; + } + if (isSystemBar && !alreadyMoveToWindow && !isPresenting) { QMUIAssert(NO, @"UINavigationBar (QMUI)", @"试图在 UINavigationBar 尚未添加到 window 上时就修改它的样式,可能导致 UINavigationBar 的样式无法与全局保持一致。"); } }; diff --git a/QMUIKit/UIKitExtensions/UINavigationController+QMUI.h b/QMUIKit/UIKitExtensions/UINavigationController+QMUI.h index 87483b3d..1a80c2d0 100644 --- a/QMUIKit/UIKitExtensions/UINavigationController+QMUI.h +++ b/QMUIKit/UIKitExtensions/UINavigationController+QMUI.h @@ -57,12 +57,19 @@ typedef void (^QMUINavigationActionDidChangeBlock)(QMUINavigationAction action, */ - (void)qmui_addNavigationActionDidChangeBlock:(QMUINavigationActionDidChangeBlock)block; +/// 系统的设定是当 UINavigationController 不可见时(例如上面盖着一个 present vc,或者切到别的 tab),push/pop 操作均不会调用 vc 的生命周期方法(viewDidLoad 也是在 nav 恢复可视时才触发),所以提供这个属性用于当你希望这种情况下依然调用生命周期方法时,你可以打开它。默认为 NO。 +/// @warning 由于强制在 push/pop 时触发生命周期方法,所以会导致 vc 的 viewDidLoad 等方法比系统默认的更早调用,知悉即可。 +@property(nonatomic, assign) BOOL qmui_alwaysInvokeAppearanceMethods; + /// 是否在 push 的过程中 @property(nonatomic, readonly) BOOL qmui_isPushing; /// 是否在 pop 的过程中,包括手势、以及代码触发的 pop @property(nonatomic, readonly) BOOL qmui_isPopping; +/// 以系统私有方法的方式去判断当前正在进行 push 动画还是 pop 动画,注意 setViewControllers 直接表现也是 push 或 pop 动画,可以通过 qmui_lastOperation 得知,但 qmui_isPushing、qmui_isPopping 无法区分 setViewControllers 的情况。 +@property(nonatomic, readonly) UINavigationControllerOperation qmui_lastOperation; + /// 获取顶部的 ViewController,相比于系统的方法,这个方法能获取到 pop 的转场过程中顶部还没有完全消失的 ViewController (请注意:这种情况下,获取到的 topViewController 已经不在栈内) @property(nullable, nonatomic, readonly) UIViewController *qmui_topViewController; diff --git a/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m b/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m index f530efa7..27adafe1 100644 --- a/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m +++ b/QMUIKit/UIKitExtensions/UINavigationController+QMUI.m @@ -34,6 +34,7 @@ @interface UINavigationController () @implementation UINavigationController (QMUI) +QMUISynthesizeBOOLProperty(qmui_alwaysInvokeAppearanceMethods, setQmui_alwaysInvokeAppearanceMethods) QMUISynthesizeIdStrongProperty(qmuinc_navigationActionDidChangeBlocks, setQmuinc_navigationActionDidChangeBlocks) QMUISynthesizeIdWeakProperty(qmui_endedTransitionTopViewController, setQmui_endedTransitionTopViewController) QMUISynthesizeIdWeakProperty(qmui_interactivePopGestureRecognizerDelegate, setQmui_interactivePopGestureRecognizerDelegate) @@ -175,8 +176,13 @@ + (void)load { OverrideImplementation([UINavigationController class], @selector(pushViewController:animated:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UINavigationController *selfObject, UIViewController *viewController, BOOL animated) { - if (selfObject.presentedViewController) { - QMUILogWarn(NSStringFromClass(originClass), @"push 的时候 UINavigationController 存在一个盖在上面的 presentedViewController,可能导致一些 UINavigationControllerDelegate 不会被调用"); + BOOL shouldInvokeAppearanceMethod = NO; + + if (selfObject.isViewLoaded && !selfObject.view.window) { + QMUILogWarn(NSStringFromClass(originClass), @"push 的时候 navigationController 不可见(例如上面盖着一个 present vc,或者切到别的 tab,可能导致 vc 的生命周期方法或者 UINavigationControllerDelegate 不会被调用"); + if (selfObject.qmui_alwaysInvokeAppearanceMethods) { + shouldInvokeAppearanceMethod = YES; + } } if ([selfObject.viewControllers containsObject:viewController]) { @@ -194,6 +200,7 @@ + (void)load { BOOL willPushActually = viewController && ![viewController isKindOfClass:UITabBarController.class] && ![selfObject.viewControllers containsObject:viewController]; if (!willPushActually) { + QMUIAssert(NO, @"UINavigationController (QMUI)", @"调用了 pushViewController 但实际上没 push 成功,viewController:%@", viewController); callSuperBlock(); return; } @@ -203,6 +210,11 @@ + (void)load { [selfObject setQmui_navigationAction:QMUINavigationActionWillPush animated:animated appearingViewController:appearingViewController disappearingViewControllers:disappearingViewControllers]; + if (shouldInvokeAppearanceMethod) { + [disappearingViewControllers.lastObject beginAppearanceTransition:NO animated:animated]; + [appearingViewController beginAppearanceTransition:YES animated:animated]; + } + callSuperBlock(); [selfObject setQmui_navigationAction:QMUINavigationActionDidPush animated:animated appearingViewController:appearingViewController disappearingViewControllers:disappearingViewControllers]; @@ -210,6 +222,11 @@ + (void)load { [selfObject qmui_animateAlongsideTransition:nil completion:^(id _Nonnull context) { [selfObject setQmui_navigationAction:QMUINavigationActionPushCompleted animated:animated appearingViewController:appearingViewController disappearingViewControllers:disappearingViewControllers]; [selfObject setQmui_navigationAction:QMUINavigationActionUnknow animated:animated appearingViewController:nil disappearingViewControllers:nil]; + + if (shouldInvokeAppearanceMethod) { + [disappearingViewControllers.lastObject endAppearanceTransition]; + [appearingViewController endAppearanceTransition]; + } }]; }; }); @@ -236,11 +253,25 @@ + (void)load { return callSuperBlock(); } + BOOL shouldInvokeAppearanceMethod = NO; + + if (selfObject.isViewLoaded && !selfObject.view.window) { + QMUILogWarn(NSStringFromClass(originClass), @"pop 的时候 navigationController 不可见(例如上面盖着一个 present vc,或者切到别的 tab,可能导致 vc 的生命周期方法或者 UINavigationControllerDelegate 不会被调用"); + if (selfObject.qmui_alwaysInvokeAppearanceMethods) { + shouldInvokeAppearanceMethod = YES; + } + } + UIViewController *appearingViewController = selfObject.viewControllers[selfObject.viewControllers.count - 2]; NSArray *disappearingViewControllers = selfObject.viewControllers.lastObject ? @[selfObject.viewControllers.lastObject] : nil; [selfObject setQmui_navigationAction:QMUINavigationActionWillPop animated:animated appearingViewController:appearingViewController disappearingViewControllers:disappearingViewControllers]; + if (shouldInvokeAppearanceMethod) { + [disappearingViewControllers.lastObject beginAppearanceTransition:NO animated:animated]; + [appearingViewController beginAppearanceTransition:YES animated:animated]; + } + UIViewController *result = callSuperBlock(); // UINavigationController 不可见时 return 值可能为 nil @@ -253,6 +284,11 @@ + (void)load { void (^transitionCompletion)(void) = ^void(void) { [selfObject setQmui_navigationAction:QMUINavigationActionPopCompleted animated:animated appearingViewController:appearingViewController disappearingViewControllers:disappearingViewControllers]; [selfObject setQmui_navigationAction:QMUINavigationActionUnknow animated:animated appearingViewController:nil disappearingViewControllers:nil]; + + if (shouldInvokeAppearanceMethod) { + [disappearingViewControllers.lastObject endAppearanceTransition]; + [appearingViewController endAppearanceTransition]; + } }; if (!result) { // 如果系统的 pop 没有成功,实际上提交给 animateAlongsideTransition:completion: 的 completion 并不会被执行,所以这里改为手动调用 @@ -423,18 +459,27 @@ - (void)qmui_addNavigationActionDidChangeBlock:(QMUINavigationActionDidChangeBlo [self.qmuinc_navigationActionDidChangeBlocks addObject:block]; } -// TODO: molice 改为用 QMUINavigationAction 判断 - (BOOL)qmui_isPushing { BOOL isPushing = self.qmui_navigationAction > QMUINavigationActionWillPush && self.qmui_navigationAction <= QMUINavigationActionPushCompleted; return isPushing; } -// TODO: molice 改为用 QMUINavigationAction 判断 - (BOOL)qmui_isPopping { BOOL isPopping = self.qmui_navigationAction > QMUINavigationActionWillPop && self.qmui_navigationAction <= QMUINavigationActionPopCompleted; return isPopping; } +- (UINavigationControllerOperation)qmui_lastOperation { + // -[UINavigationController lastOperation] + SEL operationSEL = NSSelectorFromString([NSString qmui_stringByConcat:@"last", @"Operation", nil]); + if ([self respondsToSelector:operationSEL]) { + UINavigationControllerOperation operation = UINavigationControllerOperationNone; + [self qmui_performSelector:operationSEL withPrimitiveReturnValue:&operation]; + return operation; + } + return UINavigationControllerOperationNone; +} + - (UIViewController *)qmui_topViewController { if (self.qmui_isPushing) { return self.topViewController; @@ -526,7 +571,11 @@ - (BOOL)_gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiv if ([originGestureDelegate respondsToSelector:_cmd]) { BOOL originalValue = YES; [originGestureDelegate qmui_performSelector:_cmd withPrimitiveReturnValue:&originalValue arguments:&gestureRecognizer, &event, nil]; - if (!originalValue && [self.parentViewController shouldForceEnableInteractivePopGestureRecognizer]) { + if (!originalValue + // 在开启 forceEnableInteractivePopGestureRecognizer 的界面被 push 的过程中快速手势返回,容易导致 App 卡死 + // https://github.com/Tencent/QMUI_iOS/issues/1498 + && self.parentViewController.qmui_navigationAction == QMUINavigationActionUnknow + && [self.parentViewController shouldForceEnableInteractivePopGestureRecognizer]) { return YES; } return originalValue; diff --git a/QMUIKit/UIKitExtensions/UISearchBar+QMUI.m b/QMUIKit/UIKitExtensions/UISearchBar+QMUI.m index 3ed6068d..0b64576e 100644 --- a/QMUIKit/UIKitExtensions/UISearchBar+QMUI.m +++ b/QMUIKit/UIKitExtensions/UISearchBar+QMUI.m @@ -17,6 +17,7 @@ #import "QMUICore.h" #import "UIImage+QMUI.h" #import "UIView+QMUI.h" +#import "UIViewController+QMUI.h" @interface UISearchBar () @@ -490,7 +491,10 @@ - (void)qmui_setShowsLeftAccessoryView:(BOOL)showsLeftAccessoryView animated:(BO self.qmui_leftAccessoryView.transform = CGAffineTransformMakeTranslation(-CGRectGetMaxX(self.qmui_leftAccessoryView.frame), 0); [self qmuisb_updateCustomTextFieldMargins]; } completion:^(BOOL finished) { - self.qmui_leftAccessoryView.hidden = YES; + // 快速在 show/hide 之间切换,容易出现状态错误,所以做个保护 + if (showsLeftAccessoryView == self.qmui_showsLeftAccessoryView) { + self.qmui_leftAccessoryView.hidden = YES; + } self.qmui_leftAccessoryView.transform = CGAffineTransformIdentity; }]; } @@ -567,7 +571,10 @@ - (void)qmui_setShowsRightAccessoryView:(BOOL)showsRightAccessoryView animated:( self.qmui_rightAccessoryView.transform = CGAffineTransformMakeTranslation(CGRectGetWidth(self.qmui_rightAccessoryView.superview.bounds) - CGRectGetMinX(self.qmui_rightAccessoryView.frame), 0); [self qmuisb_updateCustomTextFieldMargins]; } completion:^(BOOL finished) { - self.qmui_rightAccessoryView.hidden = YES; + // 快速在 show/hide 之间切换,容易出现状态错误,所以做个保护 + if (showsRightAccessoryView == self.qmui_showsRightAccessoryView) { + self.qmui_rightAccessoryView.hidden = YES; + } self.qmui_rightAccessoryView.transform = CGAffineTransformIdentity; self.qmui_rightAccessoryView.alpha = 1; }]; @@ -814,6 +821,9 @@ - (void)qmuisb_fixDismissingAnimationIfNeeded { } } +// UISearchController.searchBar 作为 UITableView.tableHeaderView 时,进入搜索状态,搜索结果列表顶部有一大片空白 +// 不要让系统自适应了,否则在搜索结果(navigationBar 隐藏)push 进入下一级界面(navigationBar 显示)过程中系统自动调整的 contentInset 会跳来跳去 +// https://github.com/Tencent/QMUI_iOS/issues/1473 - (void)qmuisb_fixSearchResultsScrollViewContentInsetIfNeeded { if (!self.qmuisb_shouldFixLayoutWhenUsedAsTableHeaderView) return; if (self.qmui_isActive) { @@ -824,8 +834,10 @@ - (void)qmuisb_fixSearchResultsScrollViewContentInsetIfNeeded { [view isKindOfClass:UIScrollView.class] ? view : [view.subviews.firstObject isKindOfClass:UIScrollView.class] ? view.subviews.firstObject : nil; UIView *searchBarContainerView = self.superview; - if (scrollView && searchBarContainerView) { - scrollView.contentInset = UIEdgeInsetsMake(searchBarContainerView.qmui_height, 0, 0, 0); + if (scrollView && scrollView.contentInsetAdjustmentBehavior == UIScrollViewContentInsetAdjustmentNever && searchBarContainerView) { + CGFloat containerHeight = CGRectGetHeight(searchBarContainerView.frame); + scrollView.contentInset = UIEdgeInsetsMake(containerHeight, 0, scrollView.safeAreaInsets.bottom, 0); + scrollView.scrollIndicatorInsets = scrollView.contentInset; } } } diff --git a/QMUIKit/UIKitExtensions/UITextField+QMUI.h b/QMUIKit/UIKitExtensions/UITextField+QMUI.h index 74a1e7c6..5d4ced27 100644 --- a/QMUIKit/UIKitExtensions/UITextField+QMUI.h +++ b/QMUIKit/UIKitExtensions/UITextField+QMUI.h @@ -20,6 +20,10 @@ NS_ASSUME_NONNULL_BEGIN @interface UITextField (QMUI) +/// UITextView 在输入框开头继续按删除按键,也会触发 shouldChange 的 delegate,但 UITextField 没这个行为,所以提供这个属性,当置为 YES 时,行为与 UITextView 一致,在输入框开头删除也会询问 delegate 并传 range(0, 0) 和空的 text。 +/// 默认为 NO。 +@property(nonatomic, assign) BOOL qmui_respondsToDeleteActionAtLeading; + /// UITextField 只有 selectedTextRange 属性(在 UITextInput 协议里定义),相对而言没有 NSRange 那么直观,因此这里提供 NSRange 类型的操作方式可以主动设置光标的位置或选中的区域 @property(nonatomic, assign) NSRange qmui_selectedRange; diff --git a/QMUIKit/UIKitExtensions/UITextField+QMUI.m b/QMUIKit/UIKitExtensions/UITextField+QMUI.m index 8e99c188..3ac2ea59 100644 --- a/QMUIKit/UIKitExtensions/UITextField+QMUI.m +++ b/QMUIKit/UIKitExtensions/UITextField+QMUI.m @@ -20,6 +20,33 @@ @implementation UITextField (QMUI) ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + + // iOS 13 及以下版本需要重写该方法才能替换 + // - (id) _clearButtonImageForState:(unsigned long)arg1; + // https://github.com/Tencent/QMUI_iOS/issues/1477 + if (@available(iOS 14.0, *)) { + } else { + OverrideImplementation([UITextField class], NSSelectorFromString(@"_clearButtonImageForState:"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^UIImage *(UITextField *selfObject, UIControlState firstArgv) { + + if (selfObject.qmui_clearButtonImage && (firstArgv & UIControlStateNormal) == UIControlStateNormal) { + return selfObject.qmui_clearButtonImage; + } + + // call super + UIImage *(*originSelectorIMP)(id, SEL, UIControlState); + originSelectorIMP = (UIImage *(*)(id, SEL, UIControlState))originalIMPProvider(); + UIImage *result = originSelectorIMP(selfObject, originCMD, firstArgv); + return result; + }; + }); + } + }); +} + - (void)setQmui_selectedRange:(NSRange)qmui_selectedRange { self.selectedTextRange = [self qmui_convertUITextRangeFromNSRange:qmui_selectedRange]; } @@ -32,14 +59,15 @@ - (UIButton *)qmui_clearButton { return [self qmui_valueForKey:@"clearButton"]; } -// - (id) _clearButtonImageForState:(unsigned long)arg1; static char kAssociatedObjectKey_clearButtonImage; - (void)setQmui_clearButtonImage:(UIImage *)qmui_clearButtonImage { objc_setAssociatedObject(self, &kAssociatedObjectKey_clearButtonImage, qmui_clearButtonImage, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - [self.qmui_clearButton setImage:qmui_clearButtonImage forState:UIControlStateNormal]; - // 如果当前 clearButton 正在显示的时候把自定义图片去掉,需要重新 layout 一次才能让系统默认图片显示出来 - if (!qmui_clearButtonImage) { - [self setNeedsLayout]; + if (@available(iOS 14.0, *)) { + [self.qmui_clearButton setImage:qmui_clearButtonImage forState:UIControlStateNormal]; + // 如果当前 clearButton 正在显示的时候把自定义图片去掉,需要重新 layout 一次才能让系统默认图片显示出来 + if (!qmui_clearButtonImage) { + [self setNeedsLayout]; + } } } @@ -63,4 +91,32 @@ - (UITextRange *)qmui_convertUITextRangeFromNSRange:(NSRange)range { return [self textRangeFromPosition:startPosition toPosition:endPosition]; } +static char kAssociatedObjectKey_respondsToDeleteActionAtLeading; +- (void)setQmui_respondsToDeleteActionAtLeading:(BOOL)respondsToDeleteActionAtLeading { + objc_setAssociatedObject(self, &kAssociatedObjectKey_respondsToDeleteActionAtLeading, @(respondsToDeleteActionAtLeading), OBJC_ASSOCIATION_RETAIN_NONATOMIC); + [QMUIHelper executeBlock:^{ + OverrideImplementation([UITextField class], @selector(deleteBackward), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UITextField *selfObject) { + + BOOL deletingAtLeading = NSEqualRanges(selfObject.qmui_selectedRange, NSMakeRange(0, 0)); + if (selfObject.qmui_respondsToDeleteActionAtLeading && deletingAtLeading) { + QMUILog(@"UITextField (QMUI)", @"光标已在输入框开头的情况下依然按下删除按键。"); + if ([selfObject.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:)]) { + [selfObject.delegate textField:selfObject shouldChangeCharactersInRange:NSMakeRange(0, 0) replacementString:@""]; + } + } + + // call super + void (*originSelectorIMP)(id, SEL); + originSelectorIMP = (void (*)(id, SEL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD); + }; + }); + } oncePerIdentifier:@"UITextField (QMUI) delete"]; +} + +- (BOOL)qmui_respondsToDeleteActionAtLeading { + return [((NSNumber *)objc_getAssociatedObject(self, &kAssociatedObjectKey_respondsToDeleteActionAtLeading)) boolValue]; +} + @end diff --git a/QMUIKit/UIKitExtensions/UIView+QMUI.h b/QMUIKit/UIKitExtensions/UIView+QMUI.h index dde51154..fcb9e902 100644 --- a/QMUIKit/UIKitExtensions/UIView+QMUI.h +++ b/QMUIKit/UIKitExtensions/UIView+QMUI.h @@ -44,7 +44,9 @@ NS_ASSUME_NONNULL_BEGIN */ @property(nonatomic, assign, readonly) BOOL qmui_tintColorCustomized; -/// 响应区域需要改变的大小,负值表示往外扩大,正值表示往内缩小。特别地,如果对 UISlider 使用,则扩大的是圆点的区域。 +/// 响应区域需要改变的大小,负值表示往外扩大,正值表示往内缩小。 +/// 特别地,如果对 UISlider 使用,则扩大的是圆点的区域。 +/// 当你引入了 QMUINavigationButton,它会使 UIBarButtonItem.customView 也可使用 qmui_outsideEdge(默认不可以,因为 customView 的父容器和 customView 一样大,所以 UINavigationBar 感知不到 customView 有 qmui_outsideEdge)。 @property(nonatomic,assign) UIEdgeInsets qmui_outsideEdge; /** diff --git a/QMUIKit/UIKitExtensions/UIView+QMUI.m b/QMUIKit/UIKitExtensions/UIView+QMUI.m index f8f7c0bd..91f882e1 100644 --- a/QMUIKit/UIKitExtensions/UIView+QMUI.m +++ b/QMUIKit/UIKitExtensions/UIView+QMUI.m @@ -682,7 +682,7 @@ - (void)setQmui_shouldShowDebugColor:(BOOL)qmui_shouldShowDebugColor { [selfObject renderColorWithSubviews:selfObject.subviews]; } else if (objc_getAssociatedObject(selfObject, &kAssociatedObjectKey_shouldShowDebugColor)) { // 设置过 qmui_shouldShowDebugColor,但当前的值为 NO 的情况,则无脑清空所有背景色(可能会把业务自己设置的背景色去掉,由于是调试功能,无所谓) - selfObject.backgroundColor = nil; + selfObject.backgroundColor = UIColor.clearColor; [selfObject renderColorWithSubviews:selfObject.subviews]; } }); @@ -764,7 +764,7 @@ - (void)renderColorWithSubviews:(NSArray *)subviews { if (view.qmui_shouldShowDebugColor) { view.backgroundColor = [view debugColor]; } else { - view.backgroundColor = nil; + view.backgroundColor = UIColor.clearColor; } } } diff --git a/QMUIKit/UIKitExtensions/UIViewController+QMUI.h b/QMUIKit/UIKitExtensions/UIViewController+QMUI.h index e852c4dd..51848248 100644 --- a/QMUIKit/UIKitExtensions/UIViewController+QMUI.h +++ b/QMUIKit/UIKitExtensions/UIViewController+QMUI.h @@ -147,6 +147,7 @@ typedef NS_OPTIONS(NSUInteger, QMUIViewControllerVisibleState) { @interface UIViewController (Data) /// 当数据加载完(什么时候算是“加载完”需要通过属性 qmui_dataLoaded 来设置)并且界面已经走过 viewDidAppear: 时,这个 block 会被执行,执行结束后 block 会被清空,以避免重复调用。 +/// @warning 注意,如果你在 viewWillAppear: 里设置该 block,则要留意在下一级界面手势返回触发后又取消,会触发前一个界面的 viewWillAppear:、viewDidDisappear:,过程中不会触发 viewDidAppear:,所以这次设置的 block 并没有人消费它。 @property(nullable, nonatomic, copy) void (^qmui_didAppearAndLoadDataBlock)(void); /// 请在你的数据加载完成时手动修改这个属性为 YES,如果此时界面已经走过 viewDidAppear:,则 qmui_didAppearAndLoadDataBlock 会被立即执行,如果此时界面尚未走 viewDidAppear:,则等到 viewDidAppear: 时,qmui_didAppearAndLoadDataBlock 就会被自动执行。 diff --git a/QMUIKit/UIKitExtensions/UIViewController+QMUI.m b/QMUIKit/UIKitExtensions/UIViewController+QMUI.m index 44d43385..803133dc 100644 --- a/QMUIKit/UIKitExtensions/UIViewController+QMUI.m +++ b/QMUIKit/UIKitExtensions/UIViewController+QMUI.m @@ -479,11 +479,19 @@ @implementation UIViewController (Data) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - ExtendImplementationOfVoidMethodWithSingleArgument([UIViewController class], @selector(viewDidAppear:), BOOL, ^(UIViewController *selfObject, BOOL animated) { - if (selfObject.qmui_didAppearAndLoadDataBlock && selfObject.qmui_dataLoaded) { - selfObject.qmui_didAppearAndLoadDataBlock(); - selfObject.qmui_didAppearAndLoadDataBlock = nil; - } + OverrideImplementation([UIViewController class], @selector(viewDidAppear:), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { + return ^(UIViewController *selfObject, BOOL animated) { + + // call super + void (*originSelectorIMP)(id, SEL, BOOL); + originSelectorIMP = (void (*)(id, SEL, BOOL))originalIMPProvider(); + originSelectorIMP(selfObject, originCMD, animated); + + if (selfObject.qmui_didAppearAndLoadDataBlock && selfObject.qmui_dataLoaded) { + selfObject.qmui_didAppearAndLoadDataBlock(); + selfObject.qmui_didAppearAndLoadDataBlock = nil; + } + }; }); }); } diff --git a/README.md b/README.md index 598b0630..78decd0e 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,6 @@ QMUI iOS 是一个致力于提高项目 UI 开发效率的解决方案,其设计目的是用于辅助快速搭建一个具备基本设计还原效果的 iOS 项目,同时利用自身提供的丰富控件及兼容处理, 让开发者能专注于业务需求而无需耗费精力在基础代码的设计上。不管是新项目的创建,或是已有项目的维护,均可使开发效率和项目质量得到大幅度提升。 -官网:[http://qmuiteam.com/ios](http://qmuiteam.com/ios) - [![QMUI Team Name](https://img.shields.io/badge/Team-QMUI-brightgreen.svg?style=flat)](https://github.com/QMUI "QMUI Team") [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](http://opensource.org/licenses/MIT "Feel free to contribute.") @@ -37,7 +35,9 @@ QMUI iOS 是一个致力于提高项目 UI 开发效率的解决方案,其设 ## 使用方法 -请查看官网的[开始使用](http://qmuiteam.com/ios/page/start.html)。 +``` +pod 'QMUIKit' +``` ## 代码示例 @@ -57,9 +57,7 @@ QMUI iOS 是一个致力于提高项目 UI 开发效率的解决方案,其设 ## 隐私政策 -如果你想了解使用 QMUI iOS 过程中涉及到的隐私政策,可阅读:[QMUI iOS SDK 个人信息保护规则](https://qmuiteam.com/ios/privacy/)。 - -其中特别注意的是,从 2.8.0 版本开始,QMUIKit 默认会在 Debug 模式下启动 App 时发送当前 App 的 Bundle Identifier 和 Display Name 给 QMUI 作统计用,Release 下不会发送。你也可以通过配置表的 `SendAnalyticsToQMUITeam` 开关将统计关闭。统计的代码在 [QMUIConfiguration.m:134-145](https://github.com/Tencent/QMUI_iOS/blob/master/QMUIKit/QMUICore/QMUIConfiguration.m#L134-L145),可直接查看。 +如果你想了解使用 QMUI iOS 过程中涉及到的隐私政策,可阅读:[QMUI iOS SDK 个人信息保护规则](https://github.com/Tencent/QMUI_iOS/wiki/QMUI-iOS-SDK%E4%B8%AA%E4%BA%BA%E4%BF%A1%E6%81%AF%E4%BF%9D%E6%8A%A4%E8%A7%84%E5%88%99)。 ## 设计资源 diff --git a/qmui.xcodeproj/project.pbxproj b/qmui.xcodeproj/project.pbxproj index bbfa853f..43ec7005 100644 --- a/qmui.xcodeproj/project.pbxproj +++ b/qmui.xcodeproj/project.pbxproj @@ -77,6 +77,8 @@ CD7A9A0D22C4AA2F0093DAB4 /* QMUIThemeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CD7A9A0C22C4AA2F0093DAB4 /* QMUIThemeTests.m */; }; CD7D402F231FA2900007DF6C /* QMUIThemeManagerCenter.h in Headers */ = {isa = PBXBuildFile; fileRef = CD7D402D231FA2900007DF6C /* QMUIThemeManagerCenter.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD7D4030231FA2900007DF6C /* QMUIThemeManagerCenter.m in Sources */ = {isa = PBXBuildFile; fileRef = CD7D402E231FA2900007DF6C /* QMUIThemeManagerCenter.m */; }; + CD81252A2A69CB5900132EC7 /* NSDictionary+QMUI.h in Headers */ = {isa = PBXBuildFile; fileRef = CD8125282A69CB5900132EC7 /* NSDictionary+QMUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CD81252B2A69CB5900132EC7 /* NSDictionary+QMUI.m in Sources */ = {isa = PBXBuildFile; fileRef = CD8125292A69CB5900132EC7 /* NSDictionary+QMUI.m */; }; CD82C0AF206A2C3D0046EED2 /* QMUIMultipleDelegates.h in Headers */ = {isa = PBXBuildFile; fileRef = CD82C0AD206A2C3D0046EED2 /* QMUIMultipleDelegates.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD82C0B0206A2C3D0046EED2 /* QMUIMultipleDelegates.m in Sources */ = {isa = PBXBuildFile; fileRef = CD82C0AE206A2C3D0046EED2 /* QMUIMultipleDelegates.m */; }; CD82C0B3206A82520046EED2 /* NSObject+QMUIMultipleDelegates.h in Headers */ = {isa = PBXBuildFile; fileRef = CD82C0B1206A82520046EED2 /* NSObject+QMUIMultipleDelegates.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -426,6 +428,8 @@ CD7A9A0C22C4AA2F0093DAB4 /* QMUIThemeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QMUIThemeTests.m; sourceTree = ""; }; CD7D402D231FA2900007DF6C /* QMUIThemeManagerCenter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIThemeManagerCenter.h; sourceTree = ""; }; CD7D402E231FA2900007DF6C /* QMUIThemeManagerCenter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIThemeManagerCenter.m; sourceTree = ""; }; + CD8125282A69CB5900132EC7 /* NSDictionary+QMUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+QMUI.h"; sourceTree = ""; }; + CD8125292A69CB5900132EC7 /* NSDictionary+QMUI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+QMUI.m"; sourceTree = ""; }; CD82C0AD206A2C3D0046EED2 /* QMUIMultipleDelegates.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QMUIMultipleDelegates.h; sourceTree = ""; }; CD82C0AE206A2C3D0046EED2 /* QMUIMultipleDelegates.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QMUIMultipleDelegates.m; sourceTree = ""; }; CD82C0B1206A82520046EED2 /* NSObject+QMUIMultipleDelegates.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSObject+QMUIMultipleDelegates.h"; sourceTree = ""; }; @@ -934,6 +938,8 @@ CDB8CA791DCC870700769DF0 /* NSAttributedString+QMUI.m */, CDA4083C214F7E2500740888 /* NSCharacterSet+QMUI.h */, CDA4083D214F7E2500740888 /* NSCharacterSet+QMUI.m */, + CD8125282A69CB5900132EC7 /* NSDictionary+QMUI.h */, + CD8125292A69CB5900132EC7 /* NSDictionary+QMUI.m */, CD4EA4BD2275FA0100A55066 /* NSMethodSignature+QMUI.h */, CD4EA4BE2275FA0100A55066 /* NSMethodSignature+QMUI.m */, CD1817E32010CC4000F8CDEC /* NSNumber+QMUI.h */, @@ -1307,6 +1313,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + CD81252A2A69CB5900132EC7 /* NSDictionary+QMUI.h in Headers */, CD96A2B928C74CCA00E87728 /* NSShadow+QMUI.h in Headers */, CD513E2D283527CE004A549D /* UITabBar+QMUIBarProtocol.h in Headers */, CD513E31283527DB004A549D /* UINavigationBar+QMUIBarProtocol.h in Headers */, @@ -1619,6 +1626,7 @@ CD349BAE2160AF75008653D4 /* QMUIScrollAnimator.m in Sources */, CDB8CB601DCC870700769DF0 /* NSParagraphStyle+QMUI.m in Sources */, CDB8CBB81DCC870800769DF0 /* UIImage+QMUI.m in Sources */, + CD81252B2A69CB5900132EC7 /* NSDictionary+QMUI.m in Sources */, CDB8CB581DCC870700769DF0 /* NSAttributedString+QMUI.m in Sources */, CDB8CB5C1DCC870700769DF0 /* NSObject+QMUI.m in Sources */, CDB8CBA41DCC870700769DF0 /* UIButton+QMUI.m in Sources */, @@ -1973,7 +1981,7 @@ "@loader_path/Frameworks", ); MACH_O_TYPE = mh_dylib; - MARKETING_VERSION = 4.6.3; + MARKETING_VERSION = 4.7.0; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = com.qmui.QMUIKit; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -2017,7 +2025,7 @@ "@loader_path/Frameworks", ); MACH_O_TYPE = mh_dylib; - MARKETING_VERSION = 4.6.3; + MARKETING_VERSION = 4.7.0; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = com.qmui.QMUIKit; PRODUCT_NAME = "$(TARGET_NAME)";